Statbook
(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
- Season parameter support for historical and current data queries
- Flexible configuration with builder pattern and environment variables
- 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
Season Support
Query player statistics for different seasons and time periods:
use statbook::{StatbookClient, Season, api::players::get_player_stats};
let client = StatbookClient::from_env()?;
let current = get_player_stats(&client, "josh-allen", None, &Season::Regular).await?;
let playoffs = get_player_stats(&client, "josh-allen", None, &Season::Playoffs).await?;
let season_2023 = get_player_stats(&client, "josh-allen", Some((2023, 2024)), &Season::Regular).await?;
let latest = get_player_stats(&client, "josh-allen", None, &Season::Latest).await?;
println!("Season: {}", current.season);
Available Season Types:
Season::Regular - Regular season games
Season::Playoffs - Playoff games only
Season::Current - Current active season
Season::Latest - Most recent available data
Season::Upcoming - Upcoming season data
Year Range Format:
None - Uses default season
Some((2023, 2024)) - Specific season range, formatted as "2023-2024-regular"
Setup
1. Get API Credentials
- Sign up for a free account at MySportsFeeds.com
to get your stats API key
- Sign up for a free account at NewsAPI.org
to get your news API key
2. Add to Cargo.toml
[dependencies]
statbook = "0.0.3"
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, Season, api::players::get_player_summary};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = StatbookClient::from_env()?;
let result = get_player_summary(&client, "josh-allen", None, &Season::Regular).await?;
println!("{} {} - {} #{}",
result.first_name,
result.last_name,
result.primary_position,
result.jersey_number
);
println!("Team: {} | Games: {}",
result.current_team,
result.games_played
);
if result.news.is_empty() {
println!("No news available");
} else {
println!("Recent News ({} articles):", result.news.len());
for article in result.news.iter().take(2) {
println!(" • {}", article.title);
}
}
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};
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, NewsQuery, Season,
api::players::{get_player_stats, get_player_news, get_player_summary}
};
let stats = get_player_stats(&client, "josh-allen", None, &Season::Regular).await?;
println!("{} plays {} for {} (Season: {})",
stats.first_name, stats.primary_position, stats.current_team, stats.season);
let playoff_stats = get_player_stats(&client, "josh-allen", Some((2023, 2024)), &Season::Playoffs).await?;
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?;
let summary = get_player_summary(&client, "josh-allen", None, &Season::Regular).await?;
Data Types
use statbook::{PlayerSummary, PlayerStats, Article, NewsQuery, Season};
Function Overview
The library provides three main functions for different use cases:
use statbook::{Season, api::players::{get_player_stats, get_player_news, get_player_summary}};
let stats = get_player_stats(&client, "josh-allen", None, &Season::Regular).await?;
let news = get_player_news(&client, &NewsQuery::for_player("josh-allen")).await?;
let summary = get_player_summary(&client, "josh-allen", None, &Season::Regular).await?;
Error Handling
The library provides comprehensive error types with detailed context:
use statbook::{StatbookClient, StatbookError, Season, api::players::get_player_stats};
match get_player_stats(&client, "unknown-player", None, &Season::Regular).await {
Ok(stats) => {
println!("Found: {} {}", stats.first_name, stats.last_name);
}
Err(StatbookError::PlayerNotFound { name }) => {
println!("No player named '{}'", name);
}
Err(StatbookError::Network(e)) => {
println!("Network error: {}", e);
}
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),
}
Graceful Failure Handling
use statbook::{Season, api::players::get_player_summary};
let summary = get_player_summary(&client, "josh-allen", None, &Season::Regular).await?;
println!("Player: {} {}", summary.first_name, summary.last_name);
if summary.news.is_empty() {
println!("No news available (may have failed gracefully)");
} else {
println!("Found {} news articles", summary.news.len());
}
Testing
The library provides comprehensive testing utilities
for both unit and integration testing:
Unit Testing with Mock Providers
use statbook::{create_mock_client, Season, api::players::get_player_stats};
#[tokio::test]
async fn test_player_stats() {
let client = create_mock_client();
let stats = get_player_stats(&client, "josh-allen", None, &Season::Regular).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");
assert_eq!(stats.season, "regular");
}
#[tokio::test]
async fn test_player_functions() {
let client = create_mock_client();
let stats = get_player_stats(&client, "josh-allen", None, &Season::Regular).await.unwrap();
let summary = get_player_summary(&client, "josh-allen", None, &Season::Regular).await.unwrap();
assert_eq!(stats.first_name, summary.first_name);
assert!(!summary.news.is_empty());
}
#[tokio::test]
async fn test_season_parameters() {
let client = create_mock_client();
let regular = get_player_stats(&client, "josh-allen", None, &Season::Regular).await.unwrap();
let playoffs = get_player_stats(&client, "josh-allen", None, &Season::Playoffs).await.unwrap();
assert_eq!(regular.season, "regular");
assert_eq!(playoffs.season, "playoffs");
}
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(),
primary_position: "QB".to_string(),
jersey_number: 1,
current_team: "CUSTOM".to_string(),
injury: String::new(),
rookie: false,
games_played: 16,
season: "2024-regular".to_string(),
});
let client = create_custom_mock_client(mock_stats, MockNewsProvider::new());
}
Integration Testing
use statbook::{skip_if_no_credentials, api::players::get_player_stats};
#[tokio::test]
async fn test_real_api() {
let client = match skip_if_no_credentials() {
Some(client) => client,
None => {
println!("Skipping integration test - no API credentials");
return;
}
};
let stats = get_player_stats(&client, "josh-allen", None, &Season::Regular).await.unwrap();
assert!(!stats.first_name.is_empty());
assert_eq!(stats.first_name, "Josh");
}
Running Tests
cargo test
STATS_API_KEY="your-key" NEWS_API_KEY="your-key" INTEGRATION_TESTS=1 cargo test
cargo test test_player_stats
Advanced Usage
Custom Providers
Implement your own data sources:
use statbook::{StatsProvider, NewsProvider, PlayerStats, PlayerNews, 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, season: &str) -> Result<PlayerStats> {
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,
season: season.to_string(),
})
}
}
struct MyCustomNewsProvider;
#[async_trait]
impl NewsProvider for MyCustomNewsProvider {
async fn fetch_player_news(&self, query: &NewsQuery) -> Result<PlayerNews> {
let articles = 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(),
}];
Ok(PlayerNews::new(articles, query.clone()))
}
}
let client = StatbookClient::with_providers(
Arc::new(MyCustomStatsProvider),
Arc::new(MyCustomNewsProvider),
);
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.