top_drives/docs/API.md
Jan-Peter von Hunnius 89fa69e3c8 refactor: extract lib.rs, remove club module, add bounded concurrency
- Extract shared types (Car, BestCarRecord, AnalysisContext) into src/lib.rs
- Replace global static BEST_CARS with injected AnalysisContext for testability
- Add semaphore-bounded concurrency (limit 20) in parse_all_times()
- Replace hardcoded track name match with track_names.json loaded via include_str!
- Replace magic number 115 with const MAX_RQ: u8 = 113
- Improve floating-point tie comparison with TIME_TOLERANCE (5ms) constant
- Remove src/club.rs binary (topdrives.club no longer maintained)
- Remove scraper dependency from Cargo.toml
- Remove tracks.txt (replaced by track_names.json)
- Remove empty/placeholder test bodies
- Improve error handling in get_file() for response body read
- Add load_my_cars_data() combining owned + wishlist extraction in one I/O
- Update CHANGELOG, README, and all docs to reflect changes
2026-05-06 20:20:15 +02:00

13 KiB
Raw Blame History

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

Records Module (src/records.rs)

Primary analysis engine for processing JSON-based timing data.

Public Functions

main()

Entry point for the records analyzer

#[tokio::main]
async fn main()

Purpose: Command-line interface and operation dispatcher

Arguments: Command line arguments

  • args[1]: Command (fetch, best, missing, trash, details, track, full_script)

Commands:

  • fetch: Download all data files
  • best: Show best owned cars
  • missing: Show best unowned cars
  • trash: Show removable cars
  • details: Export all timing data
  • track: Track-specific analysis
  • full_script: Run fetch + all analysis commands

Example:

cargo run --bin records -- best
print_best_cars(filter_out_owned: bool)

Displays ranked list of best performing cars

fn print_best_cars(filter_out_owned: bool)

Purpose: Aggregates performance data across all tracks and conditions

Arguments:

  • filter_out_owned: If true, excludes owned cars from output

Output Format:

RQ,Brand,Model,Year,Count
85,McLaren,F1,1993,15
82,Ferrari,250 GTO,1962,12

Algorithm:

  1. Count occurrences of each car across all track/RQ/condition combinations
  2. Sort by RQ level, then by car identifier
  3. Apply ownership filter if requested
print_best_times()

Exports comprehensive timing data for all combinations

fn print_best_times()

Purpose: Detailed performance export for external analysis

Output Format:

RQ,TrackID,Condition,CarRQ,Brand,Model,Year,Config,Time
85,laguna,Dry,85,McLaren,F1,1993,233,112.45

Data Volume: Large output file with all track/condition/RQ combinations

print_best_car(track: &String, condition: &String, rq: &String)

Finds best car for specific track conditions

fn print_best_car(track: &String, condition: &String, rq: &String)

Purpose: Targeted analysis for specific racing scenarios

Arguments:

  • track: Track identifier (e.g., "laguna", "monaco")
  • condition: Surface condition (e.g., "Dry", "Wet")
  • rq: RQ level limit

Example:

print_best_car(&"laguna".to_string(), &"Dry".to_string(), &"85".to_string());
print_trash()

Identifies cars safe for removal from collection

fn print_trash()

Purpose: Collection optimization recommendations

Safety Criteria:

  • Car not among best performers on any track
  • RQ level below 80 (preserves high-end cars)
  • Not a prize car (may have collection value)

Output Format:

RQ,Brand,Model,Year
45,Toyota,Corolla,1995
parse_car_times(car: Car)

Processes timing data for a single car

async fn parse_car_times(car: Car)

Purpose: Core parsing logic for individual car performance data

Process Flow:

  1. Load car's JSON timing file
  2. Parse each configuration (111, 233, 323, etc.)
  3. Extract timing data for each track/condition
  4. Apply quality filters (downvotes, time validation)
  5. Update global best cars cache

Quality Filters:

  • Minimum time: 1.2 seconds
  • No downvotes from community
  • Special handling for Test Bowl track
parse_all_times()

Orchestrates concurrent processing of all cars

async fn parse_all_times()

Purpose: Coordinates parallel processing of car timing files

Concurrency: One async task per car file, bounded by a semaphore (limit 20) Synchronization: Waits for all tasks to complete

update_best_car(track_id: &str, car_rq: u8, surface: &str, new_record: BestCarRecord)

Updates global best cars database with new performance data

fn update_best_car(track_id: &str, car_rq: u8, surface: &str, new_record: BestCarRecord)

Purpose: Implements core best-car selection algorithm

RQ Range Analysis:

  • Tests car from base RQ up to MAX_RQ (113)
  • Compares against existing best times
  • Updates records for improved performance

Logic:

  1. For each RQ level from car_rq to MAX_RQ (113):
  2. If new time is better → replace record
  3. If new time ties → add to list (if not duplicate)
  4. If new time is worse → ignore
get_car_list() -> Vec<Car>

Loads complete car database from repository file

fn get_car_list() -> Vec<Car>

Purpose: Provides access to all known cars in the database

Source: cars_repo.json file Returns: Vector of all cars with complete metadata

get_my_car_list() -> Vec<String>

Loads user's car collection from game export

fn get_my_car_list() -> Vec<String>

Purpose: Identifies which cars the user owns

Source: my_cars.json game export Filter: Only locked: true cards Returns: Vector of owned car GUIDs

fetch_times()

Downloads timing files for all cars with rate limiting

async fn fetch_times()

Purpose: Batch download of car timing data

Rate Limiting: 20 concurrent downloads maximum (semaphore) Progress: Shows download progress for each file

fetch_car(rid: String)

Downloads timing data for single car

async fn fetch_car(rid: String)

Purpose: Individual car data fetching

URL Format: https://api.topdrivesrecords.com/car/{rid} Storage: times/{rid}.json

get_cars_file()

Downloads main car database

async fn get_cars_file()

Purpose: Updates car metadata database

Source: GitHub repository cars database Destination: cars_repo.json

get_tracks_file()

Downloads track definitions

async fn get_tracks_file()

Purpose: Updates track name mappings

Source: GitHub repository tracks database Destination: tracks_repo.json

get_file(file_url: &str, local_file_path: &str)

Generic file download utility

async fn get_file(file_url: &str, local_file_path: &str)

Purpose: Downloads files from remote URLs

Features:

  • Automatic existing file removal
  • Error handling with graceful failure
  • Progress indication through errors

Shared Library (src/lib.rs)

Common utilities shared across the project.

translate_track_name(track_code: &str) -> &str

Converts track codes to human-readable names

fn translate_track_name(track_code: &str) -> &str

Purpose: User-friendly track name display

Implementation: Loads mappings from track_names.json at compile time via include_str!

Coverage: All tracks defined in track_names.json Fallback: Returns original code if not found

translate_track_type(track_code: &str) -> &str

Converts condition codes to surface descriptions

fn translate_track_type(track_code: &str) -> &str

Purpose: Human-readable surface condition names

Mappings:

  • "00" → "Dry"
  • "01" → "Wet"
  • "10" → "Dirt"
  • "11" → "Dirt Wet"
split_prefix_postfix<'a>(input: &'a str, delimiter: &'a str) -> Option<(&'a str, &'a str)>

String splitting utility for track ID parsing

fn split_prefix_postfix<'a>(input: &'a str, delimiter: &'a str) -> Option<(&'a str, &'a str)>

Purpose: Parse track IDs with condition suffixes

Example:

split_prefix_postfix("laguna_a00", "_a") // Some(("laguna", "00"))

Data Structures

Records Module Data Types

Card

User's car collection entry

struct Card {
    cardId: String,    // Unique card identifier
    locked: bool,      // Ownership status
}

Car

Car metadata from database

struct Car {
    rid: String,       // Repository ID
    brand: String,     // Manufacturer
    only_name: String, // Model name
    year: u16,        // Model year
    rq: u8,           // Rarity level
    prize: bool,      // Prize car status
    guid: String,     // Game GUID
}

BestCarRecord

Performance record for best cars

struct BestCarRecord {
    car_rid: String,   // Car identifier
    brand: String,     // Manufacturer
    only_name: String, // Model name
    year: u16,        // Year
    rq: u8,           // RQ level
    config: String,    // Tune configuration
    time: f64,        // Track time
    owned: bool,      // User ownership
    guid: String,     // Game identifier
}

TripletKey

Composite key for track conditions

type TripletKey = (String, u8, String);
//   Track ID    RQ Level  Surface Condition

Constants

MAX_RQ

const MAX_RQ: u8 = 113;

Purpose: Maximum RQ level for range analysis. Replaces former magic number 115 with a named constant.

Global State

BEST_CARS Cache

Thread-safe global performance database

static BEST_CARS: Lazy<Mutex<HashMap<TripletKey, Vec<BestCarRecord>>>>

Initialization: One-time setup with once_cell::Lazy Thread Safety: Protected by Mutex for concurrent access Structure: HashMap with composite keys for efficient lookups

Error Handling

Network Errors

  • HTTP Failures: Logged but don't crash application
  • Invalid JSON: Graceful fallback to defaults
  • Missing Files: Clear error messages for user

Parse Errors

  • Malformed Data: Skip invalid entries
  • Type Conversion: Use safe defaults
  • Missing Fields: Provide sensible fallbacks

User Input Errors

  • Invalid Commands: Clear usage instructions
  • Missing Arguments: Descriptive error messages
  • Out of Range: Validation with helpful feedback

Usage Examples

Basic Analysis Workflow

// 1. Load user's car collection
let my_cars = get_my_car_list();

// 2. Process all car timing data
parse_all_times().await;

// 3. Generate best cars report
print_best_cars(false);

// 4. Find collection optimization opportunities
print_trash();

Track-Specific Analysis

// Analyze specific track conditions
let track = "laguna".to_string();
let condition = "Dry".to_string();
let rq = "85".to_string();

print_best_car(&track, &condition, &rq);

Data Fetching

// Download fresh data
get_cars_file().await;
get_tracks_file().await;
fetch_times().await;

Performance Characteristics

Time Complexity

  • Parsing: O(n) where n = number of cars × tracks
  • Best Car Selection: O(1) HashMap operations
  • Sorting: O(n log n) for output ordering

Memory Usage

  • Peak: ~200-300MB during full analysis
  • Baseline: ~50MB application overhead
  • Per Car: ~1-2KB metadata + timing data

Concurrency

  • Downloads: 20 concurrent connections max (semaphore bounded)
  • Processing: One task per car file
  • Thread Safety: Mutex-protected shared state

Extension Points

Adding New Commands

  1. Add command parsing in main()
  2. Implement analysis function
  3. Update help documentation

Adding New Tracks

  1. Update tracks_repo.json with new track definitions
  2. Add track name mapping to track_names.json
  3. Track names are compiled in at build time

Custom Analysis Types

  1. Create new output formatting functions
  2. Add aggregation algorithms
  3. Extend data structures as needed

Security Considerations

Network Security

  • HTTPS-only external requests
  • Certificate validation (with fallback)
  • No sensitive data transmission

File System Security

  • Read-only access to game files
  • No modification of user data
  • Safe file path handling

Input Validation

  • Command injection prevention
  • Path traversal protection
  • Input sanitization

Troubleshooting API Issues

Common Problems

"Function not found"

  • Check module imports
  • Verify function signatures
  • Ensure proper visibility

"Type mismatch"

  • Review struct field types
  • Check function signatures
  • Verify generic parameters

"Deadlock"

  • Avoid nested Mutex locks
  • Check lock ordering
  • Use timeout where possible

Debug Strategies

Logging:

println!("Processing car: {}", car.rid);
eprintln!("Error: {}", error);

Tracing:

// Add at function entry
let _span = tracing::span!(Level::INFO, "function_name");
let _enter = _span.enter();

Profiling:

cargo flamegraph --bin records -- best

Migration Guide

Version Compatibility

  • Current: 0.2.0 API stable
  • Previous: 0.1.0 fully compatible
  • Future: Semantic versioning maintained

Breaking Changes

  • Club module (src/club.rs) has been removed
  • scraper dependency removed
  • tracks.txt removed (replaced by track_names.json)
  • Magic number 115 replaced with const MAX_RQ: u8 = 113
  • translate_track_name() now loads from track_names.json instead of hardcoded match

API Version: 0.2.0 Last Updated: 2026-05-06 Compatibility: Rust 1.70.0+