Skip to main content

citum_schema_data/reference/
contributor.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6use crate::reference::types::{MultilingualString, Place};
7#[cfg(feature = "schema")]
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10#[cfg(feature = "bindings")]
11use specta::Type;
12use std::fmt;
13
14/// Grammatical gender carried on contributor records for role-label agreement.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[cfg_attr(feature = "schema", derive(JsonSchema))]
17#[cfg_attr(feature = "bindings", derive(Type))]
18#[serde(rename_all = "kebab-case")]
19pub enum ContributorGender {
20    /// Masculine grammatical gender.
21    Masculine,
22    /// Feminine grammatical gender.
23    Feminine,
24    /// Neuter grammatical gender.
25    Neuter,
26    /// Common or shared grammatical gender.
27    Common,
28}
29
30/// A contributor can be a single string, a structured name, or a list of contributors.
31#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
32#[cfg_attr(feature = "schema", derive(JsonSchema))]
33#[cfg_attr(feature = "bindings", derive(Type))]
34#[serde(untagged)]
35pub enum Contributor {
36    SimpleName(SimpleName),
37    StructuredName(StructuredName),
38    Multilingual(MultilingualName),
39    ContributorList(ContributorList),
40}
41
42/// Holistic multilingual name representation.
43#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
44#[cfg_attr(feature = "schema", derive(JsonSchema))]
45#[cfg_attr(feature = "bindings", derive(Type))]
46#[serde(rename_all = "kebab-case")]
47pub struct MultilingualName {
48    /// The name in its original script.
49    pub original: StructuredName,
50    /// ISO 639/BCP 47 language code for the original name.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub lang: Option<crate::reference::types::LangID>,
53    /// Transliterations/Transcriptions of the name.
54    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
55    pub transliterations: std::collections::HashMap<String, StructuredName>,
56    /// Translations of the name.
57    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
58    pub translations: std::collections::HashMap<crate::reference::types::LangID, StructuredName>,
59}
60
61/// A simple name is just a string, with an optional location.
62#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
63#[cfg_attr(feature = "schema", derive(JsonSchema))]
64#[cfg_attr(feature = "bindings", derive(Type))]
65pub struct SimpleName {
66    /// Institutional or organization name.
67    pub name: MultilingualString,
68    /// Geographic place associated with the name.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub location: Option<Place>,
71    /// Short form of the name (e.g., abbreviation or shortened form).
72    #[serde(rename = "short-name", skip_serializing_if = "Option::is_none")]
73    pub short_name: Option<String>,
74}
75
76/// A structured name is a name broken down into its constituent parts.
77#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq)]
78#[cfg_attr(feature = "schema", derive(JsonSchema))]
79#[cfg_attr(feature = "bindings", derive(Type))]
80#[serde(rename_all = "kebab-case")]
81pub struct StructuredName {
82    pub given: MultilingualString,
83    pub family: MultilingualString,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub suffix: Option<String>,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub dropping_particle: Option<String>,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub non_dropping_particle: Option<String>,
90}
91
92/// A list of contributors.
93#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
94#[cfg_attr(feature = "schema", derive(JsonSchema))]
95#[cfg_attr(feature = "bindings", derive(Type))]
96pub struct ContributorList(pub Vec<Contributor>);
97
98impl Contributor {
99    pub fn to_names_vec(&self) -> Vec<FlatName> {
100        match self {
101            Contributor::SimpleName(n) => vec![FlatName {
102                literal: Some(n.name.to_string()),
103                short_name: n.short_name.clone(),
104                ..Default::default()
105            }],
106            Contributor::StructuredName(n) => vec![FlatName {
107                given: Some(n.given.to_string()),
108                family: Some(n.family.to_string()),
109                suffix: n.suffix.clone(),
110                dropping_particle: n.dropping_particle.clone(),
111                non_dropping_particle: n.non_dropping_particle.clone(),
112                ..Default::default()
113            }],
114            Contributor::Multilingual(m) => vec![FlatName {
115                given: Some(m.original.given.to_string()),
116                family: Some(m.original.family.to_string()),
117                suffix: m.original.suffix.clone(),
118                dropping_particle: m.original.dropping_particle.clone(),
119                non_dropping_particle: m.original.non_dropping_particle.clone(),
120                ..Default::default()
121            }],
122            Contributor::ContributorList(l) => l.0.iter().flat_map(|c| c.to_names_vec()).collect(),
123        }
124    }
125
126    pub fn name(&self) -> Option<String> {
127        match self {
128            Contributor::SimpleName(n) => Some(n.name.to_string()),
129            Contributor::Multilingual(m) => {
130                Some(format!("{} {}", m.original.given, m.original.family))
131            }
132            _ => None,
133        }
134    }
135
136    pub fn location(&self) -> Option<String> {
137        match self {
138            Contributor::SimpleName(n) => n.location.clone().map(Into::into),
139            _ => None,
140        }
141    }
142}
143
144/// A flattened name for internal processing.
145#[derive(Debug, Clone, Default, PartialEq, Eq)]
146pub struct FlatName {
147    pub family: Option<String>,
148    pub given: Option<String>,
149    pub suffix: Option<String>,
150    pub dropping_particle: Option<String>,
151    pub non_dropping_particle: Option<String>,
152    pub literal: Option<String>,
153    pub short_name: Option<String>,
154    /// Original-script display form of a multilingual name (e.g. `华林甫`),
155    /// carried alongside the selected transliteration so rendering can apply
156    /// native ordering and append the original script after the romanized
157    /// name when a name pattern requests both views.
158    pub original_script: Option<String>,
159}
160
161impl FlatName {
162    pub fn family_or_literal(&self) -> &str {
163        if let Some(ref f) = self.family {
164            f
165        } else if let Some(ref l) = self.literal {
166            l
167        } else {
168            ""
169        }
170    }
171}
172
173impl fmt::Display for Contributor {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        match self {
176            Contributor::SimpleName(n) => write!(f, "{}", n.name),
177            Contributor::StructuredName(n) => write!(f, "{} {}", n.given, n.family),
178            Contributor::Multilingual(m) => write!(f, "{} {}", m.original.given, m.original.family),
179            Contributor::ContributorList(l) => write!(f, "{}", l),
180        }
181    }
182}
183
184impl fmt::Display for ContributorList {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        let names: Vec<String> = self.0.iter().map(|c| c.to_string()).collect();
187        write!(f, "{}", names.join(", "))
188    }
189}
190
191crate::tolerant_enum! {
192    /// A contributor role for use in the unified contributors list.
193    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
194    pub enum ContributorRole {
195        Author = "author",
196        Editor = "editor",
197        Translator = "translator",
198        Director = "director",
199        Performer = "performer",
200        Composer = "composer",
201        Illustrator = "illustrator",
202        Narrator = "narrator",
203        Host = "host",
204        Guest = "guest",
205        Interviewer = "interviewer",
206        Recipient = "recipient",
207        Compiler = "compiler",
208        Producer = "producer",
209        Writer = "writer"
210    }
211}
212
213/// A single entry in a reference's contributors list.
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
215#[cfg_attr(feature = "schema", derive(JsonSchema))]
216#[cfg_attr(feature = "bindings", derive(Type))]
217#[serde(rename_all = "kebab-case")]
218pub struct ContributorEntry {
219    /// The role this contributor plays in relation to the work.
220    pub role: ContributorRole,
221    /// The contributor (name, organization, or list).
222    pub contributor: Contributor,
223    /// The grammatical gender used for role-label agreement.
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub gender: Option<ContributorGender>,
226}