scores

football-data.org terminal interface
Log | Files | Refs | README | LICENSE

commit 49a2fdca1c53263116ccc4fab70eac448586a09c
parent 610cc027f905579f3cb2e7cbef265ac92a51d97c
Author: mtmn <miro@haravara.org>
Date:   Fri, 10 Jul 2026 18:32:07 +0200

config: read token from a file

Diffstat:
Msrc/config.rs | 109++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/error.rs | 3---
2 files changed, 77 insertions(+), 35 deletions(-)

diff --git a/src/config.rs b/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<String>, /// 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<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() } } @@ -58,9 +48,17 @@ pub struct Config { 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) => { @@ -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<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()), @@ -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(); + } } diff --git a/src/error.rs b/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),