top_drives/docs/API.md
Jan-Peter von Hunnius a9a46ee551 refactor: modular architecture with unified error handling and DashMap concurrency
- Break monolithic records.rs into commands/, output/, parsing/ modules
- Add lib.rs with shared types (AppError, Car, BestCarRecord, etc.)
- Replace Arc<Mutex<HashMap>> with DashMap for concurrent access
- Add unified AppError enum with thiserror
- Add configurable CLI paths (--my-cars, --cars-db, --times-dir, --tracks-db)
- Add serde deserialization for Car struct
- Add CSV escaping for fields with commas/quotes/newlines
- Fix track name splitting: split_once -> rsplit_once for _a handling
- Add integration tests, proptest property-based tests, and unit tests
- Add rustfmt.toml configuration
- Update documentation (API, ARCHITECTURE, DATA_STRUCTURES, README)
2026-05-08 16:19:01 +02:00

8.7 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.

Core Modules

Shared Library (src/lib.rs)

Common types and utilities shared across the project.

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
    pub only_name: String,  // Model name (without brand)
    pub year: u16,          // Model year
    pub rq: u8,             // Rarity level (10-113)
    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

pub struct AnalysisContext {
    pub best_cars: Arc<Mutex<HashMap<TripletKey, Vec<BestCarRecord>>>>,
}

Methods:

  • new() — Create a new, empty analysis context
  • lock_best_cars() — Acquire a lock on the best_cars map (returns Result, converts poison errors to AppError)
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)]
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: mutex poisoned — {0}")]
    Poisoned(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 "Unknown" for unrecognized codes
get_file(file_url: &str, local_file_path: &str) -> Result<(), AppError>

Downloads a file from a URL and saves it locally

  • Removes existing file before downloading
  • Returns AppError::Network on HTTP failures
  • Returns AppError::Io on 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: 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<Car>) -> Result<CarCounts, AppError>

Aggregates per-car win counts from analysis context

  • Returns CarCounts with 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)

Primary analysis engine for processing JSON-based timing data.

Constants

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
DEFAULT_MY_CARS_PATH "my_cars.json" Default user data file path
CARS_DB_PATH "cars_repo.json" Car database file path
TIMES_DIR "times" Timing data directory
CARS_DB_URL GitHub URL Remote car database URL
TRACKS_DB_URL GitHub URL Remote tracks database URL
CAR_TIMING_API_BASE API URL Base URL for per-car timing data

CLI Commands

Command Description Flags
fetch Download latest car and track 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>

Key Internal Functions

run_analysis(owned: &HashSet<String>) -> Result<(AnalysisContext, CarCounts), AppError>

Common analysis setup

Loads user data, parses all timing files, and computes car counts.

parse_car_times(car: Car, my_cars: &HashSet<String>, ctx: &AnalysisContext)

Processes timing data for a single car

  • Runs inside tokio::task::spawn_blocking to avoid blocking async runtime
  • Reads car JSON file from times/ directory
  • Processes each configuration and track
  • Applies quality filters (downvotes, minimum time)
  • Handles Test Bowl time inversion
update_best_car(best_cars, track_id, car_rq, surface, new_record)

Updates best car records with a new performance entry

  • 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
parse_all_times(my_cars, ctx) -> Vec<Car>

Orchestrates concurrent parsing of all car timing files

  • Uses Semaphore(CONCURRENCY_LIMIT) for bounded concurrency
  • Spawns blocking tasks via tokio::task::spawn_blocking
  • Returns the full car list for downstream use
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

Output Functions

write_sorted_csv(path, header, entries, cmp, format_row)

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)

Print best car for a specific track/condition/RQ to stdout