statbook 0.0.2

A Rust library for accessing sports statistics and data (early development)
Documentation

Statbook

CI Crates.io License: MIT OR Apache-2.0 Rust Version

(Early in development)

A high-performance Rust library for accessing sports statistics and news data with concurrent API calls, comprehensive error handling, and flexible configuration options.

Perfect for fantasy sports apps, sports analytics, news aggregation, and data-driven sports applications.

Currently supports NFL player data via MySportsFeeds.com and news data via NewsAPI.org with plans to expand to other sports and data sources.

Features

  • Flexible configuration with builder pattern and environment variables
  • Multiple fetch strategies (stats-only, news-only, or both)
  • Comprehensive error handling with detailed error types
  • Built-in testing utilities with mock providers
  • Extensible architecture with trait-based providers
  • Concurrent API calls for improved performance
  • Clean API with intuitive imports and type safety

Setup

1. Get API Credentials

  1. Sign up for a free account at MySportsFeeds.com to get your stats API key
  2. Sign up for a free account at NewsAPI.org to get your news API key

2. Add to Cargo.toml

[dependencies]
statbook = "0.1.0"
tokio = { version = "1.0", features = ["full"] }

3. Set Environment Variables

export STATS_API_KEY="your-mysportsfeeds-api-key"
export NEWS_API_KEY="your-newsapi-key"

Quick Start

use statbook::{StatbookClient, FetchStrategy, api::players::get_player_summary};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create client from environment variables
    let client = StatbookClient::from_env()?;

    // Fetch player data with concurrent API calls
    let result = get_player_summary(&client, "josh-allen", 
        FetchStrategy::Both { fail_on_news_error: false }).await?;

    let stats = &result.player_stats;
    println!("{} {} - {} #{}", 
        stats.first_name, 
        stats.last_name, 
        stats.primary_position,
        stats.jersey_number
    );
    println!("Team: {} | Games: {}", stats.current_team, stats.games_played);

    // Handle news with graceful error handling
    match result.news_result {
        Ok(articles) => {
            println!("Recent News ({} articles):", articles.len());
            for article in articles.iter().take(2) {
                println!("{}", article.title);
            }
        }
        Err(e) => println!("News unavailable: {}", e),
    }

    Ok(())
}

Configuration Options

Environment Variables (Recommended)

use statbook::StatbookClient;

let client = StatbookClient::from_env()?;

Custom Configuration with Builder Pattern

use statbook::{StatbookClient, StatbookConfig, NewsConfig, SortBy};

// Custom news settings
let news_config = NewsConfig::new()
    .with_max_articles(15)
    .with_days_back(30)
    .with_sort_by(SortBy::Relevancy);

let config = StatbookConfig::builder()
    .stats_api_key("your-mysportsfeeds-api-key")
    .news_api_key("your-newsapi-key")
    .news_config(news_config)
    .build()?;

let client = StatbookClient::new(config);

Direct Configuration

use statbook::{StatbookClient, StatbookConfig};

let config = StatbookConfig::new(
    "your-mysportsfeeds-api-key".to_string(), 
    "your-newsapi-key".to_string()
);
let client = StatbookClient::new(config);

API Reference

Core Functions

use statbook::{
    StatbookClient, FetchStrategy, NewsQuery,
    api::players::{get_player_stats, get_player_news, get_player_summary}
};

// Get only player statistics (fastest - single API call)
let stats = get_player_stats(&client, "josh-allen").await?;
println!("{} plays {} for {}", stats.first_name, stats.primary_position, stats.current_team);

// Get only news articles with custom query
let query = NewsQuery::for_player("josh-allen")
    .with_page_size(10)
    .with_date_range("2024-01-01".to_string());
let news = get_player_news(&client, &query).await?;

// Get both with concurrent fetching (recommended)
let result = get_player_summary(&client, "josh-allen", 
    FetchStrategy::Both { fail_on_news_error: false }).await?;

Data Types

use statbook::{PlayerSummary, PlayerStats, Article, NewsQuery, FetchStrategy};

// PlayerSummary - Complete player information
// PlayerStats - Just the statistics
// Article - News article with title, description, content, published_at
// NewsQuery - Configurable news search parameters
// FetchStrategy - Control how data is fetched (StatsOnly, NewsOnly, Both)

Fetch Strategies

Choose the right strategy for your use case:

use statbook::{FetchStrategy, api::players::get_player_summary};

// Stats only - fastest option (single API call)
let result = get_player_summary(&client, "josh-allen", FetchStrategy::StatsOnly).await?;
// Use case: Live scoreboards, quick player lookups

// News only - for news aggregation
let result = get_player_summary(&client, "josh-allen", FetchStrategy::NewsOnly).await?;
// Use case: Sports news apps, content aggregation

// Both with graceful degradation (recommended)
let result = get_player_summary(&client, "josh-allen", 
    FetchStrategy::Both { fail_on_news_error: false }).await?;
// Use case: Fantasy apps, comprehensive player profiles

// Both with strict error handling
let result = get_player_summary(&client, "josh-allen", 
    FetchStrategy::Both { fail_on_news_error: true }).await?;
// Use case: Critical applications requiring complete data

Performance Comparison:

  • StatsOnly: ~200ms (1 API call)
  • NewsOnly: ~300ms (1 API call)
  • Both: ~400ms (2 concurrent API calls)

Error Handling

The library provides comprehensive error types with detailed context:

use statbook::{StatbookClient, StatbookError, api::players::get_player_stats};

match get_player_stats(&client, "unknown-player").await {
    Ok(stats) => {
        println!("Found: {} {}", stats.first_name, stats.last_name);
    }
    Err(StatbookError::PlayerNotFound { name }) => {
        println!("No player named '{}'", name);
        // Suggest similar names, check spelling, etc.
    }
    Err(StatbookError::Network(e)) => {
        println!("Network error: {}", e);
        // Retry logic, check connectivity
    }
    Err(StatbookError::StatsApi { status, message }) => {
        match status {
            401 => println!("Invalid API key: {}", message),
            429 => println!("Rate limited: {}", message),
            _ => println!("Stats API error {}: {}", status, message),
        }
    }
    Err(StatbookError::NewsApi { status, message }) => {
        println!("News API error {}: {}", status, message);
    }
    Err(StatbookError::MissingApiKey { key }) => {
        println!("Missing API key: {}. Set environment variable.", key);
    }
    Err(StatbookError::Config(msg)) => {
        println!("Configuration error: {}", msg);
    }
    Err(StatbookError::Validation(msg)) => {
        println!("Validation error: {}", msg);
    }
    Err(e) => println!("Unexpected error: {}", e),
}

Partial Failure Handling

use statbook::{FetchStrategy, api::players::get_player_summary};

let result = get_player_summary(&client, "josh-allen", 
    FetchStrategy::Both { fail_on_news_error: false }).await?;

println!("Player: {} {}", result.player_stats.first_name, result.player_stats.last_name);

match result.news_result {
    Ok(articles) => println!("Found {} news articles", articles.len()),
    Err(e) => println!("News failed but stats succeeded: {}", e),
}

Testing

The library provides comprehensive testing utilities for both unit and integration testing:

Unit Testing with Mock Providers

use statbook::{create_mock_client, api::players::get_player_stats};

#[tokio::test]
async fn test_player_stats() {
    // Mock client - no real API calls, instant responses
    let client = create_mock_client();
    let stats = get_player_stats(&client, "josh-allen").await.unwrap();

    assert_eq!(stats.first_name, "Josh");
    assert_eq!(stats.last_name, "Allen");
    assert_eq!(stats.primary_position, "QB");
    assert_eq!(stats.current_team, "BUF");
}

#[tokio::test]
async fn test_fetch_strategies() {
    let client = create_mock_client();

    // Test different strategies
    let stats_only = get_player_summary(&client, "josh-allen", FetchStrategy::StatsOnly).await.unwrap();
    let both = get_player_summary(&client, "josh-allen", 
        FetchStrategy::Both { fail_on_news_error: false }).await.unwrap();

    assert_eq!(stats_only.player_stats.first_name, both.player_stats.first_name);
    assert!(both.news_result.is_ok());
}

Custom Mock Data

use statbook::{create_custom_mock_client, MockStatsProvider, MockNewsProvider, PlayerStats};

#[tokio::test]
async fn test_custom_data() {
    let mut mock_stats = MockStatsProvider::new();
    mock_stats.add_player_stats("custom-player", PlayerStats {
        first_name: "Custom".to_string(),
        last_name: "Player".to_string(),
        // ... other fields
    });

    let client = create_custom_mock_client(mock_stats, MockNewsProvider::new());
    // Test with your custom data
}

Integration Testing

use statbook::{skip_if_no_credentials, api::players::get_player_stats};

#[tokio::test]
async fn test_real_api() {
    // Skip test if API credentials not available
    let client = match skip_if_no_credentials() {
        Some(client) => client,
        None => {
            println!("Skipping integration test - no API credentials");
            return;
        }
    };
    
    // Test with real API calls
    let stats = get_player_stats(&client, "josh-allen").await.unwrap();
    assert!(!stats.first_name.is_empty());
    assert_eq!(stats.first_name, "Josh");
}

Running Tests

# Unit tests only (fast, no API calls)
cargo test

# Integration tests (requires API keys)
STATS_API_KEY="your-key" NEWS_API_KEY="your-key" INTEGRATION_TESTS=1 cargo test

# Run specific test
cargo test test_player_stats

Advanced Usage

Custom Providers

Implement your own data sources:

use statbook::{StatsProvider, NewsProvider, PlayerStats, Article, NewsQuery, Result, StatbookClient};
use async_trait::async_trait;
use std::sync::Arc;

struct MyCustomStatsProvider;

#[async_trait]
impl StatsProvider for MyCustomStatsProvider {
    async fn fetch_player_stats(&self, name: &str) -> Result<PlayerStats> {
        // Your custom implementation - fetch from your own API, database, etc.
        Ok(PlayerStats {
            first_name: "Custom".to_string(),
            last_name: "Player".to_string(),
            primary_position: "QB".to_string(),
            jersey_number: 1,
            current_team: "CUSTOM".to_string(),
            injury: String::new(),
            rookie: false,
            games_played: 16,
        })
    }
}

struct MyCustomNewsProvider;

#[async_trait]
impl NewsProvider for MyCustomNewsProvider {
    async fn fetch_player_news(&self, query: &NewsQuery) -> Result<Vec<Article>> {
        // Your custom news implementation
        Ok(vec![Article {
            title: format!("Custom news about {}", query.player_name),
            description: "Custom news description".to_string(),
            published_at: "2024-01-01T00:00:00Z".to_string(),
            content: "Custom news content".to_string(),
        }])
    }
}

// Use your custom providers
let client = StatbookClient::with_providers(
    Arc::new(MyCustomStatsProvider),
    Arc::new(MyCustomNewsProvider),
);

// Or mix custom with mock providers for testing
let client = StatbookClient::with_providers(
    Arc::new(MyCustomStatsProvider),
    Arc::new(statbook::MockNewsProvider::new()),
);

Future Plans

  • Caching layer for improved performance and reduced API calls
  • Enhanced NFL data (team statistics, game data, season analytics)
  • Advanced news filtering (sentiment analysis, relevance scoring)
  • Additional sports (NHL, NBA, MLB, etc.)
  • More data providers (ESPN, The Athletic, etc.)
  • Real-time updates via WebSocket connections
  • Data export (JSON, CSV, database integration)

License

Licensed under either of

  • Apache License, Version 2.0
  • MIT license

at your option.