prehnite_core/db/schema/app_global/
book_search_api.rs

1use crate::db::schema::app_global::book_search_result::BookSearchResult;
2use reqwest::IntoUrl;
3use rhai::{Dynamic, Engine, EvalAltResult, Scope};
4use sqlx::{Acquire, FromRow};
5use std::fmt::{Display, Pointer};
6use thiserror::Error;
7
8#[derive(Error, Debug)]
9pub enum BookSearchApiError {
10    #[error("Failed to fetch api request.")]
11    RequestError(#[from] reqwest::Error),
12    #[error("Failed to run mapping script.")]
13    MappingScriptRuntimeError(String),
14    #[error("Failed to compile script.")]
15    MappingScriptCompileError(#[from] rhai::ParseError),
16    #[error("Mapping script was returned invalid type.")]
17    MappingScriptInvalidTypeError(String)
18}
19
20impl From<Box<EvalAltResult>> for BookSearchApiError {
21    fn from(value: Box<EvalAltResult>) -> Self {
22        BookSearchApiError::MappingScriptRuntimeError(value.to_string())
23    }
24}
25
26pub type BookSearchApiResult<T> = Result<T, BookSearchApiError>;
27
28#[derive(Default, Debug, Clone, FromRow, Eq, PartialEq)]
29pub struct BookSearchApi {
30    pub id: i64,
31    pub name: String,
32    pub detail: String,
33    pub isbn_url: String,
34    pub text_url: String,
35    pub mapping_script: String,
36    pub is_example: bool,
37}
38
39fn option_str_to_dynamic(value: Option<String>) -> Dynamic {
40    match value {
41        None => Dynamic::from(()),
42        Some(v) => Dynamic::from(v),
43    }
44}
45
46impl BookSearchApi {
47    async fn api_request(
48        &self,
49        url: impl IntoUrl,
50        isbn: Option<String>,
51        search_text: Option<String>,
52    ) -> BookSearchApiResult<Vec<BookSearchResult>> {
53        if self.is_example {
54            return Ok(vec![BookSearchResult::example()]);
55        }
56        let response = reqwest::Client::new()
57            .get(url)
58            .send()
59            .await?
60            .json::<Dynamic>()
61            .await?;
62        self.mapper(
63            response,
64            option_str_to_dynamic(isbn),
65            option_str_to_dynamic(search_text),
66        )
67    }
68
69    fn mapper(
70        &self,
71        response: Dynamic,
72        isbn: Dynamic,
73        search_text: Dynamic,
74    ) -> BookSearchApiResult<Vec<BookSearchResult>> {
75        let mut engine = Engine::new();
76        engine
77            .register_type_with_name::<BookSearchResult>("BookSearchResult")
78            .register_fn("new_res", BookSearchResult::new);
79        let engine = engine;
80        let ast = engine.compile(self.mapping_script.clone())?;
81        let mut scope = Scope::new();
82        Ok(match engine
83            .call_fn::<Dynamic>(&mut scope, &ast, "mapper", (isbn, search_text, response))?
84            .into_typed_array::<BookSearchResult>() {
85            Ok(v) => {v}
86            Err(e) => {return Err(BookSearchApiError::MappingScriptRuntimeError(e.to_string()))}
87        })
88    }
89
90    pub async fn search_isbn(
91        &self,
92        isbn: impl AsRef<str>,
93    ) -> BookSearchApiResult<Vec<BookSearchResult>> {
94        self.api_request(
95            self.isbn_url.replace("<isbn>", isbn.as_ref()),
96            Some(isbn.as_ref().into()),
97            None,
98        )
99        .await
100    }
101
102    pub async fn search_text(
103        &self,
104        text: impl AsRef<str>,
105    ) -> BookSearchApiResult<Vec<BookSearchResult>> {
106        self.api_request(
107            self.text_url.replace("<text>", text.as_ref()),
108            None,
109            Some(text.as_ref().into()),
110        )
111        .await
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use crate::db::schema::app_global::book_search_api::BookSearchApi;
118    use crate::db::schema::app_global::book_search_result::BookSearchResult;
119    use rhai::Dynamic;
120    use serde::{Deserializer, Serialize};
121
122    #[derive(Serialize, Clone)]
123    struct Result {
124        isbn: Option<String>,
125        url: Option<String>,
126        title: String,
127        authors: Option<Vec<String>>,
128        detail: Option<String>,
129        publisher: Option<String>,
130        publication_date: Option<String>,
131    }
132
133    #[derive(Serialize, Clone)]
134    struct Object {
135        status: i64,
136        result: Vec<Result>,
137    }
138
139    const MAPPER_TEST_SCRIPT: &str = r#"fn mapper(isbn, search_text, response){
140    let x = [];
141    for result in response.result {
142        x += new_res(
143            result.isbn, // isbn
144            result.url, // url
145            result.title, // title
146            result.detail, // detail
147            result.authors, // authors
148            result.publisher, // publisher
149            result.publication_date, // publication date
150        )
151    }
152    x
153}"#;
154
155    const MAPPER_OPTIONAL_TEST_SCRIPT: &str = r#"fn mapper(isbn, search_text, response){
156    let x = [];
157    x += new_res(
158        (), // isbn
159        (), // url
160        "", // title
161        (), // detail
162        (), // authors
163        (), // publisher
164        (), // publication date
165    );
166    x
167}"#;
168
169    const MAPPER_REQUIRED_TEST_SCRIPT: &str = r#"fn mapper(isbn, search_text, response){
170    let x = [];
171    x += new_res(
172        (), // isbn
173        (), // url
174        (), // title
175        (), // detail
176        (), // authors
177        (), // publisher
178        (), // publication date
179    );
180    x
181}"#;
182
183    #[test]
184    #[should_panic]
185    fn invalid_mapper_required_attr() {
186        BookSearchApi {
187            mapping_script: MAPPER_REQUIRED_TEST_SCRIPT.to_string(),
188            ..Default::default()
189        }
190        .mapper(Default::default(), Default::default(), Default::default())
191        .unwrap();
192    }
193
194    #[test]
195    fn valid_mapper_optional_attr() {
196        assert_eq!(
197            BookSearchApi {
198                mapping_script: MAPPER_OPTIONAL_TEST_SCRIPT.to_string(),
199                ..Default::default()
200            }
201            .mapper(Default::default(), Default::default(), Default::default())
202            .unwrap(),
203            vec![BookSearchResult {
204                isbn: None,
205                url: None,
206                title: "".to_string(),
207                detail: None,
208                authors: vec![],
209                publisher: None,
210                publication_date: None,
211            }]
212        );
213    }
214
215    #[test]
216    fn valid_mapper() {
217        let response = Object {
218            status: 0,
219            result: vec![
220                Result {
221                    isbn: Some("aaaaaaaa".to_string()),
222                    url: None,
223                    title: "bbbbbbbb".to_string(),
224                    authors: Some(vec!["cccccccc".to_string(), "dddddddd".to_string()]),
225                    detail: Some("eeeeeeee".to_string()),
226                    publisher: None,
227                    publication_date: Some("2023-01-01".to_string()),
228                },
229                Result {
230                    isbn: None,
231                    url: Some("ffffffff".to_string()),
232                    title: "gggggggg".to_string(),
233                    authors: None,
234                    detail: None,
235                    publisher: Some("hhhhhhhh".to_string()),
236                    publication_date: None,
237                },
238                Result {
239                    isbn: Some("iiiiiiii".to_string()),
240                    url: Some("jjjjjjjj".to_string()),
241                    title: "kkkkkkkk".to_string(),
242                    authors: Some(vec!["llllllll".to_string()]),
243                    detail: Some("mmmmmmmm".to_string()),
244                    publisher: Some("nnnnnnnn".to_string()),
245                    publication_date: Some("2023-05-10".to_string()),
246                },
247                Result {
248                    isbn: Some("oooooooo".to_string()),
249                    url: None,
250                    title: "pppppppp".to_string(),
251                    authors: Some(vec!["qqqqqqqq".to_string()]),
252                    detail: None,
253                    publisher: None,
254                    publication_date: Some("2022-12-25".to_string()),
255                },
256                Result {
257                    isbn: None,
258                    url: Some("rrrrrrrr".to_string()),
259                    title: "ssssssss".to_string(),
260                    authors: None,
261                    detail: Some("tttttttt".to_string()),
262                    publisher: Some("uuuuuuuu".to_string()),
263                    publication_date: None,
264                },
265                Result {
266                    isbn: Some("vvvvvvvv".to_string()),
267                    url: Some("wwwwwwww".to_string()),
268                    title: "xxxxxxxx".to_string(),
269                    authors: Some(vec!["yyyyyyyy".to_string(), "zzzzzzzz".to_string()]),
270                    detail: Some("aaaaaaaa".to_string()),
271                    publisher: None,
272                    publication_date: Some("2021-07-07".to_string()),
273                },
274                Result {
275                    isbn: None,
276                    url: None,
277                    title: "bbbbbbbb".to_string(),
278                    authors: None,
279                    detail: None,
280                    publisher: Some("cccccccc".to_string()),
281                    publication_date: None,
282                },
283                Result {
284                    isbn: Some("dddddddd".to_string()),
285                    url: Some("eeeeeeee".to_string()),
286                    title: "ffffffff".to_string(),
287                    authors: Some(vec!["gggggggg".to_string()]),
288                    detail: Some("hhhhhhhh".to_string()),
289                    publisher: Some("iiiiiiii".to_string()),
290                    publication_date: Some("2020-01-01".to_string()),
291                },
292                Result {
293                    isbn: Some("jjjjjjjj".to_string()),
294                    url: None,
295                    title: "kkkkkkkk".to_string(),
296                    authors: None,
297                    detail: Some("llllllll".to_string()),
298                    publisher: None,
299                    publication_date: Some("2024-02-29".to_string()),
300                },
301                Result {
302                    isbn: None,
303                    url: Some("mmmmmmmm".to_string()),
304                    title: "nnnnnnnn".to_string(),
305                    authors: Some(vec!["oooooooo".to_string()]),
306                    detail: None,
307                    publisher: Some("pppppppp".to_string()),
308                    publication_date: None,
309                },
310            ],
311        };
312
313        let mapped_result: Vec<BookSearchResult> = response
314            .clone()
315            .result
316            .into_iter()
317            .map(|v| BookSearchResult {
318                isbn: v.isbn,
319                url: v.url,
320                title: v.title,
321                detail: v.detail,
322                authors: v.authors.unwrap_or_default(),
323                publisher: v.publisher,
324                publication_date: v.publication_date,
325            })
326            .collect();
327
328        let api = BookSearchApi {
329            mapping_script: MAPPER_TEST_SCRIPT.to_string(),
330            ..Default::default()
331        };
332
333        assert_eq!(
334            api.mapper(
335                rhai::serde::to_dynamic(response).unwrap(),
336                Dynamic::from(()),
337                Dynamic::from(())
338            )
339            .unwrap(),
340            mapped_result
341        )
342    }
343}