1use time::Date;
9
10use crate::backends::{ApiFootballProvider, EspnProvider, FootballDataProvider};
11use crate::domain::{Bracket, Calendar, Group, Match, MatchDetail};
12use crate::error::{DataError, Result};
13use crate::transport::Http;
14
15#[allow(async_fn_in_trait)]
21pub trait ScoreProvider {
22 fn name(&self) -> &'static str;
24
25 async fn calendar(&self) -> Result<Calendar>;
27
28 async fn scoreboard(&self, day: Option<Date>) -> Result<Vec<Match>>;
32
33 async fn standings(&self) -> Result<Vec<Group>>;
35
36 async fn bracket(&self) -> Result<Bracket>;
38
39 async fn match_detail(&self, id: &str) -> Result<MatchDetail>;
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
45pub enum ProviderKind {
46 #[default]
48 Espn,
49 ApiFootball,
51 FootballData,
53}
54
55impl ProviderKind {
56 #[must_use]
58 pub fn all() -> [ProviderKind; 3] {
59 [
60 ProviderKind::Espn,
61 ProviderKind::ApiFootball,
62 ProviderKind::FootballData,
63 ]
64 }
65
66 #[must_use]
68 pub fn as_str(self) -> &'static str {
69 match self {
70 ProviderKind::Espn => "espn",
71 ProviderKind::ApiFootball => "api-football",
72 ProviderKind::FootballData => "football-data",
73 }
74 }
75
76 #[must_use]
78 pub fn parse(s: &str) -> Option<ProviderKind> {
79 match s.trim().to_ascii_lowercase().as_str() {
80 "espn" => Some(ProviderKind::Espn),
81 "api-football" | "apifootball" | "api_football" => Some(ProviderKind::ApiFootball),
82 "football-data" | "footballdata" | "football_data" => Some(ProviderKind::FootballData),
83 _ => None,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Default)]
90pub struct ProviderConfig {
91 pub kind: ProviderKind,
93 pub api_football_key: Option<String>,
95 pub football_data_key: Option<String>,
97}
98
99pub enum Provider {
101 Espn(EspnProvider),
103 ApiFootball(ApiFootballProvider),
105 FootballData(FootballDataProvider),
107}
108
109impl Provider {
110 pub fn from_config(config: &ProviderConfig, http: Http) -> Result<Self> {
117 match config.kind {
118 ProviderKind::Espn => Ok(Provider::Espn(EspnProvider::new(http))),
119 ProviderKind::ApiFootball => {
120 let key = config
121 .api_football_key
122 .clone()
123 .filter(|k| !k.trim().is_empty())
124 .ok_or(DataError::MissingKey("API-Football"))?;
125 Ok(Provider::ApiFootball(ApiFootballProvider::new(http, key)))
126 }
127 ProviderKind::FootballData => {
128 let key = config
129 .football_data_key
130 .clone()
131 .filter(|k| !k.trim().is_empty())
132 .ok_or(DataError::MissingKey("football-data.org"))?;
133 Ok(Provider::FootballData(FootballDataProvider::new(http, key)))
134 }
135 }
136 }
137
138 #[must_use]
140 pub fn name(&self) -> &'static str {
141 match self {
142 Provider::Espn(p) => p.name(),
143 Provider::ApiFootball(p) => p.name(),
144 Provider::FootballData(p) => p.name(),
145 }
146 }
147
148 pub async fn calendar(&self) -> Result<Calendar> {
153 match self {
154 Provider::Espn(p) => p.calendar().await,
155 Provider::ApiFootball(p) => p.calendar().await,
156 Provider::FootballData(p) => p.calendar().await,
157 }
158 }
159
160 pub async fn scoreboard(&self, day: Option<Date>) -> Result<Vec<Match>> {
165 match self {
166 Provider::Espn(p) => p.scoreboard(day).await,
167 Provider::ApiFootball(p) => p.scoreboard(day).await,
168 Provider::FootballData(p) => p.scoreboard(day).await,
169 }
170 }
171
172 pub async fn standings(&self) -> Result<Vec<Group>> {
177 match self {
178 Provider::Espn(p) => p.standings().await,
179 Provider::ApiFootball(p) => p.standings().await,
180 Provider::FootballData(p) => p.standings().await,
181 }
182 }
183
184 pub async fn bracket(&self) -> Result<Bracket> {
189 match self {
190 Provider::Espn(p) => p.bracket().await,
191 Provider::ApiFootball(p) => p.bracket().await,
192 Provider::FootballData(p) => p.bracket().await,
193 }
194 }
195
196 pub async fn match_detail(&self, id: &str) -> Result<MatchDetail> {
201 match self {
202 Provider::Espn(p) => p.match_detail(id).await,
203 Provider::ApiFootball(p) => p.match_detail(id).await,
204 Provider::FootballData(p) => p.match_detail(id).await,
205 }
206 }
207}