use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DublinCore {
pub version: String,
pub terms: DublinCoreTerms,
}
impl DublinCore {
#[must_use]
pub fn new(title: impl Into<String>, creator: impl Into<String>) -> Self {
Self {
version: "1.1".to_string(),
terms: DublinCoreTerms {
title: title.into(),
creator: StringOrArray::Single(creator.into()),
subject: None,
description: None,
publisher: None,
contributor: None,
date: None,
dc_type: None,
format: None,
identifier: None,
source: None,
language: None,
relation: None,
coverage: None,
rights: None,
},
}
}
#[must_use]
pub fn title(&self) -> &str {
&self.terms.title
}
#[must_use]
pub fn creators(&self) -> Vec<&str> {
self.terms.creator.as_slice()
}
#[must_use]
pub fn description(&self) -> Option<&str> {
self.terms.description.as_deref()
}
#[must_use]
pub fn language(&self) -> Option<&str> {
self.terms.language.as_deref()
}
#[must_use]
pub fn subjects(&self) -> Vec<&str> {
self.terms
.subject
.as_ref()
.map_or_else(Vec::new, StringOrArray::as_slice)
}
#[must_use]
pub fn publisher(&self) -> Option<&str> {
self.terms.publisher.as_deref()
}
#[must_use]
pub fn contributors(&self) -> Vec<&str> {
self.terms
.contributor
.as_ref()
.map_or_else(Vec::new, StringOrArray::as_slice)
}
#[must_use]
pub fn date(&self) -> Option<&str> {
self.terms.date.as_deref()
}
#[must_use]
pub fn dc_type(&self) -> Option<&str> {
self.terms.dc_type.as_deref()
}
#[must_use]
pub fn format(&self) -> Option<&str> {
self.terms.format.as_deref()
}
#[must_use]
pub fn identifier(&self) -> Option<&str> {
self.terms.identifier.as_deref()
}
#[must_use]
pub fn source(&self) -> Option<&str> {
self.terms.source.as_deref()
}
#[must_use]
pub fn relation(&self) -> Option<&str> {
self.terms.relation.as_deref()
}
#[must_use]
pub fn coverage(&self) -> Option<&str> {
self.terms.coverage.as_deref()
}
#[must_use]
pub fn rights(&self) -> Option<&str> {
self.terms.rights.as_deref()
}
pub fn set_title(&mut self, title: impl Into<String>) {
self.terms.title = title.into();
}
pub fn set_creators(&mut self, creators: Vec<String>) {
self.terms.creator = match creators.len() {
1 => StringOrArray::Single(creators.into_iter().next().unwrap_or_default()),
_ => StringOrArray::Multiple(creators),
};
}
pub fn set_description(&mut self, description: Option<String>) {
self.terms.description = description;
}
pub fn set_subjects(&mut self, subjects: Vec<String>) {
self.terms.subject = match subjects.len() {
0 => None,
1 => Some(StringOrArray::Single(
subjects.into_iter().next().unwrap_or_default(),
)),
_ => Some(StringOrArray::Multiple(subjects)),
};
}
pub fn set_publisher(&mut self, publisher: Option<String>) {
self.terms.publisher = publisher;
}
pub fn set_language(&mut self, language: Option<String>) {
self.terms.language = language;
}
pub fn set_rights(&mut self, rights: Option<String>) {
self.terms.rights = rights;
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DublinCoreTerms {
pub title: String,
pub creator: StringOrArray,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<StringOrArray>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub publisher: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contributor: Option<StringOrArray>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date: Option<String>,
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub dc_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identifier: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coverage: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rights: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StringOrArray {
Single(String),
Multiple(Vec<String>),
}
impl StringOrArray {
#[must_use]
pub fn as_slice(&self) -> Vec<&str> {
match self {
Self::Single(s) => vec![s.as_str()],
Self::Multiple(v) => v.iter().map(String::as_str).collect(),
}
}
#[must_use]
pub fn first(&self) -> &str {
match self {
Self::Single(s) => s,
Self::Multiple(v) => v.first().map_or("", String::as_str),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
match self {
Self::Single(s) => s.is_empty(),
Self::Multiple(v) => v.is_empty(),
}
}
}
impl From<String> for StringOrArray {
fn from(s: String) -> Self {
Self::Single(s)
}
}
impl From<&str> for StringOrArray {
fn from(s: &str) -> Self {
Self::Single(s.to_string())
}
}
impl From<Vec<String>> for StringOrArray {
fn from(v: Vec<String>) -> Self {
Self::Multiple(v)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dublin_core_new() {
let dc = DublinCore::new("Test Document", "Author Name");
assert_eq!(dc.title(), "Test Document");
assert_eq!(dc.creators(), vec!["Author Name"]);
assert_eq!(dc.version, "1.1");
}
#[test]
fn test_string_or_array() {
let single = StringOrArray::Single("one".to_string());
assert_eq!(single.as_slice(), vec!["one"]);
assert_eq!(single.first(), "one");
let multiple = StringOrArray::Multiple(vec!["one".to_string(), "two".to_string()]);
assert_eq!(multiple.as_slice(), vec!["one", "two"]);
assert_eq!(multiple.first(), "one");
}
#[test]
fn test_serialization() {
let dc = DublinCore::new("Test", "Author");
let json = serde_json::to_string_pretty(&dc).unwrap();
assert!(json.contains("\"title\": \"Test\""));
assert!(json.contains("\"creator\": \"Author\""));
}
#[test]
fn test_deserialization_single_creator() {
let json = r#"{
"version": "1.1",
"terms": {
"title": "My Document",
"creator": "John Doe"
}
}"#;
let dc: DublinCore = serde_json::from_str(json).unwrap();
assert_eq!(dc.title(), "My Document");
assert_eq!(dc.creators(), vec!["John Doe"]);
}
#[test]
fn test_deserialization_multiple_creators() {
let json = r#"{
"version": "1.1",
"terms": {
"title": "Collaboration",
"creator": ["Alice", "Bob", "Charlie"],
"subject": ["Science", "Research"]
}
}"#;
let dc: DublinCore = serde_json::from_str(json).unwrap();
assert_eq!(dc.creators(), vec!["Alice", "Bob", "Charlie"]);
assert_eq!(
dc.terms.subject.as_ref().unwrap().as_slice(),
vec!["Science", "Research"]
);
}
#[test]
fn test_full_dublin_core() {
let json = r#"{
"version": "1.1",
"terms": {
"title": "Annual Report 2025",
"creator": ["Jane Doe", "John Smith"],
"subject": ["Finance", "Annual Report"],
"description": "Comprehensive annual financial report",
"publisher": "Acme Corporation",
"contributor": "Finance Team",
"date": "2025-01-15",
"type": "Text",
"format": "application/vnd.codex+json",
"identifier": "sha256:3a7bd3e2",
"language": "en",
"coverage": "2024 fiscal year",
"rights": "Copyright 2025 Acme Corporation"
}
}"#;
let dc: DublinCore = serde_json::from_str(json).unwrap();
assert_eq!(dc.title(), "Annual Report 2025");
assert_eq!(
dc.description(),
Some("Comprehensive annual financial report")
);
assert_eq!(dc.language(), Some("en"));
}
}