- Split monolithic src/lib.rs (~640 lines) into 7 sub-modules: types.rs, context.rs, user_data.rs, translate.rs, http.rs, csv_util.rs, test_utils.rs - Add configurable --output-dir CLI option for CSV output files - Implement atomic file writes (write-to-temp + rename) for all CSVs - Change compute_car_counts() to return CarCounts directly (not Result) - Add test-utils feature gate for shared test helpers - Add HTTP mock tests with mockito (6 tests) - Add proptest tests for compute_car_counts (3 property tests) - Update integration tests to use shared test utilities - Update documentation (API.md, ARCHITECTURE.md, DATA_STRUCTURES.md)
12 KiB
API Documentation
Overview
This document provides comprehensive API documentation for the Top Drives Records Analyzer, including all public functions, data structures, and their usage patterns.
Module Structure
src/
├── lib.rs # Thin re-export module: declares sub-modules, re-exports public types
├── types.rs # Core types: AppError, Car, BestCarRecord, TripletKey
├── context.rs # Analysis state: AnalysisContext, CarCounts, compute_car_counts
├── user_data.rs # User data: MyCarsData, load_my_cars_data
├── translate.rs # Translation: translate_track_name, translate_track_type
├── http.rs # HTTP: get_file (async download with retry)
├── csv_util.rs # CSV utilities: compare_cars, escape_csv_field
├── test_utils.rs # Test helpers: make_record, make_car
├── records.rs # Binary entry point: CLI definition and dispatch
├── commands/
│ ├── mod.rs # Command dispatch, run_analysis(), load_my_cars_or_warn()
│ ├── fetch.rs # Fetch command: download car/track/timing data
│ ├── best.rs # Best command: best.csv + very_best.csv
│ ├── trash.rs # Trash command: trash.csv
│ ├── wishlist.rs # Wishlist command: wishlist.csv, add.csv, remove.csv
│ ├── details.rs # Details command: details.csv
│ ├── track.rs # Track command: stdout query
│ └── all.rs # All command: generates all output files
├── parsing/
│ ├── mod.rs # Core parsing: parse_car_times, update_best_car, parse_all_times
│ └── car_list.rs # Car database loading: get_car_list
└── output/
└── mod.rs # CSV output: write_sorted_csv, write_best_csv, wishlist logic, etc.
Shared Library (src/lib.rs)
Thin re-export module that declares 6 focused sub-modules and re-exports all public types for backward compatibility. External code can use top_drives::Car without knowing the internal module layout.
Public Types
Car
Core car information structure
#[derive(Debug, Clone, Deserialize)]
pub struct Car {
pub rid: String, // Repository identifier
pub brand: String, // Manufacturer/brand name
#[serde(rename = "onlyName")]
pub only_name: String, // Model name (without brand)
pub year: u16, // Model year
pub rq: u8, // Rarity level (10-113)
#[serde(default)]
pub prize: bool, // Whether car is prize-only
pub guid: String, // Game-specific card identifier
}
Purpose: Represents a car entity from the records database.
Deserialization: Uses serde::Deserialize with #[serde(rename = "onlyName")] for the only_name field and #[serde(default)] for prize.
BestCarRecord
Best performing car record for a specific track/RQ/condition
#[derive(Debug, Clone)]
pub struct BestCarRecord {
pub car_rid: String, // Car identifier
pub brand: String, // Manufacturer
pub only_name: String, // Model name
pub year: u16, // Year
pub rq: u8, // RQ level
pub config: String, // Tune config (111, 233, etc.)
pub time: f64, // Track time in seconds
pub owned: bool, // User ownership status
pub prize: bool, // Prize car status
pub guid: String, // Game identifier
}
TripletKey
Composite key for unique track conditions
pub type TripletKey = (String, u8, String);
// Track ID RQ Level Surface Condition
AnalysisContext
Mutable analysis state using concurrent DashMap
pub struct AnalysisContext {
pub best_cars: Arc<DashMap<TripletKey, Vec<BestCarRecord>>>,
}
Methods:
new()— Create a new, empty analysis context
Thread Safety: Uses DashMap for sharded internal locking, allowing concurrent access from multiple parsing tasks without a global lock.
CarCounts
Aggregated per-car win counts
pub struct CarCounts {
pub by_rid: HashMap<String, (BestCarRecord, usize)>,
pub guid_to_rid: HashMap<String, String>,
pub car_map: HashMap<String, Car>,
pub car_list: Vec<Car>,
}
MyCarsData
Parsed user data from my_cars.json
#[derive(Debug, Default)]
pub struct MyCarsData {
pub owned: HashSet<String>, // Owned car GUIDs
pub wishlist: HashSet<String>, // Wishlist car GUIDs
}
AppError
Unified error type
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Network error: {0}")]
Network(String),
#[error("Parse error in {file}: {reason}")]
Parse { file: String, reason: String },
#[error("Missing data file: {0}")]
MissingFile(String),
#[error("Internal error: {0}")]
Internal(String),
}
Public Functions
translate_track_name(track_code: &str) -> String
Translates internal track identifiers to human-readable names
- Input: Track code (e.g.,
"laguna") - Output: Track name (e.g.,
"Laguna Seca") - Fallback: Returns original code if no mapping found
- Data source:
track_names.json(compiled into binary viainclude_str!)
translate_track_type(track_code: &str) -> &'static str
Translates condition codes to surface names
- Input: Condition code (e.g.,
"00") - Output: Surface name (e.g.,
"Dry") - Fallback: Returns the raw code string for unrecognized codes
get_file(file_url: &str, local_file_path: &str) -> Result<(), AppError>
Downloads a file from a URL and saves it locally
- Writes to a
.tmpfile first, then renames on success (atomic write) - Retries up to 3 times on server errors (5xx) and empty responses
- Returns
AppError::Networkon HTTP failures - Returns
AppError::Ioon file write failures
load_my_cars_data(path: &str) -> Result<MyCarsData, AppError>
Loads and parses user car collection data
- Input: Path to
my_cars.json - Returns:
MyCarsDatawith owned and wishlist GUIDs - Errors:
AppError::MissingFileif file not found,AppError::Parseif JSON invalid
compare_cars(a: &Car, b: &Car) -> std::cmp::Ordering
Sorting comparator for cars
- Sorts by: RQ → Brand → Model name
compute_car_counts(ctx: &AnalysisContext, car_list: Vec<Car>) -> CarCounts
Aggregates per-car win counts from analysis context
- Iterates all entries in the
DashMap - Returns
CarCountswith by_rid, guid_to_rid, car_map, and car_list
escape_csv_field(field: &str) -> String
Escapes a field for safe CSV inclusion
- Wraps in quotes if field contains comma, quote, newline, or carriage return
- Doubles internal quotes per CSV spec
Records Binary (src/records.rs)
CLI entry point. Defines command-line arguments and dispatches to command handlers.
CLI Global Options
| Option | Default | Description |
|---|---|---|
--my-cars |
my_cars.json |
Path to user's car collection export |
--cars-db |
cars_repo.json |
Path to car database file |
--times-dir |
times |
Directory containing per-car timing JSON files |
--tracks-db |
tracks_repo.json |
Path to tracks database file |
CLI Commands
| Command | Description | Flags |
|---|---|---|
fetch |
Download latest car, track, and timing data | None |
best |
Generate best.csv and very_best.csv |
--min-count N (default: 10) |
trash |
Generate trash.csv |
None |
wishlist |
Generate wishlist.csv, add.csv, remove.csv |
--min-count N, --max-wishlist M |
all |
Generate all output files in a single pass | --min-count N, --max-wishlist M |
details |
Generate details.csv with all timing data |
None |
track |
Show best car for specific track/condition/RQ | <track> <condition> <rq> |
Parsing Module (src/parsing/)
Constants (src/parsing/mod.rs)
| Constant | Value | Description |
|---|---|---|
MAX_RQ |
113 | Maximum RQ level for range analysis |
MIN_VALID_TIME |
1.2 | Minimum valid track time (seconds) |
TIME_TOLERANCE |
0.005 | Tolerance for tie detection (seconds) |
TEST_BOWL_FACTOR |
2000.0 | Multiplier for Test Bowl time inversion |
CONCURRENCY_LIMIT |
20 | Maximum concurrent tasks |
CAR_TIMING_API_BASE |
API URL | Base URL for per-car timing data |
Key Functions
parse_car_times(car, my_cars, best_cars, times_dir) -> Result<(), AppError>
Processes timing data for a single car (src/parsing/mod.rs)
- Synchronous — designed to be called from
tokio::task::spawn_blocking - Reads car JSON file from the times directory
- Processes each configuration and track
- Applies quality filters (downvotes, minimum time)
- Handles Test Bowl time inversion
- Uses
rsplit_once("_a")for correct track name splitting
update_best_car(best_cars, track_id, car_rq, surface, new_record)
Updates best car records with a new performance entry (src/parsing/mod.rs)
- Iterates RQ range from car's RQ to
MAX_RQ - Replaces existing record if new time is faster
- Adds to tie list if within
TIME_TOLERANCE - Prevents duplicate
car_ridentries in tie lists - Optimized: avoids cloning on the last iteration via
is_lastguard
parse_all_times(my_cars, ctx, cars_db_path, times_dir) -> Result<Vec<Car>, AppError>
Orchestrates concurrent parsing of all car timing files (src/parsing/mod.rs)
- Loads car list from database
- Uses
Semaphore(CONCURRENCY_LIMIT)for bounded concurrency - Acquires semaphore in async context, then runs sync parsing via
spawn_blocking - Returns the full car list for downstream use
fetch_times(cars_db_path, times_dir) -> Result<(), AppError>
Fetches timing data files for all cars (src/parsing/mod.rs)
- Uses bounded concurrency via semaphore
- Returns first error encountered, or
Ok(())if all succeed
get_car_list(cars_db_path: &str) -> Result<Vec<Car>, AppError>
Loads the car database (src/parsing/car_list.rs)
- Uses serde deserialization for type-safe parsing
- Returns
AppError::MissingFileorAppError::Parseon failure
Commands Module (src/commands/)
run_analysis(owned, cars_db_path, times_dir) -> Result<(AnalysisContext, CarCounts), AppError>
Common analysis setup (src/commands/mod.rs)
Loads car database, parses all timing files concurrently, and computes car counts.
load_my_cars_or_warn(path: &str) -> Result<MyCarsData, AppError>
Loads user data with graceful degradation (src/commands/mod.rs)
- Returns empty
MyCarsData::default()with a warning if file is missing - Propagates parse errors and other failures
Output Module (src/output/)
CSV Output Functions
write_sorted_csv(path, header, entries, cmp, format_row) -> Result<(), AppError>
Generic sorted CSV writer
- Sorts by index (avoids cloning entries)
- Writes header row
- Formats each row via closure
write_best_csv(counts), write_very_best_csv(counts, min_count)
Generate best car CSV files
- Format:
RQ,Brand,Model,Year,Count - Fields are CSV-escaped
write_trash_csv(counts, owned)
Generate trash car CSV file
- Filters: not best on any track, RQ < 80, not prize car
- Format:
RQ,Brand,Model,Year
write_wishlist_files(counts, owned, wishlist, min_count, max_wishlist)
Generate wishlist, add, and remove CSV files
- Computes ideal wishlist once
- Derives all three files from it
- Format:
RQ,Brand,Model,Year,Count
write_details_csv(ctx)
Generate comprehensive timing data CSV
- Format:
RQ,TrackID,Condition,CarRQ,Brand,Model,Year,Config,Time - Sorted by RQ then track ID
print_best_car(ctx, track, condition, rq) -> Result<(), AppError>
Print best car for a specific track/condition/RQ to stdout
compute_ideal_wishlist(counts, owned, min_count, max_wishlist) -> Vec<(BestCarRecord, usize)>
Computes the ideal wishlist
- Filters to cars with count >=
min_count - Excludes owned cars
- Sorts by RQ
- Caps at
max_wishlist
Sorting Helpers
compare_records(a: &BestCarRecord, b: &BestCarRecord) -> std::cmp::Ordering
Comparison for BestCarRecord sorting
- Sorts by: RQ → Brand → Model name