commit 49a2fdca1c53263116ccc4fab70eac448586a09c from: mtmn date: Fri Jul 10 16:46:36 2026 UTC config: read token from a file commit - 610cc027f905579f3cb2e7cbef265ac92a51d97c commit + 49a2fdca1c53263116ccc4fab70eac448586a09c blob - d4e0030c044a2c2b4d5e65ad1efa39438722f883 blob + ea2863fba3c25048f0ea4af1b645dc86ec74ce4c --- src/config.rs +++ src/config.rs @@ -1,6 +1,8 @@ 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")] @@ -9,9 +11,9 @@ use clap::{CommandFactory, Parser}; 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, /// Show league standings/table #[arg(long, short = 's')] @@ -32,18 +34,6 @@ pub struct Cli { impl Cli { pub fn parse_cli() -> Self { - let args: Vec = 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() } } @@ -58,10 +48,18 @@ pub struct Config { impl Config { pub fn from_cli(cli: &Cli) -> Result { - 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()))?) @@ -70,23 +68,62 @@ impl Config { }; 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, +} + +impl Default for TokenSources { + fn default() -> Self { + Self { + config_path: config_file_path(), + } + } +} + +fn resolve_token(cli_token: Option<&str>, sources: &TokenSources) -> Option { + 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 { + 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 { + 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()), @@ -104,7 +141,7 @@ mod tests { #[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, @@ -117,22 +154,13 @@ mod tests { #[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, @@ -142,4 +170,21 @@ mod tests { 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 @@ -8,9 +8,6 @@ pub enum AppError { #[error("API returned error status: {0}")] ApiStatus(u16), - #[error("Invalid API token: token is missing or empty")] - InvalidToken, - #[error("Invalid league: {0}")] InvalidLeague(String),