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