# 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 │ ├── improve.rs # Improve command: improve.csv (owned cars not at final build) │ └── 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** ```rust #[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** ```rust #[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, // Original tune config from JSON (111, 221, 233, etc.) pub normalized_config: String, // Normalized final build (323, 233, or 332) 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** ```rust pub type TripletKey = (String, u8, String); // Track ID RQ Level Surface Condition ``` #### `AnalysisContext` **Mutable analysis state using concurrent DashMap** ```rust pub struct AnalysisContext { pub best_cars: Arc>>, } ``` **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** ```rust pub struct CarCounts { pub by_rid: HashMap<(String, String), (BestCarRecord, usize)>, pub guid_to_rid: HashMap>, pub car_map: HashMap, pub car_list: Vec, pub owned_configs: HashMap, } ``` **Key grouping**: `by_rid` is keyed by `(car_rid, normalized_config)` so different tune configs of the same car are tracked separately. `guid_to_rid` maps each GUID to all `(rid, config)` pairs it appears under. #### `MyCarsData` **Parsed user data from `my_cars.json`** ```rust #[derive(Debug, Default)] pub struct MyCarsData { pub owned: HashSet, // Owned car GUIDs pub wishlist: HashSet, // Wishlist car GUIDs pub owned_configs: HashMap, // GUID → actual tune config (e.g., "233") } ``` #### `AppError` **Unified error type** ```rust #[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 via `include_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 `.tmp` file first, then renames on success (atomic write) - Retries up to 3 times on server errors (5xx) and empty responses - Returns `AppError::Network` on HTTP failures - Returns `AppError::Io` on file write failures #### `load_my_cars_data(path: &str) -> Result` **Loads and parses user car collection data** - **Input**: Path to `my_cars.json` - **Returns**: `MyCarsData` with owned and wishlist GUIDs - **Errors**: `AppError::MissingFile` if file not found, `AppError::Parse` if 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, owned_configs: HashMap) -> CarCounts` **Aggregates per-car win counts from analysis context** - Iterates all entries in the `DashMap` - Groups by `(car_rid, normalized_config)` for per-config tracking - Returns `CarCounts` with by_rid, guid_to_rid, car_map, car_list, and owned_configs #### `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 #### `normalize_to_final_build(config: &str) -> String` **Normalizes a tune configuration to the closest "final build"** - The three final builds are `"323"`, `"233"`, and `"332"` (permutations of digits summing to 8) - For non-standard configs, repeatedly increments the smallest digit until sum reaches 8 - Returns the original string unchanged if config cannot be parsed or sum already ≥ 8 - Version prefixes (`v2c_`, `v1c_`) are stripped at parse time before reaching this function --- ## 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 | ` ` | | `improve` | Generate `improve.csv` (owned cars not at final build) | None | --- ## 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_rid` entries in tie lists - Optimized: avoids cloning on the last iteration via `is_last` guard #### `parse_all_times(my_cars, ctx, cars_db_path, times_dir) -> Result, 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, AppError>` **Loads the car database** (`src/parsing/car_list.rs`) - Uses serde deserialization for type-safe parsing - Returns `AppError::MissingFile` or `AppError::Parse` on 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` **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,FinalBuild,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,FinalBuild,Count` #### `write_improve_csv(counts) -> Result` **Generate improve CSV file** - Lists owned cars whose actual player config is not yet a final build (323/233/332) - Uses actual player config from `my_cars.json`, not timing data configs - Format: `RQ,Brand,Model,Year,Target,Count` - Sorted by track count descending (most impactful upgrades first) - Returns the number of cars written #### `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 → Normalized Config → Brand → Model name