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}