scores

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

config.rs (5197B)


      1 use crate::error::{AppError, Result};
      2 use crate::league::League;
      3 use clap::{CommandFactory, Parser};
      4 use std::fs;
      5 use std::path::{Path, PathBuf};
      6 
      7 #[derive(Parser, Debug)]
      8 #[command(name = "scores")]
      9 #[command(about = "Display football scores and standings from football-data.org")]
     10 #[command(
     11     after_help = "LEAGUES:\n  epl             Premier League\n  efl             Championship\n  laliga          La Liga\n  ucl             Champions League"
     12 )]
     13 pub struct Cli {
     14     /// API token from football-data.org (or set ~/.config/scores)
     15     #[arg(long, short = 't')]
     16     pub token: Option<String>,
     17 
     18     /// Show league standings/table
     19     #[arg(long, short = 's')]
     20     pub standings: bool,
     21 
     22     /// List all matches across every competition (default)
     23     #[arg(long, short = 'a')]
     24     pub all: bool,
     25 
     26     /// Filter by team name or abbreviation
     27     #[arg(long, short = 'f')]
     28     pub filter: Option<String>,
     29 
     30     /// League to display
     31     #[arg(long, short = 'l', value_name = "LEAGUE")]
     32     pub league: Option<String>,
     33 }
     34 
     35 impl Cli {
     36     pub fn parse_cli() -> Self {
     37         Self::parse()
     38     }
     39 }
     40 
     41 pub struct Config {
     42     pub api_token: String,
     43     pub league: Option<League>,
     44     pub show_all: bool,
     45     pub show_standings: bool,
     46     pub team_filter: Option<String>,
     47 }
     48 
     49 impl Config {
     50     pub fn from_cli(cli: &Cli) -> Result<Self> {
     51         let sources = TokenSources::default();
     52         let token_from_file = sources
     53             .config_path
     54             .as_deref()
     55             .and_then(read_token_from_file)
     56             .is_some();
     57 
     58         let Some(api_token) = resolve_token(cli.token.as_deref(), &sources) else {
     59             Cli::command().print_help().unwrap();
     60             std::process::exit(0);
     61         };
     62 
     63         let league = match &cli.league {
     64             Some(arg) => {
     65                 Some(League::from_arg(arg).ok_or_else(|| AppError::InvalidLeague(arg.clone()))?)
     66             }
     67             None => None,
     68         };
     69 
     70         Ok(Self {
     71             api_token,
     72             league,
     73             show_all: cli.all || token_from_file,
     74             show_standings: cli.standings,
     75             team_filter: cli.filter.clone(),
     76         })
     77     }
     78 }
     79 
     80 pub struct TokenSources {
     81     pub config_path: Option<PathBuf>,
     82 }
     83 
     84 impl Default for TokenSources {
     85     fn default() -> Self {
     86         Self {
     87             config_path: config_file_path(),
     88         }
     89     }
     90 }
     91 
     92 fn resolve_token(cli_token: Option<&str>, sources: &TokenSources) -> Option<String> {
     93     if let Some(t) = cli_token {
     94         return Some(t.to_string());
     95     }
     96     if let Some(path) = &sources.config_path
     97         && let Some(t) = read_token_from_file(path)
     98     {
     99         return Some(t);
    100     }
    101     None
    102 }
    103 
    104 fn read_token_from_file(path: &Path) -> Option<String> {
    105     let token = fs::read_to_string(path).ok()?;
    106     let token = token.trim().to_string();
    107     if token.is_empty() { None } else { Some(token) }
    108 }
    109 
    110 fn config_file_path() -> Option<PathBuf> {
    111     let home = std::env::var("HOME").ok()?;
    112     Some(PathBuf::from(home).join(".config").join("scores"))
    113 }
    114 
    115 #[cfg(test)]
    116 mod tests {
    117     use super::*;
    118 
    119     fn no_sources() -> TokenSources {
    120         TokenSources { config_path: None }
    121     }
    122 
    123     #[test]
    124     fn test_config_from_cli_valid() {
    125         let cli = Cli {
    126             token: Some("test-token".to_string()),
    127             standings: false,
    128             all: true,
    129             filter: Some("arsenal".to_string()),
    130             league: Some("epl".to_string()),
    131         };
    132 
    133         let config = Config::from_cli(&cli).unwrap();
    134         assert_eq!(config.api_token, "test-token");
    135         assert_eq!(config.league, Some(League::PremierLeague));
    136         assert!(config.show_all);
    137         assert!(!config.show_standings);
    138         assert_eq!(config.team_filter, Some("arsenal".to_string()));
    139     }
    140 
    141     #[test]
    142     fn test_config_no_league_defaults_to_all() {
    143         let cli = Cli {
    144             token: Some("test-token".to_string()),
    145             standings: false,
    146             all: false,
    147             filter: None,
    148             league: None,
    149         };
    150 
    151         let config = Config::from_cli(&cli).unwrap();
    152         assert_eq!(config.league, None);
    153     }
    154 
    155     #[test]
    156     fn test_config_missing_token() {
    157         assert_eq!(resolve_token(None, &no_sources()), None);
    158     }
    159 
    160     #[test]
    161     fn test_config_invalid_league() {
    162         let cli = Cli {
    163             token: Some("test-token".to_string()),
    164             standings: false,
    165             all: false,
    166             filter: None,
    167             league: Some("bundesliga".to_string()),
    168         };
    169 
    170         let result = Config::from_cli(&cli);
    171         assert!(matches!(result, Err(AppError::InvalidLeague(_))));
    172     }
    173 
    174     #[test]
    175     fn test_config_reads_token_from_file() {
    176         let dir = std::env::temp_dir().join("scores-test-home-12345");
    177         let config_dir = dir.join(".config");
    178         fs::create_dir_all(&config_dir).unwrap();
    179         let path = config_dir.join("scores");
    180         fs::write(&path, "file-token-abc").unwrap();
    181 
    182         let sources = TokenSources {
    183             config_path: Some(path.clone()),
    184         };
    185         let token = resolve_token(None, &sources);
    186         assert_eq!(token.as_deref(), Some("file-token-abc"));
    187 
    188         fs::remove_dir_all(&dir).ok();
    189     }
    190 }