Skip to main content

mlb_api/
cache.rs

1//! The caching feature of `mlb-api`
2//!
3//! Some requests (especially [`MetaRequest`]s) are great candidates for a cache since their contents do not change.
4//!
5//! Because of this, many types implement [`Requestable`] and [`RequestableEntrypoint`] such that they can be accessed easily from within the code.
6//!
7//! Types like [`NamedPosition`](crate::NamedPosition) can benefit even more than [`Person`] due to their ability to be cached more aggressively.
8//! By enabling the `aggressive_cache` feature, and or calling [`precache`] at the start of your `main` fn. You can cache these values in advance to make their lookups extremely fast.
9//! 
10//! Note that even without the `cache` feature, some of this module is still accessible, making requests just... not cache, and instead act as another lookup.
11//! 
12//! # Examples
13//! ```
14//! use mlb_api::person::PersonId;
15//!
16//! let person: PersonId = 660_271.into();
17//! // dbg!(&person.full_name); // person.full_name does not exist
18//!
19//! let person: Arc<Person> = person.as_complete_or_request().await.unwrap();
20//! dbg!(&person.full_name);
21//! ```
22//! 
23//! ```
24//! use mlb_api::meta::NamedPosition;
25//!
26//! let position: NamedPosition = NamedPosition { ..Default::default() }; // very common type to see
27//!
28//! let position: Arc<Position> = position.as_complete_or_request().await.unwrap();
29//! dbg!(&position.short_name);
30//! ```
31
32use 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
46/// A type that can be requested via a URL, such as a [`Position`], [`Award`], or [`Team`].
47///
48/// [`Position`]: crate::meta::Position
49/// [`Award`]: crate::awards::Award
50/// [`Team`]: crate::team::Team
51pub 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
65/// A type in which it can be [`as_complete_or_request`](RequestableEntrypoint::as_complete_or_request)ed into it's [`Complete`](RequestableEntrypoint::Complete) type.
66pub trait RequestableEntrypoint {
67    type Complete: Requestable;
68
69    fn id(&self) -> &<<Self as RequestableEntrypoint>::Complete as Requestable>::Identifier;
70
71    #[cfg(feature = "reqwest")]
72    #[cfg(feature = "cache")]
73    fn as_complete_or_request(&self) -> impl Future<Output = Result<Arc<<Self as RequestableEntrypoint>::Complete>, Error<Self>>>
74    where
75        Self: Sized,
76    { async {
77        let cache_lock = <<Self as RequestableEntrypoint>::Complete as Requestable>::get_cache_table();
78        let id = self.id();
79        let cache = cache_lock.read().await;
80        if let Some(complete_entry) = cache.get(id).cloned() {
81            return Ok(complete_entry);
82        }
83        drop(cache);
84
85        let mut cache = cache_lock.write().await;
86        cache.request_and_add(id).await?;
87        cache.get(id).cloned().ok_or_else(|| Error::NoMatchingVariant(id.clone()))
88    } }
89
90    #[cfg(feature = "ureq")]
91    #[cfg(feature = "cache")]
92    fn as_complete_or_request(&self) -> Result<Arc<<Self as RequestableEntrypoint>::Complete>, Error<Self>>
93    where
94        Self: Sized
95    {
96        let cache_lock = <<Self as RequestableEntrypoint>::Complete as Requestable>::get_cache_table();
97        let id = self.id();
98        let cache = cache_lock.read();
99        if let Some(complete_entry) = cache.get(id).cloned() {
100            return Ok(complete_entry);
101        }
102
103        let mut cache = cache_lock.write();
104        cache.request_and_add(id)?;
105        cache.get(id).cloned().ok_or_else(|| Error::NoMatchingVariant(id.clone()))
106    }
107
108    #[cfg(feature = "reqwest")]
109    #[cfg(not(feature = "cache"))]
110    fn as_complete_or_request(&self) -> impl Future<Output = Result<<Self as RequestableEntrypoint>::Complete, Error<Self>>>
111    where
112        Self: Sized,
113    { async {
114        let id = self.id();
115        let url = <Self::Complete as Requestable>::url_for_id(id).to_string();
116        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?;
117        let entries = <Self::Complete as Requestable>::get_entries(response);
118        entries.into_iter().next().ok_or_else(|| Error::<Self>::NoMatchingVariant(id.clone()))
119    } }
120
121    #[cfg(feature = "ureq")]
122    #[cfg(not(feature = "cache"))]
123    fn as_complete_or_request(&self) -> Result<Arc<<Self as RequestableEntrypoint>::Complete>, Error<Self>> {
124        let id = self.id();
125        let url = <Self::Complete as Requestable>::url_for_id(id).to_string();
126        let response = crate::request::get::<<<Self::Complete as Requestable>::URL as RequestURL>::Response>(&url)?;
127        let entries = <Self::Complete as Requestable>::get_entries(response);
128        entries.into_iter().next().ok_or_else(|| Error::<Self>::NoMatchingVariant(id.clone()))
129    }
130}
131
132/// Type representing the cached values of `T`; stored as `static` using [`Arc<RwLock<_>>`]
133///
134/// underlying structure is an [`FxHashMap`](fxhash::FxHashMap).
135#[cfg(feature = "cache")]
136pub struct CacheTable<T: Requestable> {
137    cached_values: HashMap<T::Identifier, Arc<T>, FxBuildHasher>,
138}
139
140/// Errors for [`as_complete_or_request`](RequestableEntrypoint::as_complete_or_request) calls.
141#[derive(Debug, Error)]
142pub enum Error<T: RequestableEntrypoint> {
143    #[error(transparent)]
144    Url(#[from] crate::request::Error),
145    #[error("No matching entry was found for id {0}")]
146    NoMatchingVariant(<T::Complete as Requestable>::Identifier),
147}
148
149#[cfg(feature = "cache")]
150impl<T: Requestable> CacheTable<T> {
151    #[allow(clippy::new_without_default, reason = "needs to be const")]
152    #[must_use]
153    pub const fn new() -> Self {
154        Self {
155            cached_values: HashMap::with_hasher(FxBuildHasher::new()),
156        }
157    }
158
159    #[must_use]
160    pub fn get(&self, id: &T::Identifier) -> Option<&Arc<T>> {
161        self.cached_values.get(id)
162    }
163
164    pub fn insert(&mut self, value: T) {
165        self.cached_values.insert(value.id().clone(), Arc::new(value));
166    }
167    
168    pub fn clear(&mut self) {
169        self.cached_values.clear();
170    }
171    
172    pub fn add_entries(&mut self, entries: impl IntoIterator<Item = T>) {
173        for entry in entries {
174            self.insert(entry);
175        }
176    }
177
178    /// # Errors
179    /// See variants of [`crate::request::Error`]
180    #[cfg(feature = "reqwest")]
181    pub async fn request_and_add(&mut self, id: &T::Identifier) -> Result<(), crate::request::Error> {
182        let url = <T as Requestable>::url_for_id(id).to_string();
183        let response = crate::request::get::<<<T as Requestable>::URL as RequestURL>::Response>(url).await?;
184        self.add_entries(<T as Requestable>::get_entries(response));
185        Ok(())
186    }
187
188    #[cfg(feature = "ureq")]
189    pub fn request_and_add(&mut self, id: &T::Identifier) -> Result<(), crate::request::Error> {
190        let url = <T as Requestable>::url_for_id(id).to_string();
191        let response = crate::request::get::<<<T as Requestable>::URL as RequestURL>::Response>(url)?;
192        self.add_entries(<T as Requestable>::get_entries(response));
193        Ok(())
194    }
195}
196
197/// Caches popular types for [`Requestable`] use.
198///
199/// # Errors
200/// See variants of [`crate::request::Error`]
201#[cfg(feature = "cache")]
202#[cfg(feature = "reqwest")]
203pub async fn precache() -> Result<(), crate::request::Error> {
204    let people_response = PlayersRequest::for_sport(SportId::MLB).build_and_get();
205    
206    let award_response = crate::awards::AwardRequest::builder().build_and_get();
207    let division_response = crate::division::DivisionsRequest::builder().build_and_get();
208    let conference_response = crate::conference::ConferencesRequest::builder().build_and_get();
209    let venue_response = crate::venue::VenuesRequest::builder().build_and_get();
210    let league_response = crate::league::LeaguesRequest::builder().build_and_get();
211    let sport_response = crate::sport::SportsRequest::builder().build_and_get();
212    <crate::awards::Award as Requestable>::get_cache_table().write().await.add_entries(award_response.await?.awards);
213    <crate::division::Division as Requestable>::get_cache_table().write().await.add_entries(division_response.await?.divisions);
214    <crate::conference::Conference as Requestable>::get_cache_table().write().await.add_entries(conference_response.await?.conferences);
215    <crate::venue::Venue as Requestable>::get_cache_table().write().await.add_entries(venue_response.await?.venues);
216    <crate::league::League as Requestable>::get_cache_table().write().await.add_entries(league_response.await?.leagues);
217    <crate::sport::Sport as Requestable>::get_cache_table().write().await.add_entries(sport_response.await?.sports);
218    
219    <crate::meta::BaseballStat as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::BaseballStat>::new().get().await?.entries);
220    <crate::meta::JobType as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::JobType>::new().get().await?.entries);
221    <crate::meta::GameStatus as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::GameStatus>::new().get().await?.entries);
222    <crate::meta::Metric as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::Metric>::new().get().await?.entries);
223    <crate::meta::PitchCode as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::PitchCode>::new().get().await?.entries);
224    <crate::meta::PitchType as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::PitchType>::new().get().await?.entries);
225    <crate::meta::Platform as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::Platform>::new().get().await?.entries);
226    <crate::meta::Position as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::Position>::new().get().await?.entries);
227    <crate::meta::ReviewReason as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::ReviewReason>::new().get().await?.entries);
228    <crate::meta::ScheduleEventType as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::ScheduleEventType>::new().get().await?.entries);
229    <crate::meta::SituationCode as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::SituationCode>::new().get().await?.entries);
230    <crate::meta::SkyDescription as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::SkyDescription>::new().get().await?.entries);
231    <crate::meta::GameType as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::GameType>::new().get().await?.entries);
232    <crate::meta::GameType as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::GameType>::new().get().await?.entries);
233    <crate::meta::WindDirection as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::WindDirection>::new().get().await?.entries);
234
235    <Person as Requestable>::get_cache_table().write().await.add_entries(people_response.await?.people);
236
237    Ok(())
238}
239
240/// Caches popular types for [`Requestable`] use.
241///
242/// # Errors
243/// See variants of [`crate::request::Error`]
244#[cfg(feature = "cache")]
245#[cfg(feature = "ureq")]
246pub fn precache() -> Result<(), crate::request::Error> {
247    <crate::awards::Award as Requestable>::get_cache_table().write().add_entries(crate::awards::AwardRequest::builder().build_and_get()?.awards);
248    <crate::division::Division as Requestable>::get_cache_table().write().add_entries(crate::division::DivisionsRequest::builder().build_and_get()?.divisions);
249    <crate::conference::Conference as Requestable>::get_cache_table().write().add_entries(crate::conference::ConferencesRequest::builder().build_and_get()?.conferences);
250    <crate::venue::Venue as Requestable>::get_cache_table().write().add_entries(crate::venue::VenuesRequest::builder().build_and_get()?.venues);
251    <crate::league::League as Requestable>::get_cache_table().write().add_entries(crate::league::LeaguesRequest::builder().build_and_get()?.leagues);
252    <crate::sport::Sport as Requestable>::get_cache_table().write().add_entries(crate::sport::SportsRequest::builder().build_and_get()?.sports);
253
254    <crate::meta::BaseballStat as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::BaseballStat>::new().get()?.entries);
255    <crate::meta::JobType as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::JobType>::new().get()?.entries);
256    <crate::meta::GameStatus as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::GameStatus>::new().get()?.entries);
257    <crate::meta::Metric as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::Metric>::new().get()?.entries);
258    <crate::meta::PitchCode as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::PitchCode>::new().get()?.entries);
259    <crate::meta::PitchType as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::PitchType>::new().get()?.entries);
260    <crate::meta::Platform as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::Platform>::new().get()?.entries);
261    <crate::meta::Position as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::Position>::new().get()?.entries);
262    <crate::meta::ReviewReason as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::ReviewReason>::new().get()?.entries);
263    <crate::meta::ScheduleEventType as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::ScheduleEventType>::new().get()?.entries);
264    <crate::meta::SituationCode as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::SituationCode>::new().get()?.entries);
265    <crate::meta::SkyDescription as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::SkyDescription>::new().get()?.entries);
266    <crate::meta::GameType as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::GameType>::new().get()?.entries);
267    <crate::meta::GameType as Requestable>::get_cache_table().write().await.add_entries(MetaRequest::<crate::meta::GameType>::new().get().await?.entries);
268    <crate::meta::WindDirection as Requestable>::get_cache_table().write().add_entries(MetaRequest::<crate::meta::WindDirection>::new().get()?.entries);
269
270    <crate::person::Person as Requestable>::get_cache_table().write().add_entries(PlayersRequest::for_sport(SportId::MLB).build_and_get()?.people);
271
272    Ok(())
273}