1use crate::RwLock;
33use crate::meta::MetaRequest;
34use crate::request::{RequestURL, RequestURLBuilderExt};
35use fxhash::FxBuildHasher;
36use serde::de::DeserializeOwned;
37use std::collections::HashMap;
38use std::fmt::{Debug, Display};
39use std::hash::Hash;
40use std::sync::Arc;
41use thiserror::Error;
42use crate::person::Person;
43use crate::person::players::PlayersRequest;
44use crate::sport::SportId;
45
46pub trait Requestable: 'static + Send + Sync + DeserializeOwned + Debug + Clone + PartialEq {
52 type Identifier: Clone + Eq + Hash + Display + Sync + Debug;
53 type URL: RequestURL;
54
55 fn id(&self) -> &Self::Identifier;
56
57 fn url_for_id(id: &Self::Identifier) -> Self::URL;
58
59 fn get_entries(response: <Self::URL as RequestURL>::Response) -> impl IntoIterator<Item = Self> where Self: Sized;
60
61 #[cfg(feature = "cache")]
62 fn get_cache_table() -> &'static RwLock<CacheTable<Self>> where Self: Sized;
63}
64
65pub trait RequestableEntrypoint {
67 type Complete: Requestable;
68
69 fn id(&self) -> &<<Self as RequestableEntrypoint>::Complete as Requestable>::Identifier;
70
71 #[cfg(feature = "cache")]
72 fn as_complete_or_request(&self) -> impl Future<Output = Result<Arc<<Self as RequestableEntrypoint>::Complete>, Error<Self>>>
73 where
74 Self: Sized,
75 { async {
76 let cache_lock = <<Self as RequestableEntrypoint>::Complete as Requestable>::get_cache_table();
77 let id = self.id();
78 let cache = cache_lock.read().await;
79 if let Some(complete_entry) = cache.get(id).cloned() {
80 return Ok(complete_entry);
81 }
82 drop(cache);
83
84 let mut cache = cache_lock.write().await;
85 cache.request_and_add(id).await?;
86 cache.get(id).cloned().ok_or_else(|| Error::NoMatchingVariant(id.clone()))
87 } }
88
89 #[cfg(not(feature = "cache"))]
90 fn as_complete_or_request(&self) -> impl Future<Output = Result<<Self as RequestableEntrypoint>::Complete, Error<Self>>>
91 where
92 Self: Sized,
93 { async {
94 let id = self.id();
95 let url = <Self::Complete as Requestable>::url_for_id(id).to_string();
96 let response: <<Self::Complete as Requestable>::URL as RequestURL>::Response = crate::request::get::<<<<Self as RequestableEntrypoint>::Complete as Requestable>::URL as RequestURL>::Response>(url).await?;
97 let entries = <Self::Complete as Requestable>::get_entries(response);
98 entries.into_iter().next().ok_or_else(|| Error::<Self>::NoMatchingVariant(id.clone()))
99 } }
100}
101
102#[cfg(feature = "cache")]
106pub struct CacheTable<T: Requestable> {
107 cached_values: HashMap<T::Identifier, Arc<T>, FxBuildHasher>,
108}
109
110#[derive(Debug, Error)]
112pub enum Error<T: RequestableEntrypoint> {
113 #[error(transparent)]
114 Url(#[from] crate::request::Error),
115 #[error("No matching entry was found for id {0}")]
116 NoMatchingVariant(<T::Complete as Requestable>::Identifier),
117}
118
119#[cfg(feature = "cache")]
120impl<T: Requestable> CacheTable<T> {
121 #[allow(clippy::new_without_default, reason = "needs to be const")]
122 #[must_use]
123 pub const fn new() -> Self {
124 Self {
125 cached_values: HashMap::with_hasher(FxBuildHasher::new()),
126 }
127 }
128
129 #[must_use]
130 pub fn get(&self, id: &T::Identifier) -> Option<&Arc<T>> {
131 self.cached_values.get(id)
132 }
133
134 pub fn insert(&mut self, value: T) {
135 self.cached_values.insert(value.id().clone(), Arc::new(value));
136 }
137
138 pub fn clear(&mut self) {
139 self.cached_values.clear();
140 }
141
142 pub fn add_entries(&mut self, entries: impl IntoIterator<Item = T>) {
143 for entry in entries {
144 self.insert(entry);
145 }
146 }
147
148 pub async fn request_and_add(&mut self, id: &T::Identifier) -> Result<(), crate::request::Error> {
151 let url = <T as Requestable>::url_for_id(id).to_string();
152 let response = crate::request::get::<<<T as Requestable>::URL as RequestURL>::Response>(url).await?;
153 self.add_entries(<T as Requestable>::get_entries(response));
154 Ok(())
155 }
156}
157
158#[cfg(feature = "cache")]
163#[allow(clippy::too_many_lines, reason = "low cognitive complexity")]
164pub async fn precache() -> Result<(), crate::request::Error> {
165 let people_response = PlayersRequest::for_sport(SportId::MLB).build_and_get();
166
167 let award_response = crate::awards::AwardRequest::builder().build_and_get();
168 let division_response = crate::division::DivisionsRequest::builder().build_and_get();
169 let conference_response = crate::conference::ConferencesRequest::builder().build_and_get();
170 let venue_response = crate::venue::VenuesRequest::builder().build_and_get();
171 let league_response = crate::league::LeaguesRequest::builder().build_and_get();
172 let sport_response = crate::sport::SportsRequest::builder().build_and_get();
173 <crate::awards::Award as Requestable>::get_cache_table().write().await.add_entries(award_response.await?.awards);
174 <crate::division::Division as Requestable>::get_cache_table().write().await.add_entries(division_response.await?.divisions);
175 <crate::conference::Conference as Requestable>::get_cache_table().write().await.add_entries(conference_response.await?.conferences);
176 <crate::venue::Venue as Requestable>::get_cache_table().write().await.add_entries(venue_response.await?.venues);
177 <crate::league::League as Requestable>::get_cache_table().write().await.add_entries(league_response.await?.leagues);
178 <crate::sport::Sport as Requestable>::get_cache_table().write().await.add_entries(sport_response.await?.sports);
179
180 <crate::meta::BaseballStat as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::BaseballStat>::new().get().await?.entries);
181 <crate::meta::JobType as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::JobType>::new().get().await?.entries);
182 <crate::meta::GameStatus as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::GameStatus>::new().get().await?.entries);
183 <crate::meta::Metric as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::Metric>::new().get().await?.entries);
184 <crate::meta::PitchCode as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::PitchCode>::new().get().await?.entries);
185 <crate::meta::PitchType as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::PitchType>::new().get().await?.entries);
186 <crate::meta::Platform as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::Platform>::new().get().await?.entries);
187 <crate::meta::Position as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::Position>::new().get().await?.entries);
188 <crate::meta::ReviewReason as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::ReviewReason>::new().get().await?.entries);
189 <crate::meta::ScheduleEventType as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::ScheduleEventType>::new().get().await?.entries);
190 <crate::meta::SituationCode as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::SituationCode>::new().get().await?.entries);
191 <crate::meta::SkyDescription as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::SkyDescription>::new().get().await?.entries);
192 <crate::meta::GameType as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::GameType>::new().get().await?.entries);
193 <crate::meta::GameType as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::GameType>::new().get().await?.entries);
194 <crate::meta::WindDirection as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::WindDirection>::new().get().await?.entries);
195
196 <Person as Requestable>::get_cache_table().write().await.add_entries(people_response.await?.people);
197
198 Ok(())
199}