1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct DublinCore {
10 pub version: String,
12
13 pub terms: DublinCoreTerms,
15}
16
17impl DublinCore {
18 #[must_use]
20 pub fn new(title: impl Into<String>, creator: impl Into<String>) -> Self {
21 Self {
22 version: "1.1".to_string(),
23 terms: DublinCoreTerms {
24 title: title.into(),
25 creator: StringOrArray::Single(creator.into()),
26 subject: None,
27 description: None,
28 publisher: None,
29 contributor: None,
30 date: None,
31 dc_type: None,
32 format: None,
33 identifier: None,
34 source: None,
35 language: None,
36 relation: None,
37 coverage: None,
38 rights: None,
39 },
40 }
41 }
42
43 #[must_use]
45 pub fn title(&self) -> &str {
46 &self.terms.title
47 }
48
49 #[must_use]
51 pub fn creators(&self) -> Vec<&str> {
52 self.terms.creator.as_slice()
53 }
54
55 #[must_use]
57 pub fn description(&self) -> Option<&str> {
58 self.terms.description.as_deref()
59 }
60
61 #[must_use]
63 pub fn language(&self) -> Option<&str> {
64 self.terms.language.as_deref()
65 }
66
67 #[must_use]
69 pub fn subjects(&self) -> Vec<&str> {
70 self.terms
71 .subject
72 .as_ref()
73 .map_or_else(Vec::new, StringOrArray::as_slice)
74 }
75
76 #[must_use]
78 pub fn publisher(&self) -> Option<&str> {
79 self.terms.publisher.as_deref()
80 }
81
82 #[must_use]
84 pub fn contributors(&self) -> Vec<&str> {
85 self.terms
86 .contributor
87 .as_ref()
88 .map_or_else(Vec::new, StringOrArray::as_slice)
89 }
90
91 #[must_use]
93 pub fn date(&self) -> Option<&str> {
94 self.terms.date.as_deref()
95 }
96
97 #[must_use]
99 pub fn dc_type(&self) -> Option<&str> {
100 self.terms.dc_type.as_deref()
101 }
102
103 #[must_use]
105 pub fn format(&self) -> Option<&str> {
106 self.terms.format.as_deref()
107 }
108
109 #[must_use]
111 pub fn identifier(&self) -> Option<&str> {
112 self.terms.identifier.as_deref()
113 }
114
115 #[must_use]
117 pub fn source(&self) -> Option<&str> {
118 self.terms.source.as_deref()
119 }
120
121 #[must_use]
123 pub fn relation(&self) -> Option<&str> {
124 self.terms.relation.as_deref()
125 }
126
127 #[must_use]
129 pub fn coverage(&self) -> Option<&str> {
130 self.terms.coverage.as_deref()
131 }
132
133 #[must_use]
135 pub fn rights(&self) -> Option<&str> {
136 self.terms.rights.as_deref()
137 }
138
139 pub fn set_title(&mut self, title: impl Into<String>) {
141 self.terms.title = title.into();
142 }
143
144 pub fn set_creators(&mut self, creators: Vec<String>) {
146 self.terms.creator = match creators.len() {
147 1 => StringOrArray::Single(creators.into_iter().next().unwrap_or_default()),
148 _ => StringOrArray::Multiple(creators),
149 };
150 }
151
152 pub fn set_description(&mut self, description: Option<String>) {
154 self.terms.description = description;
155 }
156
157 pub fn set_subjects(&mut self, subjects: Vec<String>) {
159 self.terms.subject = match subjects.len() {
160 0 => None,
161 1 => Some(StringOrArray::Single(
162 subjects.into_iter().next().unwrap_or_default(),
163 )),
164 _ => Some(StringOrArray::Multiple(subjects)),
165 };
166 }
167
168 pub fn set_publisher(&mut self, publisher: Option<String>) {
170 self.terms.publisher = publisher;
171 }
172
173 pub fn set_language(&mut self, language: Option<String>) {
175 self.terms.language = language;
176 }
177
178 pub fn set_rights(&mut self, rights: Option<String>) {
180 self.terms.rights = rights;
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188pub struct DublinCoreTerms {
189 pub title: String,
191
192 pub creator: StringOrArray,
194
195 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub subject: Option<StringOrArray>,
198
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub description: Option<String>,
202
203 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub publisher: Option<String>,
206
207 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub contributor: Option<StringOrArray>,
210
211 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub date: Option<String>,
214
215 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
217 pub dc_type: Option<String>,
218
219 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub format: Option<String>,
222
223 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub identifier: Option<String>,
226
227 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub source: Option<String>,
230
231 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub language: Option<String>,
234
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub relation: Option<String>,
238
239 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub coverage: Option<String>,
242
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub rights: Option<String>,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(untagged)]
253pub enum StringOrArray {
254 Single(String),
256 Multiple(Vec<String>),
258}
259
260impl StringOrArray {
261 #[must_use]
263 pub fn as_slice(&self) -> Vec<&str> {
264 match self {
265 Self::Single(s) => vec![s.as_str()],
266 Self::Multiple(v) => v.iter().map(String::as_str).collect(),
267 }
268 }
269
270 #[must_use]
272 pub fn first(&self) -> &str {
273 match self {
274 Self::Single(s) => s,
275 Self::Multiple(v) => v.first().map_or("", String::as_str),
276 }
277 }
278
279 #[must_use]
281 pub fn is_empty(&self) -> bool {
282 match self {
283 Self::Single(s) => s.is_empty(),
284 Self::Multiple(v) => v.is_empty(),
285 }
286 }
287}
288
289impl From<String> for StringOrArray {
290 fn from(s: String) -> Self {
291 Self::Single(s)
292 }
293}
294
295impl From<&str> for StringOrArray {
296 fn from(s: &str) -> Self {
297 Self::Single(s.to_string())
298 }
299}
300
301impl From<Vec<String>> for StringOrArray {
302 fn from(v: Vec<String>) -> Self {
303 Self::Multiple(v)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_dublin_core_new() {
313 let dc = DublinCore::new("Test Document", "Author Name");
314 assert_eq!(dc.title(), "Test Document");
315 assert_eq!(dc.creators(), vec!["Author Name"]);
316 assert_eq!(dc.version, "1.1");
317 }
318
319 #[test]
320 fn test_string_or_array() {
321 let single = StringOrArray::Single("one".to_string());
322 assert_eq!(single.as_slice(), vec!["one"]);
323 assert_eq!(single.first(), "one");
324
325 let multiple = StringOrArray::Multiple(vec!["one".to_string(), "two".to_string()]);
326 assert_eq!(multiple.as_slice(), vec!["one", "two"]);
327 assert_eq!(multiple.first(), "one");
328 }
329
330 #[test]
331 fn test_serialization() {
332 let dc = DublinCore::new("Test", "Author");
333 let json = serde_json::to_string_pretty(&dc).unwrap();
334 assert!(json.contains("\"title\": \"Test\""));
335 assert!(json.contains("\"creator\": \"Author\""));
336 }
337
338 #[test]
339 fn test_deserialization_single_creator() {
340 let json = r#"{
341 "version": "1.1",
342 "terms": {
343 "title": "My Document",
344 "creator": "John Doe"
345 }
346 }"#;
347 let dc: DublinCore = serde_json::from_str(json).unwrap();
348 assert_eq!(dc.title(), "My Document");
349 assert_eq!(dc.creators(), vec!["John Doe"]);
350 }
351
352 #[test]
353 fn test_deserialization_multiple_creators() {
354 let json = r#"{
355 "version": "1.1",
356 "terms": {
357 "title": "Collaboration",
358 "creator": ["Alice", "Bob", "Charlie"],
359 "subject": ["Science", "Research"]
360 }
361 }"#;
362 let dc: DublinCore = serde_json::from_str(json).unwrap();
363 assert_eq!(dc.creators(), vec!["Alice", "Bob", "Charlie"]);
364 assert_eq!(
365 dc.terms.subject.as_ref().unwrap().as_slice(),
366 vec!["Science", "Research"]
367 );
368 }
369
370 #[test]
371 fn test_full_dublin_core() {
372 let json = r#"{
373 "version": "1.1",
374 "terms": {
375 "title": "Annual Report 2025",
376 "creator": ["Jane Doe", "John Smith"],
377 "subject": ["Finance", "Annual Report"],
378 "description": "Comprehensive annual financial report",
379 "publisher": "Acme Corporation",
380 "contributor": "Finance Team",
381 "date": "2025-01-15",
382 "type": "Text",
383 "format": "application/vnd.codex+json",
384 "identifier": "sha256:3a7bd3e2",
385 "language": "en",
386 "coverage": "2024 fiscal year",
387 "rights": "Copyright 2025 Acme Corporation"
388 }
389 }"#;
390 let dc: DublinCore = serde_json::from_str(json).unwrap();
391 assert_eq!(dc.title(), "Annual Report 2025");
392 assert_eq!(
393 dc.description(),
394 Some("Comprehensive annual financial report")
395 );
396 assert_eq!(dc.language(), Some("en"));
397 }
398}