Skip to main content

rs_plugin_common_interfaces/domain/
book.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::domain::{
5    media::MediaItemReference, other_ids::OtherIds, person::Person, rs_ids::{ApplyRsIds, RsIds}, tag::Tag
6};
7
8#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
9#[serde(rename_all = "camelCase")]
10pub struct Book {
11    #[serde(default)]
12    pub id: String,
13    pub name: String,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    #[serde(rename = "type")]
16    pub kind: Option<String>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub serie_ref: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub volume: Option<f64>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub chapter: Option<f64>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub year: Option<u16>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub airdate: Option<i64>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub overview: Option<String>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub pages: Option<u32>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub params: Option<Value>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub lang: Option<String>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub original: Option<String>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub isbn13: Option<String>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub openlibrary_edition_id: Option<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub openlibrary_work_id: Option<String>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub google_books_volume_id: Option<String>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub asin: Option<String>,
47    pub otherids: Option<OtherIds>,
48    #[serde(default)]
49    pub modified: u64,
50    #[serde(default)]
51    pub added: u64,
52}
53
54impl From<Book> for RsIds {
55    fn from(value: Book) -> Self {
56        let mut ids = RsIds::default();
57        if let Some(v) = value.isbn13 { ids.set("isbn13", v); }
58        if let Some(v) = value.openlibrary_edition_id { ids.set("oleid", v); }
59        if let Some(v) = value.openlibrary_work_id { ids.set("olwid", v); }
60        if let Some(v) = value.google_books_volume_id { ids.set("gbvid", v); }
61        if let Some(v) = value.asin { ids.set("asin", v); }
62        if let Some(v) = value.volume { ids.set("volume", v); }
63        if let Some(v) = value.chapter { ids.set("chapter", v); }
64        if let Some(other) = value.otherids {
65            for entry in other.into_vec() {
66                let _ = ids.try_add(entry);
67            }
68        }
69        if ids.try_add(value.id.clone()).is_err() {
70            ids.set("redseat", value.id);
71        }
72        ids
73    }
74}
75
76impl ApplyRsIds for Book {
77    fn apply_rs_ids(&mut self, ids: &RsIds) {
78        if let Some(redseat) = ids.redseat() {
79            self.id = redseat.to_string();
80        }
81        if let Some(isbn13) = ids.isbn13() {
82            self.isbn13 = Some(isbn13.to_string());
83        }
84        if let Some(oleid) = ids.openlibrary_edition_id() {
85            self.openlibrary_edition_id = Some(oleid.to_string());
86        }
87        if let Some(olwid) = ids.openlibrary_work_id() {
88            self.openlibrary_work_id = Some(olwid.to_string());
89        }
90        if let Some(gbvid) = ids.google_books_volume_id() {
91            self.google_books_volume_id = Some(gbvid.to_string());
92        }
93        if let Some(asin) = ids.asin() {
94            self.asin = Some(asin.to_string());
95        }
96        if let Some(volume) = ids.find_detail_f64("volume") {
97            self.volume = Some(volume);
98        }
99        if let Some(chapter) = ids.find_detail_f64("chapter") {
100            self.chapter = Some(chapter);
101        }
102    }
103}
104
105#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
106#[serde(rename_all = "camelCase")]
107pub struct BookForUpdate {
108    pub name: Option<String>,
109    #[serde(rename = "type")]
110    pub kind: Option<String>,
111    pub serie_ref: Option<String>,
112    pub volume: Option<f64>,
113    pub chapter: Option<f64>,
114    pub year: Option<u16>,
115    pub airdate: Option<i64>,
116    pub overview: Option<String>,
117    pub pages: Option<u32>,
118    pub params: Option<Value>,
119    pub lang: Option<String>,
120    pub original: Option<String>,
121    pub isbn13: Option<String>,
122    pub openlibrary_edition_id: Option<String>,
123    pub openlibrary_work_id: Option<String>,
124    pub google_books_volume_id: Option<String>,
125    pub asin: Option<String>,
126    pub otherids: Option<OtherIds>,
127
128
129    pub add_tags: Option<Vec<MediaItemReference>>,
130    pub remove_tags: Option<Vec<String>>,
131    pub tags_lookup: Option<Vec<Tag>>,
132
133    pub add_people: Option<Vec<MediaItemReference>>,
134    pub remove_people: Option<Vec<String>>,
135    pub people_lookup: Option<Vec<Person>>,
136}
137
138impl BookForUpdate {
139    pub fn has_update(&self) -> bool {
140        self != &BookForUpdate::default()
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::Book;
147    use crate::domain::{
148        other_ids::OtherIds,
149        rs_ids::{ApplyRsIds, RsIds},
150    };
151    use serde_json::json;
152
153    #[test]
154    fn book_otherids_serializes_as_array_and_rejects_string() {
155        let book = Book {
156            id: "book-1".to_string(),
157            name: "Book 1".to_string(),
158            otherids: Some(OtherIds(vec!["goodreads:321".to_string()])),
159            ..Default::default()
160        };
161        let value = serde_json::to_value(&book).unwrap();
162        assert_eq!(value.get("otherids"), Some(&json!(["goodreads:321"])));
163
164        let parsed: Book = serde_json::from_value(json!({
165            "id": "book-1",
166            "name": "Book 1",
167            "otherids": ["custom:1"]
168        }))
169        .unwrap();
170        assert_eq!(
171            parsed.otherids,
172            Some(OtherIds(vec!["custom:1".to_string()]))
173        );
174
175        let invalid = serde_json::from_value::<Book>(json!({
176            "id": "book-1",
177            "name": "Book 1",
178            "otherids": "custom:1"
179        }));
180        assert!(invalid.is_err());
181    }
182
183    #[test]
184    fn book_apply_rs_ids_updates_only_present_values() {
185        let mut book = Book {
186            id: "book-old".to_string(),
187            name: "Book 1".to_string(),
188            openlibrary_work_id: Some("olw-old".to_string()),
189            chapter: Some(7.0),
190            ..Default::default()
191        };
192        let mut ids = RsIds::default();
193        ids.set("redseat", "book-new");
194        ids.set("isbn13", "9783161484100");
195        ids.set("oleid", "ole-new");
196        ids.set("gbvid", "gb-1");
197        ids.set("asin", "B00TEST");
198        ids.set("volume", "3");
199
200        book.apply_rs_ids(&ids);
201
202        assert_eq!(book.id, "book-new");
203        assert_eq!(book.isbn13.as_deref(), Some("9783161484100"));
204        assert_eq!(book.openlibrary_edition_id.as_deref(), Some("ole-new"));
205        assert_eq!(book.openlibrary_work_id.as_deref(), Some("olw-old"));
206        assert_eq!(book.google_books_volume_id.as_deref(), Some("gb-1"));
207        assert_eq!(book.asin.as_deref(), Some("B00TEST"));
208        assert_eq!(book.volume, Some(3.0));
209        assert_eq!(book.chapter, Some(7.0));
210    }
211}