Skip to main content

citum_schema_data/reference/
input.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Public `InputReference` container and unknown-class payload.
7
8use std::borrow::Borrow;
9use std::collections::BTreeMap;
10use std::str::FromStr;
11use std::sync::LazyLock;
12
13use serde::{Deserialize, Serialize};
14use serde_json::{Map as JsonMap, Value as JsonValue};
15
16#[cfg(feature = "bindings")]
17use specta::Type;
18
19use super::classes::ClassExtension;
20use super::types::common::FieldLanguageMap;
21
22/// Empty field-language map returned by accessors on unknown-class references.
23///
24/// `FieldLanguageMap` is a `HashMap`, whose `::new()` is not `const`, so a
25/// `LazyLock` is required. The map is constructed once for the process and
26/// reused by every unknown-class reference.
27pub(crate) static EMPTY_FIELD_LANGUAGES: LazyLock<FieldLanguageMap> =
28    LazyLock::new(FieldLanguageMap::new);
29
30const RESERVED_IDENTIFIER_NAMES: &[&str] = &[
31    "ads-bibcode",
32    "doi",
33    "docket-number",
34    "eprint-id",
35    "isbn",
36    "issn",
37    "patent-number",
38    "pmcid",
39    "pmid",
40    "report-number",
41    "standard-number",
42    "url",
43];
44
45/// A validated name for a supplementary standardized identifier.
46///
47/// Names use lowercase kebab-case. Identifiers with dedicated Citum fields,
48/// such as `doi` and `isbn`, are reserved and cannot be duplicated here.
49#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
50#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
51#[cfg_attr(feature = "bindings", derive(Type))]
52#[serde(transparent)]
53pub struct IdentifierName(String);
54
55impl IdentifierName {
56    /// Validate and construct a supplementary identifier name.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error when the name is not lowercase kebab-case, does not
61    /// begin with a letter, or is reserved for a first-class reference field.
62    pub fn new(value: impl Into<String>) -> Result<Self, String> {
63        let value = value.into();
64        let valid = !value.is_empty()
65            && value.split('-').all(|segment| {
66                !segment.is_empty()
67                    && segment
68                        .chars()
69                        .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit())
70            })
71            && value
72                .chars()
73                .next()
74                .is_some_and(|ch| ch.is_ascii_lowercase());
75        if !valid {
76            return Err(
77                "identifier name must be lowercase kebab-case and begin with a letter".to_string(),
78            );
79        }
80        if RESERVED_IDENTIFIER_NAMES.contains(&value.as_str()) {
81            return Err(format!(
82                "identifier name `{value}` is reserved for a first-class reference field"
83            ));
84        }
85        Ok(Self(value))
86    }
87
88    /// Return the validated wire-format name.
89    #[must_use]
90    pub fn as_str(&self) -> &str {
91        &self.0
92    }
93}
94
95impl FromStr for IdentifierName {
96    type Err = String;
97
98    fn from_str(value: &str) -> Result<Self, Self::Err> {
99        Self::new(value)
100    }
101}
102
103impl Borrow<str> for IdentifierName {
104    fn borrow(&self) -> &str {
105        self.as_str()
106    }
107}
108
109impl<'de> Deserialize<'de> for IdentifierName {
110    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111    where
112        D: serde::Deserializer<'de>,
113    {
114        let value = String::deserialize(deserializer)?;
115        Self::new(value).map_err(serde::de::Error::custom)
116    }
117}
118
119/// Extensible standardized identifiers without dedicated Citum fields.
120#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
121#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
122#[cfg_attr(feature = "bindings", derive(Type))]
123#[serde(transparent)]
124pub struct SupplementaryIdentifiers(BTreeMap<IdentifierName, String>);
125
126impl SupplementaryIdentifiers {
127    /// Construct an empty supplementary identifier map.
128    #[must_use]
129    pub fn new() -> Self {
130        Self::default()
131    }
132
133    /// Return the value associated with a validated identifier name.
134    #[must_use]
135    pub fn get(&self, name: &str) -> Option<&str> {
136        self.0.get(name).map(String::as_str)
137    }
138
139    /// Insert a supplementary identifier value.
140    pub fn insert(&mut self, name: IdentifierName, value: impl Into<String>) -> Option<String> {
141        self.0.insert(name, value.into())
142    }
143
144    /// Return whether the map has no identifiers.
145    #[must_use]
146    pub fn is_empty(&self) -> bool {
147        self.0.is_empty()
148    }
149}
150
151/// The Reference model: a class-specific overlay reachable through accessor methods.
152///
153/// All shared bibliographic data (id, title, contributors, dates, publisher, ...)
154/// lives inside the class-specific payload in `extension`. The accessor methods
155/// (`id()`, `title()`, etc.) dispatch through the extension and are the public
156/// read path; the typed setters (`set_id`, ...) are the public mutation path.
157#[derive(Debug, Clone, PartialEq)]
158#[cfg_attr(feature = "bindings", derive(Type))]
159pub struct InputReference {
160    pub(crate) extension: ClassExtension,
161    pub(crate) identifiers: SupplementaryIdentifiers,
162}
163
164/// Unknown reference-class payload captured by the discriminator dispatcher.
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
167#[cfg_attr(feature = "bindings", derive(Type))]
168pub struct UnknownClassData {
169    /// Raw `class:` string from the input object.
170    pub class: String,
171    /// Non-shared fields captured verbatim for round-trip preservation.
172    #[cfg_attr(feature = "bindings", specta(type = serde_json::Value))]
173    pub fields: JsonMap<String, JsonValue>,
174}