From 446fd86af2b5127045e88189c2465acc61bd8abe Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Fri, 21 Feb 2025 13:26:06 -0500 Subject: [PATCH] Treat Item::custom as a free-form object --- .github/workflows/ci.yml | 3 +- Cargo.toml | 3 +- src/json.rs | 60 ++++++++++++++++++++++++++++++++-------- 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5867cbd..ede4eb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,8 @@ jobs: git clone --depth 1 https://github.com/citation-style-language/locales - uses: Swatinem/rust-cache@v2 - run: cargo build - - run: cargo test + - run: cargo test --no-default-features + - run: cargo test --all-features checks: name: Check clippy, formatting, and documentation diff --git a/Cargo.toml b/Cargo.toml index 05f78e4..3be67ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,12 +13,13 @@ exclude = ["assets/*"] [features] default = [] -json = ["unscanny"] # adds support for CSL-json parsing +json = ["unscanny", "serde_json" ] # adds support for CSL-json parsing [dependencies] quick-xml = { version = "0.36.2", features = ["serialize", "overlapped-lists"] } serde = { version = "1.0", features = ["derive"] } unscanny = { version = "0.1.0", optional = true } +serde_json = { version = "1.0.107", optional = true } [dev-dependencies] ciborium = "0.2.1" diff --git a/src/json.rs b/src/json.rs index 12aed14..3bfef16 100644 --- a/src/json.rs +++ b/src/json.rs @@ -8,30 +8,45 @@ use std::{collections::BTreeMap, str::FromStr}; use serde::{Deserialize, Serialize}; use unscanny::Scanner; +/// Potential values of [`Item::custom`]. +pub type CustomValue = serde_json::Value; + +/// A free-form object storing custom key-value pairs. +pub type Custom = serde_json::Map; + /// A CSL-JSON item. #[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] -#[serde(transparent)] -pub struct Item(pub BTreeMap); +pub struct Item { + /// Custom key-value pairs. + /// + /// Used to store additional information that does not have a designated CSL JSON field. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub custom: Option, + + /// All fields of the item except for [`custom`][Self::custom]. + #[serde(flatten)] + pub fields: BTreeMap, +} impl Item { /// The item's ID. pub fn id(&self) -> Option> { - self.0.get("id")?.to_str() + self.fields.get("id")?.to_str() } /// The item type. pub fn type_(&self) -> Option> { - self.0.get("type")?.to_str() + self.fields.get("type")?.to_str() } /// Whether any of the fields values contains any HTML. pub fn has_html(&self) -> bool { - self.0.values().any(|v| v.has_html()) + self.fields.values().any(|v| v.has_html()) } /// Whether this entry may contain "cheater syntax" for odd fields. pub fn may_have_hack(&self) -> bool { - self.0.contains_key("note") + self.fields.contains_key("note") } } @@ -459,9 +474,9 @@ mod tests { #[test] fn test_serialize() { - let mut map = BTreeMap::new(); - map.insert("title".to_string(), Value::String("The Title".to_string())); - map.insert( + let mut fields = BTreeMap::new(); + fields.insert("title".to_string(), Value::String("The Title".to_string())); + fields.insert( "author".to_string(), Value::Names(vec![NameValue::Item(NameItem { family: "Doe".to_string(), @@ -471,7 +486,7 @@ mod tests { suffix: None, })]), ); - map.insert( + fields.insert( "date".to_string(), Value::Date(DateValue::Raw { raw: FixedDateRange::from_str("2021-09-10/2022-01-01").unwrap(), @@ -480,7 +495,30 @@ mod tests { }), ); - let item = Item(map); + let item = Item { custom: None, fields }; println!("{}", serde_json::to_string_pretty(&item).unwrap()); } + + #[test] + fn test_roundtrip_custom() { + let mut fields = BTreeMap::new(); + fields.insert("foo".into(), Value::String("bar".into())); + + let custom = serde_json::json! {{ + "bool": true, + "float": 35.6, + "null": null, + }} + .as_object() + .cloned() + .unwrap() + .into(); + + let item = Item { custom, fields }; + + let serialized = serde_json::to_string(&item).unwrap(); + let deserialized: Item = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(deserialized, item); + } }