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 = "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/// Type representing the cached values of `T`; stored as `static` using [`Arc<RwLock<_>>`]
103///
104/// underlying structure is an [`FxHashMap`](fxhash::FxHashMap).
105#[cfg(feature = "cache")]
106pub struct CacheTable<T: Requestable> {
107    cached_values: HashMap<T::Identifier, Arc<T>, FxBuildHasher>,
108}
109
110/// Errors for [`as_complete_or_request`](RequestableEntrypoint::as_complete_or_request) calls.
111#[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    /// # Errors
149    /// See variants of [`crate::request::Error`]
150    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/// Caches popular types for [`Requestable`] use.
159///
160/// # Errors
161/// See variants of [`crate::request::Error`]
162#[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}