rosu_v2/future/
mod.rs

1use std::{
2    future::{Future, IntoFuture},
3    ops::ControlFlow,
4    pin::Pin,
5    sync::Arc,
6    task::{Context, Poll},
7};
8
9use pin_project::pin_project;
10
11use crate::{
12    request::{GetUser, Request, UserId},
13    Osu, OsuResult,
14};
15
16use self::stage::{OsuFutureStage, OsuRequestStageInner};
17
18pub(crate) use self::token::TokenFuture;
19
20pub use self::traits::*;
21
22mod request_generator;
23mod stage;
24mod token;
25mod traits;
26
27type FromUserFn<T> = fn(u32, <T as OsuFutureData>::FromUserData) -> Request;
28
29struct FromUser<T: OsuFutureData> {
30    data: T::FromUserData,
31    f: FromUserFn<T>,
32}
33
34type PostProcessFn<T> = fn(
35    <T as OsuFutureData>::FromBytes,
36    <T as OsuFutureData>::PostProcessData,
37) -> OsuResult<<T as OsuFutureData>::OsuOutput>;
38
39struct PostProcess<T: OsuFutureData> {
40    data: T::PostProcessData,
41    f: PostProcessFn<T>,
42}
43
44/// Awaitable [`Future`] to fetch and process data from an endpoint.
45///
46/// When fetching from user endpoints by name instead of id, the [`OsuFuture`]
47/// might first perform a request to fetch the user itself, and then perform
48/// the actual request by using the fetched user id. If the `cache` feature
49/// is enabled, fetched user data will be stored to potentially prevent
50/// intermediate user requests later on.
51#[pin_project]
52pub struct OsuFuture<T: OsuFutureData> {
53    #[pin]
54    stage: OsuFutureStage,
55    from_user: Option<FromUser<T>>,
56    post_process: Option<PostProcess<T>>,
57}
58
59impl<T: OsuFutureData> OsuFuture<T> {
60    /// Creates a new [`OsuFuture`] from the given [`Request`].
61    pub(crate) fn new(
62        osu: &Osu,
63        req: Request,
64        post_process_data: T::PostProcessData,
65        post_process_fn: PostProcessFn<T>,
66    ) -> Self {
67        let osu = Arc::clone(&osu.inner);
68
69        Self {
70            stage: OsuRequestStageInner::new(osu, req)
71                .map_or_else(OsuFutureStage::Failed, OsuFutureStage::Final),
72            from_user: None,
73            post_process: Some(PostProcess {
74                data: post_process_data,
75                f: post_process_fn,
76            }),
77        }
78    }
79
80    /// Creates a new [`OsuFuture`] which might fetch a user first if the
81    /// given [`UserId`] is a name that has not been cached yet.
82    pub(crate) fn from_user_id(
83        osu: &Osu,
84        user_id: UserId,
85        from_user_data: T::FromUserData,
86        from_user_fn: FromUserFn<T>,
87        post_process_data: T::PostProcessData,
88        post_process_fn: PostProcessFn<T>,
89    ) -> Self {
90        #[cfg(not(feature = "cache"))]
91        let get_user_id: fn(UserId) -> UserId = std::convert::identity;
92
93        #[cfg(feature = "cache")]
94        fn get_user_id(mut user_id: UserId, osu: &Osu) -> UserId {
95            if let UserId::Name(ref mut name) = user_id {
96                name.make_ascii_lowercase();
97
98                if let Some(id) = osu.inner.cache.get(name) {
99                    return UserId::Id(*id);
100                }
101            }
102
103            user_id
104        }
105
106        match get_user_id(
107            user_id,
108            #[cfg(feature = "cache")]
109            osu,
110        ) {
111            UserId::Id(user_id) => {
112                let req = from_user_fn(user_id, from_user_data);
113
114                Self::new(osu, req, post_process_data, post_process_fn)
115            }
116            user_id @ UserId::Name(_) => {
117                #[cfg(not(feature = "cache"))]
118                {
119                    static NOTIF: std::sync::Once = std::sync::Once::new();
120
121                    // In case users intend to fetch frequently from user
122                    // endpoints by username but weren't aware either that
123                    // they have the `cache` feature disabled or that
124                    // disabling the feature will add an additional request
125                    // on every fetch, let's remind them a single time.
126                    NOTIF.call_once(|| {
127                        warn!(
128                            "Fetching from a user endpoint by username will \
129                            always perform two requests because the `cache` \
130                            feature is not enabled"
131                        );
132                    });
133                }
134
135                let osu = Arc::clone(&osu.inner);
136                let req = GetUser::create_request(user_id, None);
137
138                Self {
139                    stage: OsuRequestStageInner::new(osu, req)
140                        .map_or_else(OsuFutureStage::Failed, OsuFutureStage::User),
141                    from_user: Some(FromUser {
142                        data: from_user_data,
143                        f: from_user_fn,
144                    }),
145                    post_process: Some(PostProcess {
146                        data: post_process_data,
147                        f: post_process_fn,
148                    }),
149                }
150            }
151        }
152    }
153}
154
155impl<T: OsuFutureData> Future for OsuFuture<T> {
156    type Output = <T as IntoFuture>::Output;
157
158    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
159        let mut this = self.as_mut().project();
160
161        match this.stage.as_mut().poll(cx) {
162            Poll::Ready(ControlFlow::Break(Ok((bytes, osu)))) => {
163                let res = <T::FromBytes>::from_bytes(bytes)?;
164                let PostProcess { data, f } =
165                    this.post_process.take().expect("missing post_process");
166
167                let value = f(res, data)?;
168
169                #[cfg(feature = "cache")]
170                crate::model::ContainedUsers::apply_to_users(&value, |id, name| {
171                    osu.update_cache(id, name);
172                });
173
174                // Preventing "unused variable" lint w/o `cache` feature
175                let _ = osu;
176
177                Poll::Ready(Ok(value))
178            }
179            Poll::Ready(ControlFlow::Continue((user, osu))) => {
180                #[cfg(feature = "cache")]
181                osu.update_cache(user.user_id, &user.username);
182
183                #[cfg(feature = "metrics")]
184                // Technically, using a gauge and setting it to
185                // `osu.cache.len()` would be more correct but since
186                // `DashMap::len` is a non-trivial call, it should be fine
187                // to increment a counter. This works because we're only in
188                // this path if the cache did not contain the username in
189                // the first place, meaning we indeed add a new entry.
190                ::metrics::counter!(crate::metrics::USERNAME_CACHE_SIZE).increment(1);
191
192                let FromUser { data, f } = this.from_user.take().expect("missing from_user");
193                let req = f(user.user_id, data);
194
195                let next = OsuRequestStageInner::new(osu, req)?;
196                this.stage.project_replace(OsuFutureStage::Final(next));
197
198                self.poll(cx)
199            }
200            Poll::Ready(ControlFlow::Break(Err(err))) => Poll::Ready(Err(err)),
201            Poll::Pending => Poll::Pending,
202        }
203    }
204}
205
206#[allow(clippy::unnecessary_wraps)]
207pub(crate) const fn noop_post_process<T>(value: T, _: ()) -> OsuResult<T> {
208    Ok(value)
209}