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
- 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
- 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.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>> {
let client = StatbookClient::from_env()?;
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);
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};
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}
};
let stats = get_player_stats(&client, "josh-allen").await?;
println!("{} plays {} for {}", stats.first_name, stats.primary_position, stats.current_team);
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 result = get_player_summary(&client, "josh-allen",
FetchStrategy::Both { fail_on_news_error: false }).await?;
Data Types
use statbook::{PlayerSummary, PlayerStats, Article, NewsQuery, FetchStrategy};
Fetch Strategies
Choose the right strategy for your use case:
use statbook::{FetchStrategy, api::players::get_player_summary};
let result = get_player_summary(&client, "josh-allen", FetchStrategy::StatsOnly).await?;
let result = get_player_summary(&client, "josh-allen", FetchStrategy::NewsOnly).await?;
let result = get_player_summary(&client, "josh-allen",
FetchStrategy::Both { fail_on_news_error: false }).await?;
let result = get_player_summary(&client, "josh-allen",
FetchStrategy::Both { fail_on_news_error: true }).await?;
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);
}
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),
}
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() {
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();
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(),
});
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").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, 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> {
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>> {
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(),
}])
}
}
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.