rusty_booru/shared/
client.rs

1use std::{
2    fmt::{Debug, Display},
3    marker::PhantomData,
4};
5
6use crate::generic::{AutoCompleteItem, BooruPost, Rating};
7
8use super::{Sort, Tag, Tags};
9use itertools::Itertools;
10
11#[derive(Debug)]
12pub struct ClientBuilder<T: ClientTypes> {
13    pub client: reqwest::Client,
14    pub url: String,
15
16    _marker: PhantomData<T>,
17}
18
19impl<T: ClientTypes> Clone for ClientBuilder<T> {
20    fn clone(&self) -> Self {
21        Self {
22            client: self.client.clone(),
23            url: self.url.clone(),
24            _marker: self._marker,
25        }
26    }
27}
28
29pub trait ClientInformation {
30    const URL: &'static str;
31    const SORT: &'static str;
32}
33
34pub trait ClientTypes {
35    type Rating: From<Rating> + Display + Debug + Clone;
36    type Post: Into<BooruPost>;
37}
38
39pub type QueryVec = Vec<(String, String)>;
40
41pub enum QueryLike {
42    Gelbooru,
43}
44
45pub enum QueryMode<'a, T: ClientTypes + ClientInformation> {
46    Single(u32),
47    Multiple(&'a ClientQueryBuilder<T>),
48}
49
50pub trait WithClientBuilder<T: ClientTypes> {
51    fn builder() -> ClientBuilder<T>;
52}
53
54impl<T: ClientInformation + ClientTypes + From<ClientBuilder<T>>> WithClientBuilder<T> for T {
55    fn builder() -> ClientBuilder<T> {
56        ClientBuilder::new()
57    }
58}
59
60pub trait QueryDispatcher<T: ClientTypes> {
61    fn get_autocomplete<In: Into<String> + Send>(
62        &self,
63        input: In,
64    ) -> impl std::future::Future<Output = Result<Vec<AutoCompleteItem>, reqwest::Error>> + Send;
65
66    /// Pack the [`ClientBuilder`] and sent the request to the API to retrieve the posts
67    fn get_by_id(
68        &self,
69        id: u32,
70    ) -> impl std::future::Future<Output = Result<Option<T::Post>, crate::shared::Error>> + Send;
71
72    /// Directly get a post by its unique Id
73    fn get(
74        &self,
75    ) -> impl std::future::Future<Output = Result<Vec<T::Post>, crate::shared::Error>> + Send;
76}
77
78pub trait WithCommonQuery {
79    fn common_query_type() -> QueryLike;
80}
81
82pub trait ImplementedWithCommonQuery<T: ClientTypes + ClientInformation> {
83    fn get_query(query_mode: QueryMode<T>) -> QueryVec;
84}
85
86impl<T: WithCommonQuery + ClientTypes + ClientInformation> ImplementedWithCommonQuery<T>
87    for ClientQueryDispatcher<T>
88{
89    fn get_query(query_mode: QueryMode<T>) -> QueryVec {
90        let query_type = T::common_query_type();
91
92        let mut base = match query_type {
93            QueryLike::Gelbooru => vec![
94                ("page", "dapi"),
95                ("s", "post"),
96                ("q", "index"),
97                ("json", "1"),
98            ],
99        }
100        .iter()
101        .map(|(k, v)| (k.to_string(), v.to_string()))
102        .collect_vec();
103
104        let extension = match query_type {
105            QueryLike::Gelbooru => match query_mode {
106                QueryMode::Single(id) => vec![("id", id.to_string())],
107                QueryMode::Multiple(query) => vec![
108                    ("limit", query.limit.to_string()),
109                    ("tags", query.tags.unpack()),
110                ],
111            },
112        }
113        .into_iter()
114        .map(|(k, v)| (k.to_string(), v))
115        .collect_vec();
116
117        base.extend(extension);
118        base
119    }
120}
121
122// ClientQueryBuilder is structured separately for a reason, it can't hold references to a client
123// builder that has an url. This because the generic client does not have a proper url, and thus
124// can't have the ClientInformation trait. By structuring it separately we can create a "contained"
125// query, that can then be passed on to the proper Client that will be able to figure out the
126// proper way to handle the used query at runtime.
127
128#[derive(Debug, Clone)]
129pub struct ClientQueryBuilder<T: ClientTypes> {
130    pub tags: Tags<T>,
131    pub limit: u32,
132}
133
134impl<T: ClientTypes + Clone> Default for ClientQueryBuilder<T> {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140impl<T: ClientTypes + Clone> ClientQueryBuilder<T> {
141    pub fn new() -> Self {
142        Self {
143            tags: Tags(Vec::new()),
144            limit: 100,
145        }
146    }
147
148    pub fn any_tag(&mut self, tag: Tag<T>) -> &mut Self {
149        self.tags.0.push(tag);
150        self
151    }
152
153    pub fn tag<S: ToString>(&mut self, tag: S) -> &mut Self {
154        self.any_tag(Tag::Plain(tag.to_string()))
155    }
156
157    pub fn sort(&mut self, sort: Sort) -> &mut Self {
158        self.any_tag(Tag::Sort(sort))
159    }
160
161    pub fn random(&mut self) -> &mut Self {
162        self.sort(Sort::Random)
163    }
164
165    pub fn rating(&mut self, rating: T::Rating) -> &mut Self {
166        self.any_tag(Tag::Rating(rating))
167    }
168
169    pub fn blacklist_tag<S: ToString>(&mut self, tag: S) -> &mut Self {
170        self.any_tag(Tag::Blacklist(tag.to_string()))
171    }
172
173    /// Set how many posts you want to retrieve (100 is the default and maximum)
174    pub fn limit(&mut self, limit: u32) -> &mut Self {
175        self.limit = limit;
176        self
177    }
178}
179
180impl<T: ClientTypes + Clone> ClientBuilder<T> {
181    fn create_dispatcher(&self, query: &mut ClientQueryBuilder<T>) -> ClientQueryDispatcher<T> {
182        ClientQueryDispatcher {
183            builder: self.to_owned(),
184            query: query.clone(),
185        }
186    }
187
188    pub fn query_raw(&self, query: &mut ClientQueryBuilder<T>) -> ClientQueryDispatcher<T> {
189        self.create_dispatcher(query)
190    }
191
192    pub fn query(
193        &self,
194        query_fn: impl Fn(&mut ClientQueryBuilder<T>) -> &mut ClientQueryBuilder<T>,
195    ) -> ClientQueryDispatcher<T> {
196        self.query_raw(query_fn(&mut ClientQueryBuilder::new()))
197    }
198
199    // Dispatches an empty query. Useful if you want to get a post by its id.
200    pub fn dispatch(&self) -> ClientQueryDispatcher<T> {
201        self.create_dispatcher(&mut ClientQueryBuilder::new())
202    }
203}
204
205#[derive(Debug, Clone)]
206pub struct ClientQueryDispatcher<T: ClientTypes> {
207    pub builder: ClientBuilder<T>,
208    pub query: ClientQueryBuilder<T>,
209}
210
211impl<T: ClientInformation + ClientTypes> ClientBuilder<T> {
212    pub fn new() -> Self {
213        Self {
214            client: reqwest::Client::new(),
215            url: T::URL.to_string(),
216
217            _marker: PhantomData,
218        }
219    }
220
221    /// Change the default url for the client
222    pub fn default_url(&mut self, url: &str) -> &mut Self {
223        self.url = url.to_string();
224        self
225    }
226}
227
228impl<T: ClientTypes + ClientInformation> Default for ClientBuilder<T> {
229    fn default() -> Self {
230        Self::new()
231    }
232}