commit - 610cc027f905579f3cb2e7cbef265ac92a51d97c
commit + 49a2fdca1c53263116ccc4fab70eac448586a09c
blob - d4e0030c044a2c2b4d5e65ad1efa39438722f883
blob + ea2863fba3c25048f0ea4af1b645dc86ec74ce4c
--- src/config.rs
+++ src/config.rs
use crate::error::{AppError, Result};
use crate::league::League;
use clap::{CommandFactory, Parser};
+use std::fs;
+use std::path::{Path, PathBuf};
#[derive(Parser, Debug)]
#[command(name = "scores")]
after_help = "LEAGUES:\n epl Premier League\n efl Championship\n laliga La Liga\n ucl Champions League"
)]
pub struct Cli {
- /// API token from football-data.org (required)
+ /// API token from football-data.org (or set ~/.config/scores)
#[arg(long, short = 't')]
- pub token: String,
+ pub token: Option<String>,
/// Show league standings/table
#[arg(long, short = 's')]
impl Cli {
pub fn parse_cli() -> Self {
- let args: Vec<String> = std::env::args().collect();
- if args.len() == 1 {
- Self::command().print_help().unwrap();
- println!();
- println!("LEAGUES:");
- println!(" epl Premier League");
- println!(" efl Championship");
- println!(" laliga La Liga");
- println!(" ucl Champions League");
- println!();
- std::process::exit(0);
- }
Self::parse()
}
}
impl Config {
pub fn from_cli(cli: &Cli) -> Result<Self> {
- if cli.token.is_empty() {
- return Err(AppError::InvalidToken);
- }
+ let sources = TokenSources::default();
+ let token_from_file = sources
+ .config_path
+ .as_deref()
+ .and_then(read_token_from_file)
+ .is_some();
+ let Some(api_token) = resolve_token(cli.token.as_deref(), &sources) else {
+ Cli::command().print_help().unwrap();
+ std::process::exit(0);
+ };
+
let league = match &cli.league {
Some(arg) => {
Some(League::from_arg(arg).ok_or_else(|| AppError::InvalidLeague(arg.clone()))?)
};
Ok(Self {
- api_token: cli.token.clone(),
+ api_token,
league,
- show_all: cli.all,
+ show_all: cli.all || token_from_file,
show_standings: cli.standings,
team_filter: cli.filter.clone(),
})
}
}
+pub struct TokenSources {
+ pub config_path: Option<PathBuf>,
+}
+
+impl Default for TokenSources {
+ fn default() -> Self {
+ Self {
+ config_path: config_file_path(),
+ }
+ }
+}
+
+fn resolve_token(cli_token: Option<&str>, sources: &TokenSources) -> Option<String> {
+ if let Some(t) = cli_token {
+ return Some(t.to_string());
+ }
+ if let Some(path) = &sources.config_path
+ && let Some(t) = read_token_from_file(path)
+ {
+ return Some(t);
+ }
+ None
+}
+
+fn read_token_from_file(path: &Path) -> Option<String> {
+ let token = fs::read_to_string(path).ok()?;
+ let token = token.trim().to_string();
+ if token.is_empty() { None } else { Some(token) }
+}
+
+fn config_file_path() -> Option<PathBuf> {
+ let home = std::env::var("HOME").ok()?;
+ Some(PathBuf::from(home).join(".config").join("scores"))
+}
+
#[cfg(test)]
mod tests {
use super::*;
+ fn no_sources() -> TokenSources {
+ TokenSources { config_path: None }
+ }
+
#[test]
fn test_config_from_cli_valid() {
let cli = Cli {
- token: "test-token".to_string(),
+ token: Some("test-token".to_string()),
standings: false,
all: true,
filter: Some("arsenal".to_string()),
#[test]
fn test_config_no_league_defaults_to_all() {
let cli = Cli {
- token: "test-token".to_string(),
+ token: Some("test-token".to_string()),
standings: false,
all: false,
filter: None,
#[test]
fn test_config_missing_token() {
- let cli = Cli {
- token: String::new(),
- standings: false,
- all: false,
- filter: None,
- league: Some("epl".to_string()),
- };
-
- let result = Config::from_cli(&cli);
- assert!(matches!(result, Err(AppError::InvalidToken)));
+ assert_eq!(resolve_token(None, &no_sources()), None);
}
#[test]
fn test_config_invalid_league() {
let cli = Cli {
- token: "test-token".to_string(),
+ token: Some("test-token".to_string()),
standings: false,
all: false,
filter: None,
let result = Config::from_cli(&cli);
assert!(matches!(result, Err(AppError::InvalidLeague(_))));
}
+
+ #[test]
+ fn test_config_reads_token_from_file() {
+ let dir = std::env::temp_dir().join("scores-test-home-12345");
+ let config_dir = dir.join(".config");
+ fs::create_dir_all(&config_dir).unwrap();
+ let path = config_dir.join("scores");
+ fs::write(&path, "file-token-abc").unwrap();
+
+ let sources = TokenSources {
+ config_path: Some(path.clone()),
+ };
+ let token = resolve_token(None, &sources);
+ assert_eq!(token.as_deref(), Some("file-token-abc"));
+
+ fs::remove_dir_all(&dir).ok();
+ }
}
blob - 6e1ada732b4f8bd88289ecf0f205eff27e29e953
blob + b754e72e406d46f7ed452ad89319170c88422213
--- src/error.rs
+++ src/error.rs
#[error("API returned error status: {0}")]
ApiStatus(u16),
- #[error("Invalid API token: token is missing or empty")]
- InvalidToken,
-
#[error("Invalid league: {0}")]
InvalidLeague(String),