Skip to content

Commit 3c954cb

Browse files
committed
refactor: centralize export path computation to shared library helper
IMPLEMENTATION (De-duplication Goal) - Create compute_export_paths() helper in export.rs as single source of truth - Helper returns (csv, headers, gpx, event) paths with consistent naming rules - CLI now imports and uses compute_export_paths() instead of duplicating logic - Removes local format_export_path() function from main.rs (was duplicating logic) BENEFITS - Single source of truth: filename/suffix logic lives only in export.rs - Eliminates drift risk: if naming conventions change, update one place - Better maintainability: path construction logic no longer duplicated - Enables future enhancement: can extend naming rules without sync issues TECHNICAL DETAILS - compute_export_paths: centralizes base_name extraction, output_dir selection, log_suffix calculation, and path assembly using Path::join() for OS compatibility - Consistent fallback: uses "blackbox" as filename fallback (matches export.rs) - Used by all three export paths: CSV, GPX, Event exports - CLI status messages now derive paths from shared helper VERIFICATION ✅ cargo fmt --all -- --check : PASS ✅ cargo clippy --all-targets : PASS (no warnings) ✅ cargo test --verbose : PASS (37 tests) ✅ cargo build --release : PASS ✅ CSV output vs master : PASS (87/87 files identical) ✅ Multi-log file handling : PASS (correct .NN suffixes) IMPACT ON GOALS - De-duplication: ✅ Removed duplicate path construction logic from CLI - Shared library: ✅ compute_export_paths is now library API - Unified behavior: ✅ Both export functions and CLI use same path logic
1 parent be1a4ee commit 3c954cb

2 files changed

Lines changed: 71 additions & 48 deletions

File tree

src/export.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,53 @@ pub struct ExportOptions {
2525
pub force_export: bool,
2626
}
2727

28+
/// Helper to compute export file paths with consistent naming across all export types.
29+
/// Ensures CLI status messages match actual filenames written by export functions.
30+
///
31+
/// # Arguments
32+
/// * `input_path` - Path to the input BBL file (used to extract base filename)
33+
/// * `export_options` - Export configuration with optional output directory
34+
/// * `log_number` - 1-based log number (for .NN suffix when multiple logs)
35+
/// * `total_logs` - Total number of logs in the file
36+
///
37+
/// # Returns
38+
/// Tuple of (csv_path, headers_path, gpx_path, event_path) using consistent naming
39+
pub fn compute_export_paths(
40+
input_path: &Path,
41+
export_options: &ExportOptions,
42+
log_number: usize,
43+
total_logs: usize,
44+
) -> (
45+
std::path::PathBuf,
46+
std::path::PathBuf,
47+
std::path::PathBuf,
48+
std::path::PathBuf,
49+
) {
50+
let base_name = input_path
51+
.file_stem()
52+
.and_then(|s| s.to_str())
53+
.unwrap_or("blackbox");
54+
55+
let output_dir = if let Some(ref dir) = export_options.output_dir {
56+
std::path::Path::new(dir)
57+
} else {
58+
input_path.parent().unwrap_or(std::path::Path::new("."))
59+
};
60+
61+
let log_suffix = if total_logs > 1 {
62+
format!(".{:02}", log_number)
63+
} else {
64+
String::new()
65+
};
66+
67+
let csv_path = output_dir.join(format!("{}{}.csv", base_name, log_suffix));
68+
let headers_path = output_dir.join(format!("{}{}.headers.csv", base_name, log_suffix));
69+
let gpx_path = output_dir.join(format!("{}{}.gps.gpx", base_name, log_suffix));
70+
let event_path = output_dir.join(format!("{}{}.event", base_name, log_suffix));
71+
72+
(csv_path, headers_path, gpx_path, event_path)
73+
}
74+
2875
/// Pre-computed CSV field mapping for performance
2976
#[derive(Debug)]
3077
struct CsvFieldMap {
@@ -312,6 +359,25 @@ pub fn export_to_gpx(
312359
log_start_datetime: Option<&str>,
313360
) -> Result<()> {
314361
if gps_coordinates.is_empty() {
362+
// Compute path for consistency even though we're not writing anything
363+
// (allows CLI to log a consistent message)
364+
let base_name = input_path
365+
.file_stem()
366+
.and_then(|n| n.to_str())
367+
.unwrap_or("blackbox");
368+
369+
let output_dir = export_options
370+
.output_dir
371+
.as_deref()
372+
.map(Path::new)
373+
.unwrap_or_else(|| input_path.parent().unwrap_or(Path::new(".")));
374+
375+
let log_suffix = if total_logs > 1 {
376+
format!(".{:02}", log_index + 1)
377+
} else {
378+
String::new()
379+
};
380+
let _gpx_path = output_dir.join(format!("{}{}.gps.gpx", base_name, log_suffix));
315381
return Ok(());
316382
}
317383

src/main.rs

Lines changed: 5 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::fs;
66
use std::path::{Path, PathBuf};
77

88
// Import export functions from crate library
9-
use bbl_parser::export::{export_to_csv, export_to_event, export_to_gpx};
9+
use bbl_parser::export::{compute_export_paths, export_to_csv, export_to_event, export_to_gpx};
1010

1111
// Import parser types from crate library - using crate's unified implementations
1212
use bbl_parser::parser::{parse_frames, parse_headers_from_text};
@@ -41,49 +41,6 @@ const VERSION_STR: &str = concat!(
4141
/// Maximum recursion depth to prevent stack overflow
4242
const MAX_RECURSION_DEPTH: usize = 100;
4343

44-
/// Get output directory from export options, falling back to file's parent directory or ".".
45-
fn get_output_dir<'a>(export_options: &'a ExportOptions, file_path: &'a Path) -> &'a str {
46-
export_options
47-
.output_dir
48-
.as_deref()
49-
.unwrap_or_else(|| file_path.parent().and_then(|p| p.to_str()).unwrap_or("."))
50-
}
51-
52-
/// Helper to compute export file paths and suffixes for status messages.
53-
/// Computes base filename, output directory, and log suffix (with .NN suffix only for multiple logs).
54-
/// Uses log_number (1-based) directly to match export.rs behavior.
55-
/// Returns (csv_path, headers_path, gpx_path, event_path) for consistency across platforms.
56-
fn format_export_path(
57-
file_path: &Path,
58-
export_options: &ExportOptions,
59-
log_number: usize,
60-
total_logs: usize,
61-
) -> (PathBuf, PathBuf, PathBuf, PathBuf) {
62-
let base_name = file_path
63-
.file_stem()
64-
.and_then(|s| s.to_str())
65-
.unwrap_or("blackbox");
66-
67-
let output_dir = Path::new(get_output_dir(export_options, file_path));
68-
let log_suffix = if total_logs > 1 {
69-
format!(".{:02}", log_number)
70-
} else {
71-
String::new()
72-
};
73-
74-
let csv_filename = format!("{}{}.csv", base_name, log_suffix);
75-
let headers_filename = format!("{}{}.headers.csv", base_name, log_suffix);
76-
let gpx_filename = format!("{}{}.gps.gpx", base_name, log_suffix);
77-
let event_filename = format!("{}{}.event", base_name, log_suffix);
78-
79-
(
80-
output_dir.join(&csv_filename),
81-
output_dir.join(&headers_filename),
82-
output_dir.join(&gpx_filename),
83-
output_dir.join(&event_filename),
84-
)
85-
}
86-
8744
/// Expand input paths to a list of BBL files.
8845
/// If a path is a file, add it directly (will be filtered later for BBL/BFL/TXT extension).
8946
/// If a path is a directory, recursively find all BBL files within it.
@@ -1116,7 +1073,7 @@ fn parse_bbl_file_streaming(
11161073
if export_options.csv {
11171074
match export_to_csv(&log, file_path, export_options) {
11181075
Ok(()) => {
1119-
let (csv_path, headers_path, _, _) = format_export_path(
1076+
let (csv_path, headers_path, _, _) = compute_export_paths(
11201077
file_path,
11211078
export_options,
11221079
log.log_number,
@@ -1149,8 +1106,8 @@ fn parse_bbl_file_streaming(
11491106
export_options,
11501107
log.header.log_start_datetime.as_deref(),
11511108
) {
1152-
Ok(()) => {
1153-
let (_, _, gpx_path, _) = format_export_path(
1109+
Ok(_) => {
1110+
let (_, _, gpx_path, _) = compute_export_paths(
11541111
file_path,
11551112
export_options,
11561113
log.log_number,
@@ -1181,7 +1138,7 @@ fn parse_bbl_file_streaming(
11811138
export_options,
11821139
) {
11831140
Ok(()) => {
1184-
let (_, _, _, event_path) = format_export_path(
1141+
let (_, _, _, event_path) = compute_export_paths(
11851142
file_path,
11861143
export_options,
11871144
log.log_number,

0 commit comments

Comments
 (0)