1use 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#[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,
22 Feminine,
24 Neuter,
26 Common,
28}
29
30#[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#[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 pub original: StructuredName,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub lang: Option<crate::reference::types::LangID>,
53 #[serde(rename = "sort-as", skip_serializing_if = "Option::is_none")]
55 pub sort_as: Option<String>,
56 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
58 pub transliterations: std::collections::HashMap<String, StructuredName>,
59 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
61 pub translations: std::collections::HashMap<crate::reference::types::LangID, StructuredName>,
62}
63
64#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
66#[cfg_attr(feature = "schema", derive(JsonSchema))]
67#[cfg_attr(feature = "bindings", derive(Type))]
68pub struct SimpleName {
69 pub name: MultilingualString,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub location: Option<Place>,
74 #[serde(rename = "short-name", skip_serializing_if = "Option::is_none")]
76 pub short_name: Option<String>,
77}
78
79#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq)]
81#[cfg_attr(feature = "schema", derive(JsonSchema))]
82#[cfg_attr(feature = "bindings", derive(Type))]
83#[serde(rename_all = "kebab-case")]
84pub struct StructuredName {
85 pub given: MultilingualString,
86 pub family: MultilingualString,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub suffix: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub dropping_particle: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub non_dropping_particle: Option<String>,
93}
94
95#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
97#[cfg_attr(feature = "schema", derive(JsonSchema))]
98#[cfg_attr(feature = "bindings", derive(Type))]
99pub struct ContributorList(pub Vec<Contributor>);
100
101impl Contributor {
102 pub fn to_names_vec(&self) -> Vec<FlatName> {
103 match self {
104 Contributor::SimpleName(n) => vec![FlatName {
105 literal: Some(n.name.to_string()),
106 short_name: n.short_name.clone(),
107 ..Default::default()
108 }],
109 Contributor::StructuredName(n) => vec![FlatName {
110 given: Some(n.given.to_string()),
111 family: Some(n.family.to_string()),
112 suffix: n.suffix.clone(),
113 dropping_particle: n.dropping_particle.clone(),
114 non_dropping_particle: n.non_dropping_particle.clone(),
115 ..Default::default()
116 }],
117 Contributor::Multilingual(m) => vec![FlatName {
118 given: Some(m.original.given.to_string()),
119 family: Some(m.original.family.to_string()),
120 suffix: m.original.suffix.clone(),
121 dropping_particle: m.original.dropping_particle.clone(),
122 non_dropping_particle: m.original.non_dropping_particle.clone(),
123 ..Default::default()
124 }],
125 Contributor::ContributorList(l) => l.0.iter().flat_map(|c| c.to_names_vec()).collect(),
126 }
127 }
128
129 pub fn name(&self) -> Option<String> {
130 match self {
131 Contributor::SimpleName(n) => Some(n.name.to_string()),
132 Contributor::Multilingual(m) => {
133 Some(format!("{} {}", m.original.given, m.original.family))
134 }
135 _ => None,
136 }
137 }
138
139 pub fn location(&self) -> Option<String> {
140 match self {
141 Contributor::SimpleName(n) => n.location.clone().map(Into::into),
142 _ => None,
143 }
144 }
145}
146
147#[derive(Debug, Clone, Default, PartialEq, Eq)]
149pub struct FlatName {
150 pub family: Option<String>,
151 pub given: Option<String>,
152 pub suffix: Option<String>,
153 pub dropping_particle: Option<String>,
154 pub non_dropping_particle: Option<String>,
155 pub literal: Option<String>,
156 pub short_name: Option<String>,
157 pub original_script: Option<String>,
162}
163
164impl FlatName {
165 pub fn family_or_literal(&self) -> &str {
166 if let Some(ref f) = self.family {
167 f
168 } else if let Some(ref l) = self.literal {
169 l
170 } else {
171 ""
172 }
173 }
174}
175
176impl fmt::Display for Contributor {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 match self {
179 Contributor::SimpleName(n) => write!(f, "{}", n.name),
180 Contributor::StructuredName(n) => write!(f, "{} {}", n.given, n.family),
181 Contributor::Multilingual(m) => write!(f, "{} {}", m.original.given, m.original.family),
182 Contributor::ContributorList(l) => write!(f, "{}", l),
183 }
184 }
185}
186
187impl fmt::Display for ContributorList {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 let names: Vec<String> = self.0.iter().map(|c| c.to_string()).collect();
190 write!(f, "{}", names.join(", "))
191 }
192}
193
194crate::tolerant_enum! {
195 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
197 pub enum ContributorRole {
198 Author = "author",
199 Editor = "editor",
200 Translator = "translator",
201 Director = "director",
202 Performer = "performer",
203 Composer = "composer",
204 Illustrator = "illustrator",
205 Narrator = "narrator",
206 Host = "host",
207 Guest = "guest",
208 Interviewer = "interviewer",
209 Recipient = "recipient",
210 Compiler = "compiler",
211 Producer = "producer",
212 Writer = "writer"
213 }
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
218#[cfg_attr(feature = "schema", derive(JsonSchema))]
219#[cfg_attr(feature = "bindings", derive(Type))]
220#[serde(transparent)]
221pub struct ContributorRoles(
222 #[cfg_attr(feature = "schema", schemars(length(min = 1)))] Vec<ContributorRole>,
223);
224
225impl ContributorRoles {
226 #[must_use]
228 pub fn as_slice(&self) -> &[ContributorRole] {
229 &self.0
230 }
231
232 #[must_use]
234 pub fn contains(&self, role: &ContributorRole) -> bool {
235 self.0.contains(role)
236 }
237
238 #[cfg(feature = "legacy-convert")]
240 pub(crate) fn insert(&mut self, role: ContributorRole) {
241 if !self.contains(&role) {
242 self.0.push(role);
243 }
244 }
245}
246
247impl From<ContributorRole> for ContributorRoles {
248 fn from(role: ContributorRole) -> Self {
249 Self(vec![role])
250 }
251}
252
253impl PartialEq<ContributorRole> for ContributorRoles {
254 fn eq(&self, other: &ContributorRole) -> bool {
255 self.as_slice() == std::slice::from_ref(other)
256 }
257}
258
259impl TryFrom<Vec<ContributorRole>> for ContributorRoles {
260 type Error = &'static str;
261
262 fn try_from(roles: Vec<ContributorRole>) -> Result<Self, Self::Error> {
263 if roles.is_empty() {
264 return Err("contributor roles must not be empty");
265 }
266 let mut distinct = Vec::with_capacity(roles.len());
267 for role in &roles {
268 if distinct.contains(role) {
269 return Err("contributor roles must be distinct");
270 }
271 distinct.push(role.clone());
272 }
273 Ok(Self(roles))
274 }
275}
276
277impl<'de> Deserialize<'de> for ContributorRoles {
278 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
279 where
280 D: serde::Deserializer<'de>,
281 {
282 #[derive(Deserialize)]
283 #[serde(untagged)]
284 enum AuthoredRoles {
285 Single(ContributorRole),
286 Multiple(Vec<ContributorRole>),
287 }
288
289 let roles = match AuthoredRoles::deserialize(deserializer)? {
290 AuthoredRoles::Single(role) => vec![role],
291 AuthoredRoles::Multiple(roles) => roles,
292 };
293 Self::try_from(roles).map_err(serde::de::Error::custom)
294 }
295}
296
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299#[cfg_attr(feature = "schema", derive(JsonSchema))]
300#[cfg_attr(feature = "bindings", derive(Type))]
301#[serde(rename_all = "kebab-case")]
302pub struct ContributorEntry {
303 #[serde(rename = "roles", alias = "role")]
305 pub roles: ContributorRoles,
306 pub contributor: Contributor,
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub gender: Option<ContributorGender>,
311}