Skip to main content

citum_schema_data/reference/
serde_impl.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus
4*/
5
6//! `Deserialize`/`Serialize`/`JsonSchema` impls for [`InputReference`].
7//!
8//! Owns the flat-with-discriminator wire format: a `class` string keys
9//! dispatch to the typed [`super::ClassExtension`] payload.
10
11#[cfg(feature = "schema")]
12use schemars::JsonSchema;
13use serde::de::{self, MapAccess, Visitor};
14use serde::ser::SerializeMap as _;
15use serde::{Deserialize, Deserializer, Serialize, Serializer};
16use serde_json::{Map as JsonMap, Value as JsonValue};
17
18#[cfg(feature = "schema")]
19use super::types::legal::{Brief, Hearing, LegalCase, Regulation, Statute, Treaty};
20#[cfg(feature = "schema")]
21use super::types::specialized::{
22    AudioVisualWork, Classic, Dataset, Event, Patent, Software, Standard,
23};
24#[cfg(feature = "schema")]
25use super::types::structural::{
26    Collection, CollectionComponent, Monograph, Serial, SerialComponent,
27};
28use super::{ClassExtension, InputReference, ReferenceClass, UnknownClassData};
29
30/// Produce a serde duplicate-field error with the canonical
31/// `duplicate field \`<name>\`` shape.
32///
33/// `serde::de::Error::duplicate_field` requires `&'static str`; our keys are
34/// dynamic, so we route through `custom` while preserving the exact message
35/// format that `duplicate_field` would emit. Downstream consumers matching
36/// on the `Display` output see identical text.
37fn duplicate_field_error<E: de::Error>(field: &str) -> E {
38    de::Error::custom(format_args!("duplicate field `{field}`"))
39}
40
41impl<'de> Deserialize<'de> for InputReference {
42    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
43    where
44        D: Deserializer<'de>,
45    {
46        struct ReferenceVisitor;
47
48        impl<'de> Visitor<'de> for ReferenceVisitor {
49            type Value = InputReference;
50
51            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52                formatter.write_str("a flat reference object with a `class` discriminator")
53            }
54
55            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
56            where
57                M: MapAccess<'de>,
58            {
59                let mut class = None;
60                let mut body = JsonMap::new();
61
62                while let Some(key) = map.next_key::<String>()? {
63                    if key == "class" {
64                        if class.is_some() {
65                            // Canonical serde duplicate-field error; matches the
66                            // shape that `de::Error::duplicate_field` would emit
67                            // for the dynamic-name path below.
68                            return Err(duplicate_field_error::<M::Error>("class"));
69                        }
70                        class = Some(map.next_value::<String>()?);
71                    } else {
72                        let value = map.next_value::<JsonValue>()?;
73                        if body.insert(key.clone(), value).is_some() {
74                            return Err(duplicate_field_error::<M::Error>(&key));
75                        }
76                    }
77                }
78
79                let class = class.ok_or_else(|| de::Error::missing_field("class"))?;
80                deserialize_reference_body(&class, body).map_err(de::Error::custom)
81            }
82        }
83
84        deserializer.deserialize_map(ReferenceVisitor)
85    }
86}
87
88/// Flat-with-class serialization proxy: prepends a `class` field and flattens
89/// the typed payload directly through the serializer. Avoids the
90/// `serde_json::to_value` round-trip that the previous implementation used
91/// to compute the body map.
92#[derive(Serialize)]
93struct FlatClassProxy<'a, T: Serialize + ?Sized> {
94    class: &'a str,
95    #[serde(flatten)]
96    inner: &'a T,
97}
98
99impl Serialize for InputReference {
100    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
101    where
102        S: Serializer,
103    {
104        // For known classes we let serde's `flatten` machinery splat the
105        // typed inner struct directly into the parent serializer — no
106        // intermediate `serde_json::Value` allocation per reference. For
107        // `Unknown`, the payload is already a `JsonMap`, so we walk it
108        // directly.
109        match &self.extension {
110            ClassExtension::Monograph(inner) => FlatClassProxy {
111                class: self.extension.class_name(),
112                inner: inner.as_ref(),
113            }
114            .serialize(serializer),
115            ClassExtension::CollectionComponent(inner) => FlatClassProxy {
116                class: self.extension.class_name(),
117                inner: inner.as_ref(),
118            }
119            .serialize(serializer),
120            ClassExtension::SerialComponent(inner) => FlatClassProxy {
121                class: self.extension.class_name(),
122                inner: inner.as_ref(),
123            }
124            .serialize(serializer),
125            ClassExtension::Collection(inner) => FlatClassProxy {
126                class: self.extension.class_name(),
127                inner: inner.as_ref(),
128            }
129            .serialize(serializer),
130            ClassExtension::Serial(inner) => FlatClassProxy {
131                class: self.extension.class_name(),
132                inner: inner.as_ref(),
133            }
134            .serialize(serializer),
135            ClassExtension::LegalCase(inner) => FlatClassProxy {
136                class: self.extension.class_name(),
137                inner: inner.as_ref(),
138            }
139            .serialize(serializer),
140            ClassExtension::Statute(inner) => FlatClassProxy {
141                class: self.extension.class_name(),
142                inner: inner.as_ref(),
143            }
144            .serialize(serializer),
145            ClassExtension::Treaty(inner) => FlatClassProxy {
146                class: self.extension.class_name(),
147                inner: inner.as_ref(),
148            }
149            .serialize(serializer),
150            ClassExtension::Hearing(inner) => FlatClassProxy {
151                class: self.extension.class_name(),
152                inner: inner.as_ref(),
153            }
154            .serialize(serializer),
155            ClassExtension::Regulation(inner) => FlatClassProxy {
156                class: self.extension.class_name(),
157                inner: inner.as_ref(),
158            }
159            .serialize(serializer),
160            ClassExtension::Brief(inner) => FlatClassProxy {
161                class: self.extension.class_name(),
162                inner: inner.as_ref(),
163            }
164            .serialize(serializer),
165            ClassExtension::Classic(inner) => FlatClassProxy {
166                class: self.extension.class_name(),
167                inner: inner.as_ref(),
168            }
169            .serialize(serializer),
170            ClassExtension::Patent(inner) => FlatClassProxy {
171                class: self.extension.class_name(),
172                inner: inner.as_ref(),
173            }
174            .serialize(serializer),
175            ClassExtension::Dataset(inner) => FlatClassProxy {
176                class: self.extension.class_name(),
177                inner: inner.as_ref(),
178            }
179            .serialize(serializer),
180            ClassExtension::Standard(inner) => FlatClassProxy {
181                class: self.extension.class_name(),
182                inner: inner.as_ref(),
183            }
184            .serialize(serializer),
185            ClassExtension::Software(inner) => FlatClassProxy {
186                class: self.extension.class_name(),
187                inner: inner.as_ref(),
188            }
189            .serialize(serializer),
190            ClassExtension::Event(inner) => FlatClassProxy {
191                class: self.extension.class_name(),
192                inner: inner.as_ref(),
193            }
194            .serialize(serializer),
195            ClassExtension::AudioVisual(inner) => FlatClassProxy {
196                class: self.extension.class_name(),
197                inner: inner.as_ref(),
198            }
199            .serialize(serializer),
200            ClassExtension::Unknown(data) => {
201                let mut out = serializer.serialize_map(Some(data.fields.len() + 1))?;
202                out.serialize_entry("class", &data.class)?;
203                for (key, value) in &data.fields {
204                    out.serialize_entry(key, value)?;
205                }
206                out.end()
207            }
208        }
209    }
210}
211
212#[cfg(feature = "schema")]
213impl JsonSchema for InputReference {
214    fn schema_name() -> std::borrow::Cow<'static, str> {
215        "InputReference".into()
216    }
217
218    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
219        let variants = [
220            reference_schema_branch::<Monograph>(generator, ReferenceClass::Monograph.name()),
221            reference_schema_branch::<CollectionComponent>(
222                generator,
223                ReferenceClass::CollectionComponent.name(),
224            ),
225            reference_schema_branch::<SerialComponent>(
226                generator,
227                ReferenceClass::SerialComponent.name(),
228            ),
229            reference_schema_branch::<Collection>(generator, ReferenceClass::Collection.name()),
230            reference_schema_branch::<Serial>(generator, ReferenceClass::Serial.name()),
231            reference_schema_branch::<LegalCase>(generator, ReferenceClass::LegalCase.name()),
232            reference_schema_branch::<Statute>(generator, ReferenceClass::Statute.name()),
233            reference_schema_branch::<Treaty>(generator, ReferenceClass::Treaty.name()),
234            reference_schema_branch::<Hearing>(generator, ReferenceClass::Hearing.name()),
235            reference_schema_branch::<Regulation>(generator, ReferenceClass::Regulation.name()),
236            reference_schema_branch::<Brief>(generator, ReferenceClass::Brief.name()),
237            reference_schema_branch::<Classic>(generator, ReferenceClass::Classic.name()),
238            reference_schema_branch::<Patent>(generator, ReferenceClass::Patent.name()),
239            reference_schema_branch::<Dataset>(generator, ReferenceClass::Dataset.name()),
240            reference_schema_branch::<Standard>(generator, ReferenceClass::Standard.name()),
241            reference_schema_branch::<Software>(generator, ReferenceClass::Software.name()),
242            reference_schema_branch::<Event>(generator, ReferenceClass::Event.name()),
243            reference_schema_branch::<AudioVisualWork>(
244                generator,
245                ReferenceClass::AudioVisual.name(),
246            ),
247        ];
248
249        schemars::json_schema!({
250            "oneOf": variants,
251            "unevaluatedProperties": false
252        })
253    }
254}
255
256#[cfg(feature = "schema")]
257fn reference_schema_branch<T: JsonSchema>(
258    generator: &mut schemars::SchemaGenerator,
259    class: &str,
260) -> JsonValue {
261    let mut schema = T::json_schema(generator);
262    let object = schema.ensure_object();
263    if !object.get("properties").is_some_and(JsonValue::is_object) {
264        object.insert("properties".to_string(), JsonValue::Object(JsonMap::new()));
265    }
266    let Some(properties) = object
267        .get_mut("properties")
268        .and_then(JsonValue::as_object_mut)
269    else {
270        return schema.to_value();
271    };
272    properties.insert(
273        "class".to_string(),
274        serde_json::json!({
275            "type": "string",
276            "const": class
277        }),
278    );
279
280    if !object.get("required").is_some_and(JsonValue::is_array) {
281        object.insert("required".to_string(), JsonValue::Array(Vec::new()));
282    }
283    let Some(required) = object.get_mut("required").and_then(JsonValue::as_array_mut) else {
284        return schema.to_value();
285    };
286    if !required.iter().any(|value| value.as_str() == Some("class")) {
287        required.push(JsonValue::String("class".to_string()));
288    }
289
290    schema.to_value()
291}
292
293fn deserialize_reference_body(
294    class: &str,
295    body: JsonMap<String, JsonValue>,
296) -> Result<InputReference, serde_json::Error> {
297    let value = JsonValue::Object(body);
298    match ReferenceClass::from_known_name(class) {
299        Some(ReferenceClass::Monograph) => {
300            InputReference::from_known(ClassExtension::Monograph, value)
301        }
302        Some(ReferenceClass::CollectionComponent) => {
303            InputReference::from_known(ClassExtension::CollectionComponent, value)
304        }
305        Some(ReferenceClass::SerialComponent) => {
306            InputReference::from_known(ClassExtension::SerialComponent, value)
307        }
308        Some(ReferenceClass::Collection) => {
309            InputReference::from_known(ClassExtension::Collection, value)
310        }
311        Some(ReferenceClass::Serial) => InputReference::from_known(ClassExtension::Serial, value),
312        Some(ReferenceClass::LegalCase) => {
313            InputReference::from_known(ClassExtension::LegalCase, value)
314        }
315        Some(ReferenceClass::Statute) => InputReference::from_known(ClassExtension::Statute, value),
316        Some(ReferenceClass::Treaty) => InputReference::from_known(ClassExtension::Treaty, value),
317        Some(ReferenceClass::Hearing) => InputReference::from_known(ClassExtension::Hearing, value),
318        Some(ReferenceClass::Regulation) => {
319            InputReference::from_known(ClassExtension::Regulation, value)
320        }
321        Some(ReferenceClass::Brief) => InputReference::from_known(ClassExtension::Brief, value),
322        Some(ReferenceClass::Classic) => InputReference::from_known(ClassExtension::Classic, value),
323        Some(ReferenceClass::Patent) => InputReference::from_known(ClassExtension::Patent, value),
324        Some(ReferenceClass::Dataset) => InputReference::from_known(ClassExtension::Dataset, value),
325        Some(ReferenceClass::Standard) => {
326            InputReference::from_known(ClassExtension::Standard, value)
327        }
328        Some(ReferenceClass::Software) => {
329            InputReference::from_known(ClassExtension::Software, value)
330        }
331        Some(ReferenceClass::Event) => InputReference::from_known(ClassExtension::Event, value),
332        Some(ReferenceClass::AudioVisual) => {
333            InputReference::from_known(ClassExtension::AudioVisual, value)
334        }
335        Some(ReferenceClass::Unknown(_)) | None => {
336            let fields = if let JsonValue::Object(fields) = value {
337                fields
338            } else {
339                JsonMap::new()
340            };
341            Ok(InputReference::Unknown(Box::new(UnknownClassData {
342                class: class.to_string(),
343                fields,
344            })))
345        }
346    }
347}