citum_schema_data/reference/
input.rs1use 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
22pub(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#[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 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 #[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#[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 #[must_use]
129 pub fn new() -> Self {
130 Self::default()
131 }
132
133 #[must_use]
135 pub fn get(&self, name: &str) -> Option<&str> {
136 self.0.get(name).map(String::as_str)
137 }
138
139 pub fn insert(&mut self, name: IdentifierName, value: impl Into<String>) -> Option<String> {
141 self.0.insert(name, value.into())
142 }
143
144 #[must_use]
146 pub fn is_empty(&self) -> bool {
147 self.0.is_empty()
148 }
149}
150
151#[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#[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 pub class: String,
171 #[cfg_attr(feature = "bindings", specta(type = serde_json::Value))]
173 pub fields: JsonMap<String, JsonValue>,
174}