docs: update documentation to match refactored codebase
- Delete obsolete plans/code-review.md (all suggestions implemented) - Rewrite docs/API.md to reflect modular architecture, DashMap, corrected AppError variants, accurate function signatures and module locations - Rewrite docs/ARCHITECTURE.md to reflect DashMap concurrency, remove completed future enhancements, fix AnalysisContext description - Update docs/USER_GUIDE.md with global CLI path options (--my-cars, --cars-db, --times-dir, --tracks-db) and updated troubleshooting
This commit is contained in:
parent
a9a46ee551
commit
d2c1418e5e
4 changed files with 284 additions and 539 deletions
263
docs/API.md
263
docs/API.md
|
|
@ -4,15 +4,37 @@
|
||||||
|
|
||||||
This document provides comprehensive API documentation for the Top Drives Records Analyzer, including all public functions, data structures, and their usage patterns.
|
This document provides comprehensive API documentation for the Top Drives Records Analyzer, including all public functions, data structures, and their usage patterns.
|
||||||
|
|
||||||
## Core Modules
|
## Module Structure
|
||||||
|
|
||||||
### Shared Library (`src/lib.rs`)
|
```
|
||||||
|
src/
|
||||||
|
├── lib.rs # Shared library: types, utilities, error handling
|
||||||
|
├── 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`)
|
||||||
|
|
||||||
Common types and utilities shared across the project.
|
Common types and utilities shared across the project.
|
||||||
|
|
||||||
#### Public Types
|
### Public Types
|
||||||
|
|
||||||
##### `Car`
|
#### `Car`
|
||||||
**Core car information structure**
|
**Core car information structure**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
@ -20,9 +42,11 @@ Common types and utilities shared across the project.
|
||||||
pub struct Car {
|
pub struct Car {
|
||||||
pub rid: String, // Repository identifier
|
pub rid: String, // Repository identifier
|
||||||
pub brand: String, // Manufacturer/brand name
|
pub brand: String, // Manufacturer/brand name
|
||||||
|
#[serde(rename = "onlyName")]
|
||||||
pub only_name: String, // Model name (without brand)
|
pub only_name: String, // Model name (without brand)
|
||||||
pub year: u16, // Model year
|
pub year: u16, // Model year
|
||||||
pub rq: u8, // Rarity level (10-113)
|
pub rq: u8, // Rarity level (10-113)
|
||||||
|
#[serde(default)]
|
||||||
pub prize: bool, // Whether car is prize-only
|
pub prize: bool, // Whether car is prize-only
|
||||||
pub guid: String, // Game-specific card identifier
|
pub guid: String, // Game-specific card identifier
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +56,7 @@ pub struct Car {
|
||||||
|
|
||||||
**Deserialization**: Uses `serde::Deserialize` with `#[serde(rename = "onlyName")]` for the `only_name` field and `#[serde(default)]` for `prize`.
|
**Deserialization**: Uses `serde::Deserialize` with `#[serde(rename = "onlyName")]` for the `only_name` field and `#[serde(default)]` for `prize`.
|
||||||
|
|
||||||
##### `BestCarRecord`
|
#### `BestCarRecord`
|
||||||
**Best performing car record for a specific track/RQ/condition**
|
**Best performing car record for a specific track/RQ/condition**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
@ -51,7 +75,7 @@ pub struct BestCarRecord {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
##### `TripletKey`
|
#### `TripletKey`
|
||||||
**Composite key for unique track conditions**
|
**Composite key for unique track conditions**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
@ -59,20 +83,21 @@ pub type TripletKey = (String, u8, String);
|
||||||
// Track ID RQ Level Surface Condition
|
// Track ID RQ Level Surface Condition
|
||||||
```
|
```
|
||||||
|
|
||||||
##### `AnalysisContext`
|
#### `AnalysisContext`
|
||||||
**Mutable analysis state**
|
**Mutable analysis state using concurrent DashMap**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub struct AnalysisContext {
|
pub struct AnalysisContext {
|
||||||
pub best_cars: Arc<Mutex<HashMap<TripletKey, Vec<BestCarRecord>>>>,
|
pub best_cars: Arc<DashMap<TripletKey, Vec<BestCarRecord>>>,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Methods**:
|
**Methods**:
|
||||||
- `new()` — Create a new, empty analysis context
|
- `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`
|
**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**
|
**Aggregated per-car win counts**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
@ -84,18 +109,18 @@ pub struct CarCounts {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
##### `MyCarsData`
|
#### `MyCarsData`
|
||||||
**Parsed user data from `my_cars.json`**
|
**Parsed user data from `my_cars.json`**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub struct MyCarsData {
|
pub struct MyCarsData {
|
||||||
pub owned: HashSet<String>, // Owned car GUIDs
|
pub owned: HashSet<String>, // Owned car GUIDs
|
||||||
pub wishlist: HashSet<String>, // Wishlist car GUIDs
|
pub wishlist: HashSet<String>, // Wishlist car GUIDs
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
##### `AppError`
|
#### `AppError`
|
||||||
**Unified error type**
|
**Unified error type**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
@ -109,14 +134,14 @@ pub enum AppError {
|
||||||
Parse { file: String, reason: String },
|
Parse { file: String, reason: String },
|
||||||
#[error("Missing data file: {0}")]
|
#[error("Missing data file: {0}")]
|
||||||
MissingFile(String),
|
MissingFile(String),
|
||||||
#[error("Internal error: mutex poisoned — {0}")]
|
#[error("Internal error: {0}")]
|
||||||
Poisoned(String),
|
Internal(String),
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Public Functions
|
### Public Functions
|
||||||
|
|
||||||
##### `translate_track_name(track_code: &str) -> String`
|
#### `translate_track_name(track_code: &str) -> String`
|
||||||
**Translates internal track identifiers to human-readable names**
|
**Translates internal track identifiers to human-readable names**
|
||||||
|
|
||||||
- **Input**: Track code (e.g., `"laguna"`)
|
- **Input**: Track code (e.g., `"laguna"`)
|
||||||
|
|
@ -124,38 +149,40 @@ pub enum AppError {
|
||||||
- **Fallback**: Returns original code if no mapping found
|
- **Fallback**: Returns original code if no mapping found
|
||||||
- **Data source**: `track_names.json` (compiled into binary via `include_str!`)
|
- **Data source**: `track_names.json` (compiled into binary via `include_str!`)
|
||||||
|
|
||||||
##### `translate_track_type(track_code: &str) -> &'static str`
|
#### `translate_track_type(track_code: &str) -> &'static str`
|
||||||
**Translates condition codes to surface names**
|
**Translates condition codes to surface names**
|
||||||
|
|
||||||
- **Input**: Condition code (e.g., `"00"`)
|
- **Input**: Condition code (e.g., `"00"`)
|
||||||
- **Output**: Surface name (e.g., `"Dry"`)
|
- **Output**: Surface name (e.g., `"Dry"`)
|
||||||
- **Fallback**: Returns `"Unknown"` for unrecognized codes
|
- **Fallback**: Returns the raw code string for unrecognized codes
|
||||||
|
|
||||||
##### `get_file(file_url: &str, local_file_path: &str) -> Result<(), AppError>`
|
#### `get_file(file_url: &str, local_file_path: &str) -> Result<(), AppError>`
|
||||||
**Downloads a file from a URL and saves it locally**
|
**Downloads a file from a URL and saves it locally**
|
||||||
|
|
||||||
- Removes existing file before downloading
|
- 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::Network` on HTTP failures
|
||||||
- Returns `AppError::Io` on file write failures
|
- Returns `AppError::Io` on file write failures
|
||||||
|
|
||||||
##### `load_my_cars_data(path: &str) -> Result<MyCarsData, AppError>`
|
#### `load_my_cars_data(path: &str) -> Result<MyCarsData, AppError>`
|
||||||
**Loads and parses user car collection data**
|
**Loads and parses user car collection data**
|
||||||
|
|
||||||
- **Input**: Path to `my_cars.json`
|
- **Input**: Path to `my_cars.json`
|
||||||
- **Returns**: `MyCarsData` with owned and wishlist GUIDs
|
- **Returns**: `MyCarsData` with owned and wishlist GUIDs
|
||||||
- **Errors**: `AppError::MissingFile` if file not found, `AppError::Parse` if JSON invalid
|
- **Errors**: `AppError::MissingFile` if file not found, `AppError::Parse` if JSON invalid
|
||||||
|
|
||||||
##### `compare_cars(a: &Car, b: &Car) -> std::cmp::Ordering`
|
#### `compare_cars(a: &Car, b: &Car) -> std::cmp::Ordering`
|
||||||
**Sorting comparator for cars**
|
**Sorting comparator for cars**
|
||||||
|
|
||||||
- Sorts by: RQ → Brand → Model name
|
- Sorts by: RQ → Brand → Model name
|
||||||
|
|
||||||
##### `compute_car_counts(ctx: &AnalysisContext, car_list: Vec<Car>) -> Result<CarCounts, AppError>`
|
#### `compute_car_counts(ctx: &AnalysisContext, car_list: Vec<Car>) -> Result<CarCounts, AppError>`
|
||||||
**Aggregates per-car win counts from analysis context**
|
**Aggregates per-car win counts from analysis context**
|
||||||
|
|
||||||
|
- Iterates all entries in the `DashMap`
|
||||||
- Returns `CarCounts` with by_rid, guid_to_rid, car_map, and car_list
|
- Returns `CarCounts` with by_rid, guid_to_rid, car_map, and car_list
|
||||||
|
|
||||||
##### `escape_csv_field(field: &str) -> String`
|
#### `escape_csv_field(field: &str) -> String`
|
||||||
**Escapes a field for safe CSV inclusion**
|
**Escapes a field for safe CSV inclusion**
|
||||||
|
|
||||||
- Wraps in quotes if field contains comma, quote, newline, or carriage return
|
- Wraps in quotes if field contains comma, quote, newline, or carriage return
|
||||||
|
|
@ -163,11 +190,36 @@ pub enum AppError {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Records Binary (`src/records.rs`)
|
## Records Binary (`src/records.rs`)
|
||||||
|
|
||||||
Primary analysis engine for processing JSON-based timing data.
|
CLI entry point. Defines command-line arguments and dispatches to command handlers.
|
||||||
|
|
||||||
#### Constants
|
### 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 |
|
| Constant | Value | Description |
|
||||||
|----------|-------|-------------|
|
|----------|-------|-------------|
|
||||||
|
|
@ -176,57 +228,106 @@ Primary analysis engine for processing JSON-based timing data.
|
||||||
| `TIME_TOLERANCE` | 0.005 | Tolerance for tie detection (seconds) |
|
| `TIME_TOLERANCE` | 0.005 | Tolerance for tie detection (seconds) |
|
||||||
| `TEST_BOWL_FACTOR` | 2000.0 | Multiplier for Test Bowl time inversion |
|
| `TEST_BOWL_FACTOR` | 2000.0 | Multiplier for Test Bowl time inversion |
|
||||||
| `CONCURRENCY_LIMIT` | 20 | Maximum concurrent tasks |
|
| `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 |
|
| `CAR_TIMING_API_BASE` | API URL | Base URL for per-car timing data |
|
||||||
|
|
||||||
#### CLI Commands
|
### Key Functions
|
||||||
|
|
||||||
| Command | Description | Flags |
|
#### `parse_car_times(car, my_cars, best_cars, times_dir) -> Result<(), AppError>`
|
||||||
|---------|-------------|-------|
|
**Processes timing data for a single car** (`src/parsing/mod.rs`)
|
||||||
| `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
|
- **Synchronous** — designed to be called from `tokio::task::spawn_blocking`
|
||||||
|
- Reads car JSON file from the times directory
|
||||||
##### `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
|
- Processes each configuration and track
|
||||||
- Applies quality filters (downvotes, minimum time)
|
- Applies quality filters (downvotes, minimum time)
|
||||||
- Handles Test Bowl time inversion
|
- 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)`
|
#### `update_best_car(best_cars, track_id, car_rq, surface, new_record)`
|
||||||
**Updates best car records with a new performance entry**
|
**Updates best car records with a new performance entry** (`src/parsing/mod.rs`)
|
||||||
|
|
||||||
- Iterates RQ range from car's RQ to `MAX_RQ`
|
- Iterates RQ range from car's RQ to `MAX_RQ`
|
||||||
- Replaces existing record if new time is faster
|
- Replaces existing record if new time is faster
|
||||||
- Adds to tie list if within `TIME_TOLERANCE`
|
- Adds to tie list if within `TIME_TOLERANCE`
|
||||||
- Prevents duplicate `car_rid` entries in tie lists
|
- 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) -> Vec<Car>`
|
#### `parse_all_times(my_cars, ctx, cars_db_path, times_dir) -> Result<Vec<Car>, AppError>`
|
||||||
**Orchestrates concurrent parsing of all car timing files**
|
**Orchestrates concurrent parsing of all car timing files** (`src/parsing/mod.rs`)
|
||||||
|
|
||||||
|
- Loads car list from database
|
||||||
- Uses `Semaphore(CONCURRENCY_LIMIT)` for bounded concurrency
|
- Uses `Semaphore(CONCURRENCY_LIMIT)` for bounded concurrency
|
||||||
- Spawns blocking tasks via `tokio::task::spawn_blocking`
|
- Acquires semaphore in async context, then runs sync parsing via `spawn_blocking`
|
||||||
- Returns the full car list for downstream use
|
- Returns the full car list for downstream use
|
||||||
|
|
||||||
##### `compute_ideal_wishlist(counts, owned, min_count, max_wishlist) -> Vec<(BestCarRecord, usize)>`
|
#### `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::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<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**
|
**Computes the ideal wishlist**
|
||||||
|
|
||||||
- Filters to cars with count >= `min_count`
|
- Filters to cars with count >= `min_count`
|
||||||
|
|
@ -234,39 +335,9 @@ Loads user data, parses all timing files, and computes car counts.
|
||||||
- Sorts by RQ
|
- Sorts by RQ
|
||||||
- Caps at `max_wishlist`
|
- Caps at `max_wishlist`
|
||||||
|
|
||||||
#### Output Functions
|
### Sorting Helpers
|
||||||
|
|
||||||
##### `write_sorted_csv(path, header, entries, cmp, format_row)`
|
#### `compare_records(a: &BestCarRecord, b: &BestCarRecord) -> std::cmp::Ordering`
|
||||||
**Generic sorted CSV writer**
|
**Comparison for BestCarRecord sorting**
|
||||||
|
|
||||||
- Sorts by index (avoids cloning entries)
|
- Sorts by: RQ → Brand → Model name
|
||||||
- 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**
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## System Overview
|
## System Overview
|
||||||
|
|
||||||
The Top Drives Records Analyzer consists of a main analysis binary and a shared library working together to provide comprehensive car performance analysis for the Top Drives racing game.
|
The Top Drives Records Analyzer is a Rust CLI tool for analyzing car performance data in the Top Drives racing game. It consists of a shared library (`src/lib.rs`), a binary entry point (`src/records.rs`), and three internal module trees: `commands/`, `parsing/`, and `output/`.
|
||||||
|
|
||||||
## Component Architecture
|
## Component Architecture
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ The Top Drives Records Analyzer consists of a main analysis binary and a shared
|
||||||
- **Data Types**: Defines `Car`, `BestCarRecord`, `AnalysisContext`, `CarCounts`, `MyCarsData`, `AppError`
|
- **Data Types**: Defines `Car`, `BestCarRecord`, `AnalysisContext`, `CarCounts`, `MyCarsData`, `AppError`
|
||||||
- **Track Name Translation**: Converts track codes to human-readable names via `translate_track_name()`
|
- **Track Name Translation**: Converts track codes to human-readable names via `translate_track_name()`
|
||||||
- **Track Type Translation**: Converts condition codes to surface descriptions via `translate_track_type()`
|
- **Track Type Translation**: Converts condition codes to surface descriptions via `translate_track_type()`
|
||||||
- **File Download**: Async file download utility via `get_file()`
|
- **File Download**: Async file download utility with retry logic via `get_file()`
|
||||||
- **User Data Loading**: Parses `my_cars.json` via `load_my_cars_data()`
|
- **User Data Loading**: Parses `my_cars.json` via `load_my_cars_data()`
|
||||||
- **Car Comparison**: Sorting comparator via `compare_cars()`
|
- **Car Comparison**: Sorting comparator via `compare_cars()`
|
||||||
- **Count Aggregation**: Per-car win count computation via `compute_car_counts()`
|
- **Count Aggregation**: Per-car win count computation via `compute_car_counts()`
|
||||||
|
|
@ -26,18 +26,44 @@ All public functions return `Result<T, AppError>` where `AppError` is a unified
|
||||||
- `AppError::Network` — HTTP/download errors
|
- `AppError::Network` — HTTP/download errors
|
||||||
- `AppError::Parse` — JSON parsing errors with file context
|
- `AppError::Parse` — JSON parsing errors with file context
|
||||||
- `AppError::MissingFile` — Required data file not found
|
- `AppError::MissingFile` — Required data file not found
|
||||||
- `AppError::Poisoned` — Mutex poisoning from panicked threads
|
- `AppError::Internal` — Runtime errors (semaphore closed, task join failures)
|
||||||
|
|
||||||
### 2. Records Binary (`src/records.rs`)
|
### 2. Records Binary (`src/records.rs`)
|
||||||
|
|
||||||
**Primary analysis engine** that processes pre-downloaded JSON timing data.
|
**CLI entry point** that defines command-line arguments and dispatches to command handlers.
|
||||||
|
|
||||||
#### Key Responsibilities
|
#### Key Responsibilities
|
||||||
- **Data Fetching**: Downloads car and track databases from GitHub repository
|
- **CLI Definition**: `clap` derive-based argument parsing with global path options
|
||||||
- **File Processing**: Parses JSON timing files for each car configuration
|
- **Command Dispatch**: Routes to the appropriate command handler in `commands/`
|
||||||
- **Performance Analysis**: Compares timing data across RQ ranges and track conditions
|
- **Error Reporting**: Catches all errors and prints to stderr with exit code 1
|
||||||
- **Collection Management**: Identifies owned cars and optimization opportunities
|
|
||||||
- **CSV Output**: Writes analysis results to CSV files with proper escaping
|
#### Global CLI Options
|
||||||
|
- `--my-cars` — Path to user's car collection export (default: `my_cars.json`)
|
||||||
|
- `--cars-db` — Path to car database file (default: `cars_repo.json`)
|
||||||
|
- `--times-dir` — Directory for per-car timing JSON files (default: `times`)
|
||||||
|
- `--tracks-db` — Path to tracks database file (default: `tracks_repo.json`)
|
||||||
|
|
||||||
|
### 3. Commands Module (`src/commands/`)
|
||||||
|
|
||||||
|
**Command handlers** that orchestrate analysis and output generation.
|
||||||
|
|
||||||
|
| Module | Command | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `fetch.rs` | `fetch` | Downloads car DB, tracks DB, and per-car timing files |
|
||||||
|
| `best.rs` | `best` | Generates `best.csv` and `very_best.csv` |
|
||||||
|
| `trash.rs` | `trash` | Generates `trash.csv` (owned cars not best on any track) |
|
||||||
|
| `wishlist.rs` | `wishlist` | Generates `wishlist.csv`, `add.csv`, `remove.csv` |
|
||||||
|
| `details.rs` | `details` | Generates `details.csv` with all timing data |
|
||||||
|
| `track.rs` | `track` | Prints best car for a specific track/condition/RQ |
|
||||||
|
| `all.rs` | `all` | Generates all output files in a single analysis pass |
|
||||||
|
|
||||||
|
#### Shared Helpers (`src/commands/mod.rs`)
|
||||||
|
- `run_analysis(owned, cars_db_path, times_dir)` — Common analysis setup: loads car DB, parses timing files, computes counts
|
||||||
|
- `load_my_cars_or_warn(path)` — Loads user data with graceful degradation (returns empty data if file missing)
|
||||||
|
|
||||||
|
### 4. Parsing Module (`src/parsing/`)
|
||||||
|
|
||||||
|
**Core parsing logic** for car timing data and car database loading.
|
||||||
|
|
||||||
#### Key Constants
|
#### Key Constants
|
||||||
- `MAX_RQ: u8 = 113` — Maximum RQ level used for range analysis
|
- `MAX_RQ: u8 = 113` — Maximum RQ level used for range analysis
|
||||||
|
|
@ -46,18 +72,42 @@ All public functions return `Result<T, AppError>` where `AppError` is a unified
|
||||||
- `TEST_BOWL_FACTOR: f64 = 2000.0` — Multiplier for Test Bowl time inversion
|
- `TEST_BOWL_FACTOR: f64 = 2000.0` — Multiplier for Test Bowl time inversion
|
||||||
- `CONCURRENCY_LIMIT: usize = 20` — Maximum concurrent tasks
|
- `CONCURRENCY_LIMIT: usize = 20` — Maximum concurrent tasks
|
||||||
|
|
||||||
|
#### Key Functions
|
||||||
|
- `parse_car_times()` — Synchronous per-car timing file parser (runs inside `spawn_blocking`)
|
||||||
|
- `update_best_car()` — Updates `DashMap` with new performance records across RQ range
|
||||||
|
- `parse_all_times()` — Orchestrates concurrent parsing with semaphore + `spawn_blocking`
|
||||||
|
- `fetch_times()` — Downloads per-car timing files with bounded concurrency
|
||||||
|
- `get_car_list()` (`car_list.rs`) — Loads car database via serde deserialization
|
||||||
|
|
||||||
|
### 5. Output Module (`src/output/`)
|
||||||
|
|
||||||
|
**CSV output utilities** and sorting helpers.
|
||||||
|
|
||||||
|
- `write_sorted_csv()` — Generic sorted CSV writer (index-based sorting to avoid cloning)
|
||||||
|
- `write_best_csv()`, `write_very_best_csv()` — Best car CSV output
|
||||||
|
- `write_trash_csv()` — Trash car CSV output
|
||||||
|
- `write_details_csv()` — Comprehensive timing data CSV output
|
||||||
|
- `write_wishlist_files()` — Wishlist, add, and remove CSV output
|
||||||
|
- `print_best_car()` — Stdout output for track-specific query
|
||||||
|
- `compute_ideal_wishlist()` — Ideal wishlist computation
|
||||||
|
- `compare_records()` — Sorting comparator for `BestCarRecord`
|
||||||
|
|
||||||
#### Data Flow
|
#### Data Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
GitHub Repository
|
GitHub Repository / API
|
||||||
↓
|
↓
|
||||||
cars_repo.json + tracks_repo.json
|
cars_repo.json + tracks_repo.json ← fetch command
|
||||||
↓
|
↓
|
||||||
times/*.json (1000+ car files)
|
times/*.json (1000+ car files) ← fetch command
|
||||||
↓
|
↓
|
||||||
JSON Parsing → BestCarRecord Analysis
|
parse_all_times() → parse_car_times() × N cars (concurrent via spawn_blocking)
|
||||||
↓
|
↓
|
||||||
User Collection (my_cars.json)
|
AnalysisContext (DashMap<TripletKey, Vec<BestCarRecord>>)
|
||||||
|
↓
|
||||||
|
compute_car_counts() → CarCounts
|
||||||
|
↓
|
||||||
|
User Collection (my_cars.json) → owned/wishlist sets
|
||||||
↓
|
↓
|
||||||
Output: CSV files / Terminal Results
|
Output: CSV files / Terminal Results
|
||||||
```
|
```
|
||||||
|
|
@ -74,7 +124,7 @@ type TripletKey = (String, u8, String);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Purpose**: Composite key for uniquely identifying track conditions
|
**Purpose**: Composite key for uniquely identifying track conditions
|
||||||
**Usage**: HashMap key for storing best car records
|
**Usage**: `DashMap` key for storing best car records
|
||||||
|
|
||||||
#### Car
|
#### Car
|
||||||
```rust
|
```rust
|
||||||
|
|
@ -82,10 +132,10 @@ type TripletKey = (String, u8, String);
|
||||||
struct Car {
|
struct Car {
|
||||||
rid: String, // Repository ID
|
rid: String, // Repository ID
|
||||||
brand: String, // Manufacturer
|
brand: String, // Manufacturer
|
||||||
only_name: String, // Model name
|
only_name: String, // Model name (serde rename: "onlyName")
|
||||||
year: u16, // Model year
|
year: u16, // Model year
|
||||||
rq: u8, // Rarity level
|
rq: u8, // Rarity level
|
||||||
prize: bool, // Prize car status
|
prize: bool, // Prize car status (serde default)
|
||||||
guid: String, // Game GUID
|
guid: String, // Game GUID
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -111,13 +161,13 @@ struct BestCarRecord {
|
||||||
### AnalysisContext
|
### AnalysisContext
|
||||||
```rust
|
```rust
|
||||||
struct AnalysisContext {
|
struct AnalysisContext {
|
||||||
best_cars: Arc<Mutex<HashMap<TripletKey, Vec<BestCarRecord>>>>,
|
best_cars: Arc<DashMap<TripletKey, Vec<BestCarRecord>>>,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Thread Safety**: Protected by `Mutex` for concurrent access during parsing
|
**Thread Safety**: Uses `DashMap` for sharded internal locking, providing high-concurrency access without a global mutex.
|
||||||
**Lifetime**: Created in `main()`, passed through the call chain
|
**Lifetime**: Created in command handlers, passed through the call chain.
|
||||||
**Access**: `lock_best_cars()` method returns `Result` with proper error handling
|
**Access**: Direct `DashMap` methods (`insert`, `get`, `iter`, `entry`).
|
||||||
|
|
||||||
### CarCounts
|
### CarCounts
|
||||||
```rust
|
```rust
|
||||||
|
|
@ -152,17 +202,19 @@ Multiple validation layers ensure data quality:
|
||||||
2. **Downvote Filter**: Community-downvoted times are excluded
|
2. **Downvote Filter**: Community-downvoted times are excluded
|
||||||
3. **Tie Detection**: Times within `TIME_TOLERANCE` (5ms) are considered equal
|
3. **Tie Detection**: Times within `TIME_TOLERANCE` (5ms) are considered equal
|
||||||
4. **Special Cases**: Test Bowl track uses inverted time calculation (`TEST_BOWL_FACTOR / time`)
|
4. **Special Cases**: Test Bowl track uses inverted time calculation (`TEST_BOWL_FACTOR / time`)
|
||||||
|
5. **Track Name Splitting**: Uses `rsplit_once("_a")` to correctly handle track names containing `_a` (e.g., `airplaneHangars`)
|
||||||
|
|
||||||
### Concurrent Processing
|
### Concurrent Processing
|
||||||
|
|
||||||
#### Bounded Concurrency Pattern
|
#### Bounded Concurrency Pattern
|
||||||
- **Task Spawning**: One blocking task per car file via `tokio::task::spawn_blocking`
|
- **Semaphore First**: Semaphore permit acquired in async context (avoids deadlocking the blocking thread pool)
|
||||||
- **Semaphore**: Limits concurrent operations to `CONCURRENCY_LIMIT` (20)
|
- **Blocking Work**: Synchronous parsing runs inside `tokio::task::spawn_blocking` while holding the permit
|
||||||
- **Error Isolation**: Individual car failures don't stop batch processing
|
- **Concurrency Limit**: Maximum `CONCURRENCY_LIMIT` (20) simultaneous operations
|
||||||
|
|
||||||
#### Synchronization
|
#### Synchronization
|
||||||
|
- **DashMap**: Sharded concurrent HashMap — no global lock needed for parallel parsing
|
||||||
- **Join Handles**: Waits for all parsing tasks to complete
|
- **Join Handles**: Waits for all parsing tasks to complete
|
||||||
- **Mutex Protection**: Safe concurrent access to shared `AnalysisContext`
|
- **Error Collection**: Parse errors are counted and reported; all-cars-failure propagates as an error
|
||||||
|
|
||||||
## File Organization
|
## File Organization
|
||||||
|
|
||||||
|
|
@ -170,7 +222,7 @@ Multiple validation layers ensure data quality:
|
||||||
```
|
```
|
||||||
├── cars_repo.json # Car database from repository
|
├── cars_repo.json # Car database from repository
|
||||||
├── tracks_repo.json # Track definitions
|
├── tracks_repo.json # Track definitions
|
||||||
├── track_names.json # Track code → name mappings (compiled in)
|
├── track_names.json # Track code → name mappings (compiled into binary)
|
||||||
├── my_cars.json # User's car collection (game export)
|
├── my_cars.json # User's car collection (game export)
|
||||||
└── times/ # Individual car timing files
|
└── times/ # Individual car timing files
|
||||||
├── Ferrari_F40_1987.json
|
├── Ferrari_F40_1987.json
|
||||||
|
|
@ -191,12 +243,13 @@ Multiple validation layers ensure data quality:
|
||||||
|
|
||||||
## Performance Characteristics
|
## Performance Characteristics
|
||||||
|
|
||||||
### Records Module
|
|
||||||
- **Startup Time**: Fast (uses cached JSON files)
|
- **Startup Time**: Fast (uses cached JSON files)
|
||||||
- **Memory Usage**: Moderate (loads all timing data into memory)
|
- **Memory Usage**: Moderate (loads all timing data into memory)
|
||||||
- **Accuracy**: High (comprehensive dataset)
|
- **Accuracy**: High (comprehensive dataset)
|
||||||
- **Update Frequency**: Manual refresh required
|
- **Update Frequency**: Manual refresh required
|
||||||
- **Concurrency**: Bounded at 20 simultaneous operations via semaphore
|
- **Concurrency**: Bounded at 20 simultaneous operations via semaphore
|
||||||
|
- **Lock Contention**: Minimal (DashMap sharded locking replaces global Mutex)
|
||||||
|
- **Allocation Optimization**: `update_best_car` avoids cloning on the last RQ iteration
|
||||||
|
|
||||||
## Error Handling Strategy
|
## Error Handling Strategy
|
||||||
|
|
||||||
|
|
@ -206,12 +259,13 @@ All errors are represented as `AppError`:
|
||||||
- **Network Errors**: Download failures with descriptive messages
|
- **Network Errors**: Download failures with descriptive messages
|
||||||
- **Parse Errors**: JSON parsing failures with file name context
|
- **Parse Errors**: JSON parsing failures with file name context
|
||||||
- **Missing Files**: Specific variant for absent data files
|
- **Missing Files**: Specific variant for absent data files
|
||||||
- **Poisoned Mutex**: Thread panic detection
|
- **Internal Errors**: Runtime failures (semaphore closed, task join errors)
|
||||||
|
|
||||||
### Graceful Degradation
|
### Graceful Degradation
|
||||||
- **Missing User Data**: `load_my_cars_or_warn()` returns empty data with a warning
|
- **Missing User Data**: `load_my_cars_or_warn()` returns empty data with a warning
|
||||||
- **Network Errors**: Fetch errors logged but don't crash
|
- **Missing Timing Files**: Individual car file failures emit warnings and skip
|
||||||
- **Parse Failures**: Individual car file failures emit warnings and skip
|
- **Network Errors**: Fetch errors logged; first error propagated
|
||||||
|
- **All Parse Failure**: If every car fails to parse, the error is propagated
|
||||||
|
|
||||||
### User Feedback
|
### User Feedback
|
||||||
- **Progress Indication**: Shows fetching/download progress
|
- **Progress Indication**: Shows fetching/download progress
|
||||||
|
|
@ -225,15 +279,15 @@ All errors are represented as `AppError`:
|
||||||
2. Add track name mapping in `track_names.json`
|
2. Add track name mapping in `track_names.json`
|
||||||
3. Add condition mappings in `translate_track_type()` if needed
|
3. Add condition mappings in `translate_track_type()` if needed
|
||||||
|
|
||||||
### Adding New Analysis Types
|
### Adding New Commands
|
||||||
1. Create new output function(s) using `write_sorted_csv`
|
1. Create new command handler in `src/commands/`
|
||||||
2. Add command variant to the `Commands` enum
|
2. Add command variant to the `Commands` enum in `src/records.rs`
|
||||||
3. Add command handler in `main()`
|
3. Add dispatch case in the `run()` function
|
||||||
4. Update help text and documentation
|
4. Update documentation
|
||||||
|
|
||||||
### Adding New Output Formats
|
### Adding New Output Formats
|
||||||
1. Create new output function(s) alongside existing CSV writers
|
1. Create new output function(s) in `src/output/mod.rs`
|
||||||
2. Reuse `CarCounts` for data access
|
2. Reuse `CarCounts` and `AnalysisContext` for data access
|
||||||
3. Add CLI flag for format selection if needed
|
3. Add CLI flag for format selection if needed
|
||||||
|
|
||||||
## Security Considerations
|
## Security Considerations
|
||||||
|
|
@ -241,6 +295,7 @@ All errors are represented as `AppError`:
|
||||||
### Network Access
|
### Network Access
|
||||||
- **HTTPS Only**: All external requests use HTTPS
|
- **HTTPS Only**: All external requests use HTTPS
|
||||||
- **Rate Limiting**: Respects server limits with controlled concurrency
|
- **Rate Limiting**: Respects server limits with controlled concurrency
|
||||||
|
- **Atomic Writes**: Downloads write to `.tmp` files first, then rename on success
|
||||||
|
|
||||||
### Local File Access
|
### Local File Access
|
||||||
- **Read Only**: No modification of game files
|
- **Read Only**: No modification of game files
|
||||||
|
|
@ -255,8 +310,6 @@ All errors are represented as `AppError`:
|
||||||
3. **Configuration Files**: External configuration for analysis parameters
|
3. **Configuration Files**: External configuration for analysis parameters
|
||||||
4. **GUI Interface**: Web-based interface for non-technical users
|
4. **GUI Interface**: Web-based interface for non-technical users
|
||||||
5. **Export Formats**: Multiple output formats (JSON, XML, etc.)
|
5. **Export Formats**: Multiple output formats (JSON, XML, etc.)
|
||||||
6. **Module Split**: Break `records.rs` into a module tree for better organization
|
|
||||||
7. **Concurrent HashMap**: Use `DashMap` for finer-grained locking
|
|
||||||
|
|
||||||
### Scalability Considerations
|
### Scalability Considerations
|
||||||
1. **Pagination**: Handle large datasets with pagination
|
1. **Pagination**: Handle large datasets with pagination
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,22 @@ Fetching https://api.topdrivesrecords.com/car/1002.json.
|
||||||
|
|
||||||
## Command Reference
|
## Command Reference
|
||||||
|
|
||||||
|
### Global Options
|
||||||
|
|
||||||
|
All commands accept these optional flags to customize file paths:
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `--my-cars <PATH>` | `my_cars.json` | Path to user's car collection export |
|
||||||
|
| `--cars-db <PATH>` | `cars_repo.json` | Path to car database file |
|
||||||
|
| `--times-dir <DIR>` | `times` | Directory containing per-car timing JSON files |
|
||||||
|
| `--tracks-db <PATH>` | `tracks_repo.json` | Path to tracks database file |
|
||||||
|
|
||||||
|
**Example with custom paths:**
|
||||||
|
```bash
|
||||||
|
cargo run --bin records -- --cars-db /data/cars.json --times-dir /data/times best
|
||||||
|
```
|
||||||
|
|
||||||
### Basic Analysis Commands
|
### Basic Analysis Commands
|
||||||
|
|
||||||
#### Best Cars Analysis
|
#### Best Cars Analysis
|
||||||
|
|
@ -407,12 +423,13 @@ The tool generates CSV files for external analysis. All files are sorted by car
|
||||||
**Problem:** No arguments provided
|
**Problem:** No arguments provided
|
||||||
**Solution:** Use `best`, `trash`, `wishlist`, `all`, `details`, `track`, or `fetch`
|
**Solution:** Use `best`, `trash`, `wishlist`, `all`, `details`, `track`, or `fetch`
|
||||||
|
|
||||||
#### "Could not read my cars file"
|
#### "Could not read my cars file" / "Missing data file"
|
||||||
**Problem:** `my_cars.json` not found or not readable
|
**Problem:** `my_cars.json` (or another data file) not found or not readable
|
||||||
**Solution:**
|
**Solution:**
|
||||||
1. Export fresh data from Top Drives game
|
1. Export fresh data from Top Drives game
|
||||||
2. Ensure file is named exactly `my_cars.json`
|
2. Ensure file is named exactly `my_cars.json` (or use `--my-cars <PATH>` to specify a custom location)
|
||||||
3. Check file permissions
|
3. Check file permissions
|
||||||
|
4. If `cars_repo.json` is missing, run `fetch` first (or use `--cars-db <PATH>`)
|
||||||
|
|
||||||
#### "Failed to fetch the file"
|
#### "Failed to fetch the file"
|
||||||
**Problem:** Network issues or API unavailable
|
**Problem:** Network issues or API unavailable
|
||||||
|
|
|
||||||
|
|
@ -1,396 +0,0 @@
|
||||||
# Top Drives Records Analyzer — Code Review
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
The project is a well-structured Rust CLI tool for analyzing car performance data from the Top Drives racing game. The code is generally clean, well-documented, and follows good Rust practices. The recent v0.3.0 refactor (moving from global mutable state to `AnalysisContext`, adding `CarCounts`, and introducing the `all` command) shows a clear improvement trajectory.
|
|
||||||
|
|
||||||
However, there are several areas where the codebase could be significantly improved: **error handling robustness**, **test coverage depth**, **documentation accuracy**, **performance optimizations**, and **code organization**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Code Structure & Design
|
|
||||||
|
|
||||||
### 1.1 ✅ What's Done Well
|
|
||||||
|
|
||||||
- **Clean separation** between library ([`lib.rs`](src/lib.rs)) and binary ([`records.rs`](src/records.rs))
|
|
||||||
- **Good use of `AnalysisContext`** to eliminate global mutable state — this is a solid architectural improvement
|
|
||||||
- **`CarCounts` struct** centralizes derived data computation, avoiding redundant calculations
|
|
||||||
- **`write_sorted_csv` generic helper** eliminates CSV writing boilerplate across output functions
|
|
||||||
- **Bounded concurrency** via `Semaphore(20)` prevents resource exhaustion
|
|
||||||
- **`clap` derive macros** for CLI parsing — idiomatic and maintainable
|
|
||||||
|
|
||||||
### 1.2 🔧 Suggested Improvements
|
|
||||||
|
|
||||||
#### A. Monolithic `records.rs` — Break Into Modules
|
|
||||||
|
|
||||||
[`records.rs`](src/records.rs) is 923 lines with parsing logic, output logic, fetch logic, CLI definition, and tests all in one file. This should be split into a module tree:
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── lib.rs
|
|
||||||
├── records.rs # main() + CLI only
|
|
||||||
├── commands/
|
|
||||||
│ ├── mod.rs # Command dispatch
|
|
||||||
│ ├── fetch.rs # Fetch logic
|
|
||||||
│ ├── best.rs # Best/very_best output
|
|
||||||
│ ├── trash.rs # Trash output
|
|
||||||
│ ├── wishlist.rs # Wishlist/add/remove output
|
|
||||||
│ ├── details.rs # Details output
|
|
||||||
│ └── track.rs # Track-specific output
|
|
||||||
├── parsing/
|
|
||||||
│ ├── mod.rs # parse_all_times, parse_car_times
|
|
||||||
│ └── car_list.rs # get_car_list
|
|
||||||
└── output/
|
|
||||||
├── mod.rs
|
|
||||||
└── csv_writer.rs # write_sorted_csv, format_counted_row
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. `parse_car_times` Is Async But Does No Async Work
|
|
||||||
|
|
||||||
[`parse_car_times()`](src/records.rs:491) is declared `async` but only performs synchronous file I/O and JSON parsing. It's called inside `tokio::task::spawn`, which means it's running on the Tokio runtime but blocking it with `fs::read_to_string` and `serde_json::from_str`.
|
|
||||||
|
|
||||||
**Recommendation**: Either use `tokio::task::spawn_blocking` for the synchronous work, or use `tokio::fs::read_to_string` for truly async file I/O. The current approach blocks the Tokio runtime threads.
|
|
||||||
|
|
||||||
#### C. `update_best_car` Allocates Excessively
|
|
||||||
|
|
||||||
[`update_best_car()`](src/records.rs:571) clones `new_record` for every RQ level from `car_rq` to `MAX_RQ` (potentially 28 clones for an RQ 85 car). It also allocates a new `String` for each key on every iteration.
|
|
||||||
|
|
||||||
**Recommendation**: Consider building the key once and reusing it, or restructuring to reduce allocations in this hot path.
|
|
||||||
|
|
||||||
#### D. `AnalysisContext` Re-construction in `parse_all_times`
|
|
||||||
|
|
||||||
In [`parse_all_times()`](src/records.rs:625), a new `AnalysisContext` is constructed from the cloned `Arc`:
|
|
||||||
```rust
|
|
||||||
parse_car_times(car, &my_cars, &AnalysisContext { best_cars }).await;
|
|
||||||
```
|
|
||||||
This works but is semantically odd — it creates a new struct just to pass the same `Arc<Mutex>`. Consider having `parse_car_times` accept `&Arc<Mutex<...>>` directly, or pass `&AnalysisContext` by cloning the Arc.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Error Handling
|
|
||||||
|
|
||||||
### 2.1 🔴 Critical: Silent Failures
|
|
||||||
|
|
||||||
Several functions silently swallow errors and continue with empty data:
|
|
||||||
|
|
||||||
- [`get_car_list()`](src/records.rs:688) returns `Vec::new()` on file-not-found or parse errors — if `cars_repo.json` is missing, the entire analysis runs on zero cars with no indication
|
|
||||||
- [`parse_car_times()`](src/records.rs:497) returns `()` and silently skips cars with unreadable files
|
|
||||||
- [`load_my_cars_data()`](src/lib.rs:172) returns empty sets on failure
|
|
||||||
|
|
||||||
**Recommendation**: Use `Result<T, E>` return types consistently. For the binary, consider using `anyhow` or `thiserror` for ergonomic error handling. At minimum, distinguish between "file missing" (warn) and "file corrupt" (error).
|
|
||||||
|
|
||||||
### 2.2 🔴 Mixed Error Handling Strategies
|
|
||||||
|
|
||||||
The codebase mixes:
|
|
||||||
- `Result<(), String>` for async functions ([`get_file()`](src/lib.rs:242))
|
|
||||||
- `std::io::Result<()>` for output functions
|
|
||||||
- `eprintln!` + early return for input parsing
|
|
||||||
- `expect()` for mutex poisoning
|
|
||||||
|
|
||||||
**Recommendation**: Define a custom error type using `thiserror`:
|
|
||||||
```rust
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
enum AppError {
|
|
||||||
#[error("IO error: {0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
#[error("Network error: {0}")]
|
|
||||||
Network(String),
|
|
||||||
#[error("Parse error in {file}: {reason}")]
|
|
||||||
Parse { file: String, reason: String },
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.3 🟡 `unwrap()` vs `expect()` Inconsistency
|
|
||||||
|
|
||||||
- [`update_best_car()`](src/records.rs:581) uses `best_cars.lock().unwrap()`
|
|
||||||
- [`compute_car_counts()`](src/lib.rs:117) uses `.expect("best_cars mutex poisoned...")`
|
|
||||||
|
|
||||||
**Recommendation**: Use `.expect()` consistently with descriptive messages, or better yet, use a helper method on `AnalysisContext` that centralizes lock acquisition.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Test Coverage
|
|
||||||
|
|
||||||
### 3.1 🔴 Critical: Minimal Test Coverage
|
|
||||||
|
|
||||||
The test suite in [`records.rs:736-923`](src/records.rs:736) has only **8 tests**, and several are superficial:
|
|
||||||
|
|
||||||
| Test | What It Actually Tests | Quality |
|
|
||||||
|------|----------------------|---------|
|
|
||||||
| `test_split_once` | Standard library `split_once` behavior | ❌ Trivial — tests stdlib |
|
|
||||||
| `test_translate_track_name` | Track name lookup | ✅ Good |
|
|
||||||
| `test_translate_track_type` | Condition code lookup | ✅ Good |
|
|
||||||
| `test_update_best_car_logic` | Only checks struct field values | ❌ Doesn't test `update_best_car` |
|
|
||||||
| `test_rq_range_analysis` | RQ range insertion | ✅ Good |
|
|
||||||
| `test_time_validation` | Two float comparisons | ❌ Trivial — tests nothing |
|
|
||||||
| `test_track_name_completeness` | Sample of track name mappings | ✅ Good |
|
|
||||||
| `test_condition_completeness` | Sample of condition mappings | ✅ Good |
|
|
||||||
| `test_time_tolerance_constant` | Tie-breaking logic | ✅ Best test in the suite |
|
|
||||||
|
|
||||||
### 3.2 🔧 Missing Test Categories
|
|
||||||
|
|
||||||
**No tests for**:
|
|
||||||
- [`compute_car_counts()`](src/lib.rs:115) — critical aggregation logic
|
|
||||||
- [`write_sorted_csv()`](src/records.rs:202) — output formatting
|
|
||||||
- [`write_best_csv()`](src/records.rs:232), [`write_trash_csv()`](src/records.rs:260), [`write_wishlist_files()`](src/records.rs:296) — all output functions
|
|
||||||
- [`compute_ideal_wishlist()`](src/records.rs:277) — wishlist algorithm
|
|
||||||
- [`get_car_list()`](src/records.rs:688) — car database parsing
|
|
||||||
- [`load_my_cars_data()`](src/lib.rs:172) — user data parsing
|
|
||||||
- [`compare_cars()`](src/lib.rs:232), [`compare_records()`](src/records.rs:191) — sort comparators
|
|
||||||
- [`parse_car_times()`](src/records.rs:491) — core parsing logic
|
|
||||||
- CSV output correctness (field ordering, escaping, headers)
|
|
||||||
- Edge cases: empty data, single car, all cars owned, no cars owned
|
|
||||||
|
|
||||||
### 3.3 🔧 Recommended Test Improvements
|
|
||||||
|
|
||||||
1. **Add integration tests** in a `tests/` directory that exercise the full pipeline with fixture data
|
|
||||||
2. **Add property-based tests** using `proptest` for the sorting and comparison functions
|
|
||||||
3. **Test `update_best_car`** more thoroughly: test replacement, test tie-breaking, test multiple cars at different RQ levels
|
|
||||||
4. **Test CSV output** by writing to a buffer and comparing against expected output
|
|
||||||
5. **Remove trivial tests** that only test standard library behavior (`test_split_once`, `test_time_validation`)
|
|
||||||
6. **Add test utilities** for creating fixture `Car`, `BestCarRecord`, and `AnalysisContext` instances
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Performance
|
|
||||||
|
|
||||||
### 4.1 🟡 `update_best_car` Hot Path
|
|
||||||
|
|
||||||
[`update_best_car()`](src/records.rs:571) is called for every track × condition × car combination (potentially millions of calls). Current issues:
|
|
||||||
|
|
||||||
- **Holds mutex lock while iterating RQ range** — lock is held for `MAX_RQ - car_rq + 1` iterations
|
|
||||||
- **Clones `new_record` for each RQ level** — up to 28 clones per call
|
|
||||||
- **Allocates `String` keys in a loop** — heap allocation per iteration
|
|
||||||
|
|
||||||
**Recommendation**:
|
|
||||||
- Pre-compute a batch of updates and apply them under a single lock
|
|
||||||
- Use `String` interning or `&str` references for track IDs and conditions to reduce allocations
|
|
||||||
- Consider `DashMap` instead of `Mutex<HashMap>` for finer-grained locking
|
|
||||||
|
|
||||||
### 4.2 🟡 `parse_car_times` Does Synchronous I/O on Async Runtime
|
|
||||||
|
|
||||||
As noted in §1.2.B, `fs::read_to_string` blocks the Tokio runtime. With 1000+ cars and a semaphore of 20, this means 20 Tokio threads can be blocked simultaneously on file I/O.
|
|
||||||
|
|
||||||
### 4.3 🟡 `compute_car_counts` Clones All Records
|
|
||||||
|
|
||||||
[`compute_car_counts()`](src/lib.rs:115) clones every `BestCarRecord` when building `by_rid`. For large datasets, this is significant memory allocation.
|
|
||||||
|
|
||||||
### 4.4 ✅ Good: Semaphore-Based Concurrency
|
|
||||||
|
|
||||||
The `Semaphore(20)` in both [`parse_all_times()`](src/records.rs:616) and [`fetch_times()`](src/records.rs:644) is a good pattern for controlling concurrency.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Documentation Issues
|
|
||||||
|
|
||||||
### 5.1 🔴 Critical: `docs/API.md` Is Severely Outdated
|
|
||||||
|
|
||||||
[`docs/API.md`](docs/API.md) references:
|
|
||||||
- `print_best_cars(filter_out_owned: bool)` — **removed in v0.3.0**
|
|
||||||
- `print_best_times()` — **removed/renamed**
|
|
||||||
- `print_trash()` — **removed in v0.3.0**
|
|
||||||
- `missing` command — **removed in v0.3.0**
|
|
||||||
- `full_script` command — **never existed in current form**
|
|
||||||
- Old function signatures that no longer match the code
|
|
||||||
|
|
||||||
**This is the most critical documentation issue.** The API docs are actively misleading.
|
|
||||||
|
|
||||||
### 5.2 🔴 `docs/ARCHITECTURE.md` References Removed Code
|
|
||||||
|
|
||||||
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md:96) still references:
|
|
||||||
```
|
|
||||||
static BEST_CARS: Lazy<Mutex<HashMap<TripletKey, Vec<BestCarRecord>>>>
|
|
||||||
```
|
|
||||||
This global static was replaced by `AnalysisContext` in v0.3.0. The doc also lists `sorted.csv` as an output file, which doesn't appear in the current code.
|
|
||||||
|
|
||||||
### 5.3 🟡 `README.md` Dependency List Is Wrong
|
|
||||||
|
|
||||||
[`README.md`](README.md:289) lists `once_cell` as a dependency, but it's not in [`Cargo.toml`](Cargo.toml). The code uses `std::sync::OnceLock` instead (which is correct — the README is outdated).
|
|
||||||
|
|
||||||
### 5.4 🟡 `BestCarRecord` Missing `prize` Field in README
|
|
||||||
|
|
||||||
The [`README.md` data structure listing](README.md:319) for `BestCarRecord` doesn't include the `prize` field that exists in the [actual struct](src/lib.rs:69).
|
|
||||||
|
|
||||||
### 5.5 🟡 `docs/DATA_STRUCTURES.md` Code Examples Are Outdated
|
|
||||||
|
|
||||||
[`docs/DATA_STRUCTURES.md`](docs/DATA_STRUCTURES.md:237) shows code examples using `unwrap_or("")` patterns, while the actual code in [`get_car_list()`](src/records.rs:706) uses `filter_map` with `?` operator.
|
|
||||||
|
|
||||||
### 5.6 ✅ Good: Inline Documentation
|
|
||||||
|
|
||||||
The inline rustdoc comments are generally excellent — well-structured with purpose, arguments, examples, and special cases. The module-level doc comments are informative.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Specific Code Issues
|
|
||||||
|
|
||||||
### 6.1 🟡 Hardcoded File Paths
|
|
||||||
|
|
||||||
Multiple functions use hardcoded relative file paths:
|
|
||||||
- `"my_cars.json"` in [`load_my_cars_data()`](src/lib.rs:173)
|
|
||||||
- `"cars_repo.json"` in [`get_car_list()`](src/records.rs:689)
|
|
||||||
- `"times/"` prefix in [`parse_car_times()`](src/records.rs:494) and [`fetch_car()`](src/records.rs:679)
|
|
||||||
- Output file names: `"best.csv"`, `"trash.csv"`, etc.
|
|
||||||
|
|
||||||
**Recommendation**: Define constants or accept configurable paths via CLI flags.
|
|
||||||
|
|
||||||
### 6.2 🟡 Hardcoded URLs
|
|
||||||
|
|
||||||
[`get_tracks_file()`](src/records.rs:727) and [`get_cars_file()`](src/records.rs:732) have hardcoded GitHub URLs. These should be constants at minimum.
|
|
||||||
|
|
||||||
### 6.3 🟡 CSV Escaping
|
|
||||||
|
|
||||||
[`format_counted_row()`](src/records.rs:225) and other output functions don't handle CSV escaping. If car brand or model names contain commas, quotes, or newlines, the CSV output will be malformed.
|
|
||||||
|
|
||||||
**Recommendation**: Use the `csv` crate, or at minimum implement proper CSV escaping.
|
|
||||||
|
|
||||||
### 6.4 🟡 `Test Bowl` Magic Number
|
|
||||||
|
|
||||||
The Test Bowl time inversion `2000.0 / time` at [line 533](src/records.rs:533) uses a magic number with no explanation of why 2000.0 is the correct constant.
|
|
||||||
|
|
||||||
### 6.5 🟢 Minor: `year as u16` and `rq as u8` Casting
|
|
||||||
|
|
||||||
In [`get_car_list()`](src/records.rs:715), `year` and `rq` are cast from `u64` to `u16`/`u8` without bounds checking. If the JSON contains values outside the valid range, this will silently truncate.
|
|
||||||
|
|
||||||
### 6.6 🟢 Minor: `test_update_best_car_logic` Test Name Is Misleading
|
|
||||||
|
|
||||||
The test at [line 777](src/records.rs:777) is named `test_update_best_car_logic` but never actually calls `update_best_car()`. It only checks struct field values.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Dependency Concerns
|
|
||||||
|
|
||||||
### 7.1 🟡 `tokio` with `features = ["full"]`
|
|
||||||
|
|
||||||
[`Cargo.toml`](Cargo.toml:16) enables all Tokio features. For a CLI tool, only `rt-multi-thread`, `macros`, and `fs` are likely needed. Using `full` increases compile time and binary size.
|
|
||||||
|
|
||||||
### 7.2 🟡 No `serde` Derive for Data Structures
|
|
||||||
|
|
||||||
`Car` and `BestCarRecord` use manual JSON field extraction via `serde_json::Value`. If the JSON schemas are stable, deriving `serde::Deserialize` would be safer and more performant.
|
|
||||||
|
|
||||||
### 7.3 🟢 Missing `csv` Crate
|
|
||||||
|
|
||||||
Output is hand-formatted CSV. The `csv` crate would handle escaping, quoting, and provide a type-safe writing interface.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. `.gitignore` and Project Configuration
|
|
||||||
|
|
||||||
### 8.1 🟡 Output CSV Files Not in `.gitignore`
|
|
||||||
|
|
||||||
[`.gitignore`](.gitignore) ignores `times/`, `cars_repo.json`, `tracks_repo.json`, and `my_cars.json`, but doesn't ignore generated output files: `best.csv`, `very_best.csv`, `trash.csv`, `wishlist.csv`, `add.csv`, `remove.csv`, `details.csv`.
|
|
||||||
|
|
||||||
### 8.2 🟡 No `rustfmt.toml` or `clippy.toml`
|
|
||||||
|
|
||||||
No configuration for code formatting or linting standards.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Proposed Improvement Plan
|
|
||||||
|
|
||||||
### Priority 1 — High Impact
|
|
||||||
|
|
||||||
| # | Improvement | Rationale |
|
|
||||||
|---|-------------|-----------|
|
|
||||||
| 1 | Fix outdated `docs/API.md` | Currently misleading — references removed functions |
|
|
||||||
| 2 | Fix outdated `docs/ARCHITECTURE.md` | References removed global static |
|
|
||||||
| 3 | Fix `README.md` dependency list and struct definitions | Lists wrong deps and missing fields |
|
|
||||||
| 4 | Add output CSV files to `.gitignore` | Prevent accidental commits |
|
|
||||||
| 5 | Add tests for `compute_car_counts` | Critical untested aggregation logic |
|
|
||||||
| 6 | Add tests for `compute_ideal_wishlist` | Wishlist algorithm has no coverage |
|
|
||||||
| 7 | Add tests for CSV output functions | Output correctness is unverified |
|
|
||||||
|
|
||||||
### Priority 2 — Medium Impact
|
|
||||||
|
|
||||||
| # | Improvement | Rationale |
|
|
||||||
|---|-------------|-----------|
|
|
||||||
| 8 | Introduce custom error type with `thiserror` | Consistent error handling |
|
|
||||||
| 9 | Use `Result` returns instead of silent failures | Prevent hidden data loss |
|
|
||||||
| 10 | Add integration tests with fixture data | End-to-end correctness |
|
|
||||||
| 11 | Fix `parse_car_times` async/blocking mismatch | Prevents runtime thread starvation |
|
|
||||||
| 12 | Add CSV escaping or use `csv` crate | Data correctness for edge cases |
|
|
||||||
| 13 | Extract hardcoded URLs as constants | Maintainability |
|
|
||||||
| 14 | Remove trivial tests, rename misleading ones | Test suite quality |
|
|
||||||
|
|
||||||
### Priority 3 — Lower Impact / Refactoring
|
|
||||||
|
|
||||||
| # | Improvement | Rationale |
|
|
||||||
|---|-------------|-----------|
|
|
||||||
| 15 | Split `records.rs` into module tree | Maintainability as project grows |
|
|
||||||
| 16 | Consider `DashMap` for concurrent HashMap | Performance under high contention |
|
|
||||||
| 17 | Reduce allocations in `update_best_car` | Hot path optimization |
|
|
||||||
| 18 | Narrow `tokio` features from `full` | Compile time and binary size |
|
|
||||||
| 19 | Add `serde` derive for data structures | Type-safe parsing |
|
|
||||||
| 20 | Add `rustfmt.toml` and `clippy.toml` | Code consistency |
|
|
||||||
| 21 | Add configurable file paths via CLI | Flexibility |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Architecture Diagram — Current vs Proposed
|
|
||||||
|
|
||||||
### Current Architecture
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[main - records.rs] --> B[CLI parsing - clap]
|
|
||||||
B --> C[Fetch]
|
|
||||||
B --> D[Best]
|
|
||||||
B --> E[Trash]
|
|
||||||
B --> F[Wishlist]
|
|
||||||
B --> G[All]
|
|
||||||
B --> H[Details]
|
|
||||||
B --> I[Track]
|
|
||||||
|
|
||||||
C --> J[get_file - lib.rs]
|
|
||||||
D --> K[run_analysis]
|
|
||||||
E --> K
|
|
||||||
F --> K
|
|
||||||
G --> K
|
|
||||||
H --> K
|
|
||||||
I --> K
|
|
||||||
|
|
||||||
K --> L[parse_all_times]
|
|
||||||
L --> M[parse_car_times x N]
|
|
||||||
M --> N[update_best_car]
|
|
||||||
N --> O[AnalysisContext.best_cars]
|
|
||||||
|
|
||||||
K --> P[compute_car_counts]
|
|
||||||
P --> Q[CarCounts]
|
|
||||||
|
|
||||||
Q --> R[write_sorted_csv]
|
|
||||||
R --> S[CSV files]
|
|
||||||
|
|
||||||
style A fill:#f9f,stroke:#333
|
|
||||||
style O fill:#ff9,stroke:#333
|
|
||||||
style Q fill:#9f9,stroke:#333
|
|
||||||
```
|
|
||||||
|
|
||||||
### Proposed Module Structure
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[main.rs - CLI only] --> B[commands/mod.rs]
|
|
||||||
B --> C[fetch.rs]
|
|
||||||
B --> D[best.rs]
|
|
||||||
B --> E[trash.rs]
|
|
||||||
B --> F[wishlist.rs]
|
|
||||||
B --> G[details.rs]
|
|
||||||
B --> H[track.rs]
|
|
||||||
|
|
||||||
I[lib.rs] --> J[types.rs - Car, BestCarRecord, etc.]
|
|
||||||
I --> K[analysis.rs - AnalysisContext, CarCounts]
|
|
||||||
I --> L[translate.rs - track name/type]
|
|
||||||
I --> M[io.rs - get_file, load_my_cars_data]
|
|
||||||
|
|
||||||
D --> N[parsing/mod.rs]
|
|
||||||
E --> N
|
|
||||||
F --> N
|
|
||||||
G --> N
|
|
||||||
H --> N
|
|
||||||
N --> O[parse_all_times]
|
|
||||||
O --> P[parse_car_times]
|
|
||||||
|
|
||||||
Q[output/mod.rs] --> R[csv_writer.rs]
|
|
||||||
D --> Q
|
|
||||||
E --> Q
|
|
||||||
F --> Q
|
|
||||||
G --> Q
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue