use std::{convert::TryInto, fmt};
use quickcheck::{Arbitrary, Gen};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use yaserde_derive::{YaDeserialize, YaSerialize};
use crate::{
Agent, Attribution, Coverage, Document, EnumAsString, Id, Identifier, Note, ResourceReference,
Result, SourceCitation, SourceReference, TextValue, Timestamp, Uri,
};
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, YaSerialize, YaDeserialize, PartialEq, Default, Clone)]
#[yaserde(
prefix = "gx",
default_namespace = "gx",
namespace = "gx: http://gedcomx.org/v1/"
)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct SourceDescription {
#[yaserde(attribute)]
pub id: Option<Id>,
#[yaserde(rename = "resourceType", attribute)]
pub resource_type: Option<ResourceType>,
#[yaserde(rename = "citation", prefix = "gx")]
pub citations: Vec<SourceCitation>,
#[yaserde(rename = "mediaType", attribute)]
pub media_type: Option<String>,
#[yaserde(attribute)]
pub about: Option<Uri>,
#[yaserde(prefix = "gx")]
pub mediator: Option<ResourceReference>,
#[yaserde(prefix = "gx")]
pub publisher: Option<ResourceReference>,
#[yaserde(rename = "author", prefix = "gx")]
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub authors: Vec<ResourceReference>,
#[yaserde(rename = "source", prefix = "gx")]
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub sources: Vec<SourceReference>,
#[yaserde(prefix = "gx")]
pub analysis: Option<ResourceReference>,
#[yaserde(rename = "componentOf", prefix = "gx")]
pub component_of: Option<SourceReference>,
#[yaserde(rename = "title", prefix = "gx")]
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub titles: Vec<TextValue>,
#[yaserde(rename = "note", prefix = "gx")]
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub notes: Vec<Note>,
#[yaserde(prefix = "gx")]
pub attribution: Option<Attribution>,
#[yaserde(prefix = "gx")]
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub rights: Vec<ResourceReference>,
#[yaserde(prefix = "gx")]
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub coverage: Vec<Coverage>,
#[yaserde(rename = "description", prefix = "gx")]
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub descriptions: Vec<TextValue>,
#[yaserde(rename = "identifier", prefix = "gx")]
#[serde(
skip_serializing_if = "Vec::is_empty",
default,
with = "crate::serde_vec_identifier_to_map"
)]
pub identifiers: Vec<Identifier>,
#[yaserde(prefix = "gx")]
pub created: Option<Timestamp>,
#[yaserde(prefix = "gx")]
pub modified: Option<Timestamp>,
#[yaserde(prefix = "gx")]
pub published: Option<Timestamp>,
#[yaserde(prefix = "gx")]
pub repository: Option<ResourceReference>,
}
#[allow(clippy::similar_names)]
impl SourceDescription {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: Option<Id>,
resource_type: Option<ResourceType>,
citations: Vec<SourceCitation>,
media_type: Option<String>,
about: Option<Uri>,
mediator: Option<ResourceReference>,
publisher: Option<ResourceReference>,
authors: Vec<ResourceReference>,
sources: Vec<SourceReference>,
analysis: Option<ResourceReference>,
component_of: Option<SourceReference>,
titles: Vec<TextValue>,
notes: Vec<Note>,
attribution: Option<Attribution>,
rights: Vec<ResourceReference>,
coverage: Vec<Coverage>,
descriptions: Vec<TextValue>,
identifiers: Vec<Identifier>,
created: Option<Timestamp>,
modified: Option<Timestamp>,
published: Option<Timestamp>,
repository: Option<ResourceReference>,
) -> Self {
Self {
id,
resource_type,
citations,
media_type,
about,
mediator,
publisher,
authors,
sources,
analysis,
component_of,
titles,
notes,
attribution,
rights,
coverage,
descriptions,
identifiers,
created,
modified,
published,
repository,
}
}
pub fn builder(citation: SourceCitation) -> SourceDescriptionBuilder {
SourceDescriptionBuilder::new(citation)
}
}
impl Arbitrary for SourceDescription {
fn arbitrary(g: &mut Gen) -> Self {
let mut source_description = Self::builder(SourceCitation::arbitrary(g))
.id(Id::arbitrary(g))
.resource_type(ResourceType::arbitrary(g))
.media_type(crate::arbitrary_trimmed(g))
.about(Uri::arbitrary(g))
.title(TextValue::arbitrary(g))
.right(Uri::arbitrary(g))
.description(TextValue::arbitrary(g))
.identifier(Identifier::arbitrary(g))
.source(SourceReference::arbitrary(g))
.component_of(SourceReference::arbitrary(g))
.note(Note::arbitrary(g))
.attribution(Attribution::arbitrary(g))
.coverage(Coverage::arbitrary(g))
.created(Timestamp::arbitrary(g))
.modified(Timestamp::arbitrary(g))
.published(Timestamp::arbitrary(g))
.build();
source_description.mediator = Some(ResourceReference::arbitrary(g));
source_description.publisher = Some(ResourceReference::arbitrary(g));
source_description.authors = vec![ResourceReference::arbitrary(g)];
source_description.repository = Some(ResourceReference::arbitrary(g));
source_description.analysis = Some(ResourceReference::arbitrary(g));
source_description
}
}
pub struct SourceDescriptionBuilder(SourceDescription);
impl SourceDescriptionBuilder {
pub(crate) fn new(citation: SourceCitation) -> Self {
Self(SourceDescription {
citations: vec![citation],
..SourceDescription::default()
})
}
pub fn id<I: Into<Id>>(&mut self, id: I) -> &mut Self {
self.0.id = Some(id.into());
self
}
pub fn resource_type(&mut self, resource_type: ResourceType) -> &mut Self {
self.0.resource_type = Some(resource_type);
self
}
pub fn citation(&mut self, source_citation: SourceCitation) -> &mut Self {
self.0.citations.push(source_citation);
self
}
pub fn media_type<I: Into<String>>(&mut self, media_type: I) -> &mut Self {
self.0.media_type = Some(media_type.into());
self
}
pub fn about(&mut self, uri: Uri) -> &mut Self {
self.0.about = Some(uri);
self
}
pub fn mediator(&mut self, mediator: &Agent) -> Result<&mut Self> {
self.0.mediator = Some(mediator.try_into()?);
Ok(self)
}
pub fn publisher(&mut self, publisher: &Agent) -> Result<&mut Self> {
self.0.publisher = Some(publisher.try_into()?);
Ok(self)
}
pub fn author(&mut self, author: &Agent) -> Result<&mut Self> {
self.0.authors.push(author.try_into()?);
Ok(self)
}
pub fn source(&mut self, source: SourceReference) -> &mut Self {
self.0.sources.push(source);
self
}
pub fn analysis(&mut self, analysis: &Document) -> Result<&mut Self> {
self.0.analysis = Some(analysis.try_into()?);
Ok(self)
}
pub fn component_of(&mut self, component_of: SourceReference) -> &mut Self {
self.0.component_of = Some(component_of);
self
}
pub fn title<I: Into<TextValue>>(&mut self, title: I) -> &mut Self {
self.0.titles.push(title.into());
self
}
pub fn note(&mut self, note: Note) -> &mut Self {
self.0.notes.push(note);
self
}
pub fn attribution(&mut self, attribution: Attribution) -> &mut Self {
self.0.attribution = Some(attribution);
self
}
pub fn right(&mut self, right: Uri) -> &mut Self {
self.0.rights.push(ResourceReference::new(right));
self
}
pub fn coverage(&mut self, coverage: Coverage) -> &mut Self {
self.0.coverage.push(coverage);
self
}
pub fn description<I: Into<TextValue>>(&mut self, description: I) -> &mut Self {
self.0.descriptions.push(description.into());
self
}
pub fn identifier(&mut self, identifier: Identifier) -> &mut Self {
self.0.identifiers.push(identifier);
self
}
pub fn created(&mut self, created: Timestamp) -> &mut Self {
self.0.created = Some(created);
self
}
pub fn modified(&mut self, modified: Timestamp) -> &mut Self {
self.0.modified = Some(modified);
self
}
pub fn published(&mut self, published: Timestamp) -> &mut Self {
self.0.published = Some(published);
self
}
pub fn repository(&mut self, repository: &Agent) -> Result<&mut Self> {
self.0.repository = Some(repository.try_into()?);
Ok(self)
}
pub fn build(&self) -> SourceDescription {
SourceDescription::new(
self.0.id.clone(),
self.0.resource_type.clone(),
self.0.citations.clone(),
self.0.media_type.clone(),
self.0.about.clone(),
self.0.mediator.clone(),
self.0.publisher.clone(),
self.0.authors.clone(),
self.0.sources.clone(),
self.0.analysis.clone(),
self.0.component_of.clone(),
self.0.titles.clone(),
self.0.notes.clone(),
self.0.attribution.clone(),
self.0.rights.clone(),
self.0.coverage.clone(),
self.0.descriptions.clone(),
self.0.identifiers.clone(),
self.0.created.clone(),
self.0.modified.clone(),
self.0.published.clone(),
self.0.repository.clone(),
)
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq)]
#[non_exhaustive]
#[serde(from = "EnumAsString", into = "EnumAsString")]
pub enum ResourceType {
Collection,
PhysicalArtifact,
DigitalArtifact,
Record,
Custom(Uri),
}
impl_enumasstring_yaserialize_yadeserialize!(ResourceType, "ResourceType");
impl From<EnumAsString> for ResourceType {
fn from(f: EnumAsString) -> Self {
match f.0.as_ref() {
"http://gedcomx.org/Collection" => Self::Collection,
"http://gedcomx.org/PhysicalArtifact" => Self::PhysicalArtifact,
"http://gedcomx.org/DigitalArtifact" => Self::DigitalArtifact,
"http://gedcomx.org/Record" => Self::Record,
_ => Self::Custom(f.0.into()),
}
}
}
impl fmt::Display for ResourceType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
match self {
Self::Collection => write!(f, "http://gedcomx.org/Collection"),
Self::PhysicalArtifact => write!(f, "http://gedcomx.org/PhysicalArtifact"),
Self::DigitalArtifact => write!(f, "http://gedcomx.org/DigitalArtifact"),
Self::Record => write!(f, "http://gedcomx.org/Record"),
Self::Custom(c) => write!(f, "{c}"),
}
}
}
impl Default for ResourceType {
fn default() -> Self {
Self::Custom(Uri::default())
}
}
impl Arbitrary for ResourceType {
fn arbitrary(g: &mut Gen) -> Self {
let options = vec![
Self::Collection,
Self::PhysicalArtifact,
Self::DigitalArtifact,
Self::Record,
Self::Custom(Uri::arbitrary(g)),
];
g.choose(&options).unwrap().clone()
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn json_deserialize() {
let json = r##"{
"id" : "local_id",
"resourceType" : "http://gedcomx.org/PhysicalArtifact",
"citations" : [ { "value": "citation", "lang": "en"}],
"mediaType" : "media_type",
"about" : "about",
"mediator": {
"resource": "#agent"
},
"publisher": {
"resource": "#agent"
}
}"##;
let source_description: SourceDescription = serde_json::from_str(json).unwrap();
let agent = Agent::builder().id("agent").build();
let expected_source_description =
SourceDescription::builder(SourceCitation::new("citation", Some("en".into())))
.id("local_id")
.resource_type(ResourceType::PhysicalArtifact)
.media_type("media_type")
.about("about".into())
.mediator(&agent)
.unwrap()
.publisher(&agent)
.unwrap()
.build();
assert_eq!(source_description, expected_source_description);
}
#[test]
fn xml_deserialize() {
let xml = r##"<SourceDescription id="local_id" about="about" mediaType="media_type" resourceType="http://gedcomx.org/PhysicalArtifact">
<citation xml:lang="en">
<value>citation</value>
</citation>
<mediator resource="#agent" />
<publisher resource="#agent" />
</SourceDescription>"##;
let source_description: SourceDescription = yaserde::de::from_str(xml).unwrap();
let agent = Agent::builder().id("agent").build();
let expected_source_description =
SourceDescription::builder(SourceCitation::new("citation", Some("en".into())))
.id("local_id")
.resource_type(ResourceType::PhysicalArtifact)
.media_type("media_type")
.about("about".into())
.mediator(&agent)
.unwrap()
.publisher(&agent)
.unwrap()
.build();
assert_eq!(source_description, expected_source_description);
}
#[test]
fn json_serialize() {
let agent = Agent::builder().id("agent").build();
let source_description =
SourceDescription::builder(SourceCitation::new("citation", Some("en".into())))
.id("local_id")
.resource_type(ResourceType::PhysicalArtifact)
.media_type("media_type")
.about("about".into())
.mediator(&agent)
.unwrap()
.publisher(&agent)
.unwrap()
.build();
let json = serde_json::to_string(&source_description).unwrap();
let expected_json = r##"{"id":"local_id","resourceType":"http://gedcomx.org/PhysicalArtifact","citations":[{"lang":"en","value":"citation"}],"mediaType":"media_type","about":"about","mediator":{"resource":"#agent"},"publisher":{"resource":"#agent"}}"##;
assert_eq!(json, expected_json);
}
#[test]
fn xml_serialize() {
let agent = Agent::builder().id("agent").build();
let source_description =
SourceDescription::builder(SourceCitation::new("citation", Some("en".into())))
.id("local_id")
.resource_type(ResourceType::PhysicalArtifact)
.media_type("media_type")
.about("about".into())
.mediator(&agent)
.unwrap()
.publisher(&agent)
.unwrap()
.build();
let config = yaserde::ser::Config {
write_document_declaration: false,
..yaserde::ser::Config::default()
};
let xml = yaserde::ser::to_string_with_config(&source_description, &config).unwrap();
let expected_xml = r##"<SourceDescription xmlns="http://gedcomx.org/v1/" id="local_id" resourceType="http://gedcomx.org/PhysicalArtifact" mediaType="media_type" about="about"><citation xml:lang="en"><value>citation</value></citation><mediator resource="#agent" /><publisher resource="#agent" /></SourceDescription>"##;
assert_eq!(xml, expected_xml);
}
#[quickcheck_macros::quickcheck]
fn roundtrip_json(input: SourceDescription) -> bool {
let json = serde_json::to_string(&input).unwrap();
let from_json: SourceDescription = serde_json::from_str(&json).unwrap();
assert_eq!(input, from_json);
input == from_json
}
#[quickcheck_macros::quickcheck]
fn roundtrip_xml(input: SourceDescription) -> bool {
let xml = yaserde::ser::to_string(&input).unwrap();
let from_xml: SourceDescription = yaserde::de::from_str(&xml).unwrap();
input == from_xml
}
}