use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::domain::{
media::MediaItemReference, other_ids::OtherIds, person::Person, rs_ids::{ApplyRsIds, RsIds}, tag::Tag
};
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Book {
#[serde(default)]
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "type")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub serie_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chapter: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub year: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub airdate: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub overview: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pages: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lang: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub original: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub isbn13: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub openlibrary_edition_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub openlibrary_work_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub google_books_volume_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub asin: Option<String>,
pub otherids: Option<OtherIds>,
#[serde(default)]
pub modified: u64,
#[serde(default)]
pub added: u64,
}
impl From<Book> for RsIds {
fn from(value: Book) -> Self {
let mut ids = RsIds::default();
if let Some(v) = value.isbn13 { ids.set("isbn13", v); }
if let Some(v) = value.openlibrary_edition_id { ids.set("oleid", v); }
if let Some(v) = value.openlibrary_work_id { ids.set("olwid", v); }
if let Some(v) = value.google_books_volume_id { ids.set("gbvid", v); }
if let Some(v) = value.asin { ids.set("asin", v); }
if let Some(v) = value.volume { ids.set("volume", v); }
if let Some(v) = value.chapter { ids.set("chapter", v); }
if let Some(other) = value.otherids {
for entry in other.into_vec() {
let _ = ids.try_add(entry);
}
}
if ids.try_add(value.id.clone()).is_err() {
ids.set("redseat", value.id);
}
ids
}
}
impl ApplyRsIds for Book {
fn apply_rs_ids(&mut self, ids: &RsIds) {
if let Some(redseat) = ids.redseat() {
self.id = redseat.to_string();
}
if let Some(isbn13) = ids.isbn13() {
self.isbn13 = Some(isbn13.to_string());
}
if let Some(oleid) = ids.openlibrary_edition_id() {
self.openlibrary_edition_id = Some(oleid.to_string());
}
if let Some(olwid) = ids.openlibrary_work_id() {
self.openlibrary_work_id = Some(olwid.to_string());
}
if let Some(gbvid) = ids.google_books_volume_id() {
self.google_books_volume_id = Some(gbvid.to_string());
}
if let Some(asin) = ids.asin() {
self.asin = Some(asin.to_string());
}
if let Some(volume) = ids.find_detail_f64("volume") {
self.volume = Some(volume);
}
if let Some(chapter) = ids.find_detail_f64("chapter") {
self.chapter = Some(chapter);
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BookForUpdate {
pub name: Option<String>,
#[serde(rename = "type")]
pub kind: Option<String>,
pub serie_ref: Option<String>,
pub volume: Option<f64>,
pub chapter: Option<f64>,
pub year: Option<u16>,
pub airdate: Option<i64>,
pub overview: Option<String>,
pub pages: Option<u32>,
pub params: Option<Value>,
pub lang: Option<String>,
pub original: Option<String>,
pub isbn13: Option<String>,
pub openlibrary_edition_id: Option<String>,
pub openlibrary_work_id: Option<String>,
pub google_books_volume_id: Option<String>,
pub asin: Option<String>,
pub otherids: Option<OtherIds>,
pub add_tags: Option<Vec<MediaItemReference>>,
pub remove_tags: Option<Vec<String>>,
pub tags_lookup: Option<Vec<Tag>>,
pub add_people: Option<Vec<MediaItemReference>>,
pub remove_people: Option<Vec<String>>,
pub people_lookup: Option<Vec<Person>>,
}
impl BookForUpdate {
pub fn has_update(&self) -> bool {
self != &BookForUpdate::default()
}
}
#[cfg(test)]
mod tests {
use super::Book;
use crate::domain::{
other_ids::OtherIds,
rs_ids::{ApplyRsIds, RsIds},
};
use serde_json::json;
#[test]
fn book_otherids_serializes_as_array_and_rejects_string() {
let book = Book {
id: "book-1".to_string(),
name: "Book 1".to_string(),
otherids: Some(OtherIds(vec!["goodreads:321".to_string()])),
..Default::default()
};
let value = serde_json::to_value(&book).unwrap();
assert_eq!(value.get("otherids"), Some(&json!(["goodreads:321"])));
let parsed: Book = serde_json::from_value(json!({
"id": "book-1",
"name": "Book 1",
"otherids": ["custom:1"]
}))
.unwrap();
assert_eq!(
parsed.otherids,
Some(OtherIds(vec!["custom:1".to_string()]))
);
let invalid = serde_json::from_value::<Book>(json!({
"id": "book-1",
"name": "Book 1",
"otherids": "custom:1"
}));
assert!(invalid.is_err());
}
#[test]
fn book_apply_rs_ids_updates_only_present_values() {
let mut book = Book {
id: "book-old".to_string(),
name: "Book 1".to_string(),
openlibrary_work_id: Some("olw-old".to_string()),
chapter: Some(7.0),
..Default::default()
};
let mut ids = RsIds::default();
ids.set("redseat", "book-new");
ids.set("isbn13", "9783161484100");
ids.set("oleid", "ole-new");
ids.set("gbvid", "gb-1");
ids.set("asin", "B00TEST");
ids.set("volume", "3");
book.apply_rs_ids(&ids);
assert_eq!(book.id, "book-new");
assert_eq!(book.isbn13.as_deref(), Some("9783161484100"));
assert_eq!(book.openlibrary_edition_id.as_deref(), Some("ole-new"));
assert_eq!(book.openlibrary_work_id.as_deref(), Some("olw-old"));
assert_eq!(book.google_books_volume_id.as_deref(), Some("gb-1"));
assert_eq!(book.asin.as_deref(), Some("B00TEST"));
assert_eq!(book.volume, Some(3.0));
assert_eq!(book.chapter, Some(7.0));
}
}