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}
155
156impl FlatName {
157    pub fn family_or_literal(&self) -> &str {
158        if let Some(ref f) = self.family {
159            f
160        } else if let Some(ref l) = self.literal {
161            l
162        } else {
163            ""
164        }
165    }
166}
167
168impl fmt::Display for Contributor {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        match self {
171            Contributor::SimpleName(n) => write!(f, "{}", n.name),
172            Contributor::StructuredName(n) => write!(f, "{} {}", n.given, n.family),
173            Contributor::Multilingual(m) => write!(f, "{} {}", m.original.given, m.original.family),
174            Contributor::ContributorList(l) => write!(f, "{}", l),
175        }
176    }
177}
178
179impl fmt::Display for ContributorList {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        let names: Vec<String> = self.0.iter().map(|c| c.to_string()).collect();
182        write!(f, "{}", names.join(", "))
183    }
184}
185
186crate::tolerant_enum! {
187    /// A contributor role for use in the unified contributors list.
188    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
189    pub enum ContributorRole {
190        Author = "author",
191        Editor = "editor",
192        Translator = "translator",
193        Director = "director",
194        Performer = "performer",
195        Composer = "composer",
196        Illustrator = "illustrator",
197        Narrator = "narrator",
198        Host = "host",
199        Guest = "guest",
200        Interviewer = "interviewer",
201        Recipient = "recipient",
202        Compiler = "compiler",
203        Producer = "producer",
204        Writer = "writer"
205    }
206}
207
208/// A single entry in a reference's contributors list.
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
210#[cfg_attr(feature = "schema", derive(JsonSchema))]
211#[cfg_attr(feature = "bindings", derive(Type))]
212#[serde(rename_all = "kebab-case")]
213pub struct ContributorEntry {
214    /// The role this contributor plays in relation to the work.
215    pub role: ContributorRole,
216    /// The contributor (name, organization, or list).
217    pub contributor: Contributor,
218    /// The grammatical gender used for role-label agreement.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub gender: Option<ContributorGender>,
221}