use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Identifier {
pub property_id: String,
pub value: String,
}
impl Identifier {
pub fn new(property_id: impl Into<String>, value: impl Into<String>) -> Option<Self> {
let property_id = property_id.into().trim().to_string();
let value = value.into().trim().to_string();
if property_id.is_empty() || value.is_empty() {
None
} else {
Some(Self { property_id, value })
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Thing {
pub name: Option<String>,
pub alternate_names: Vec<String>,
pub description: Option<String>,
pub disambiguating_description: Option<String>,
pub identifiers: Vec<Identifier>,
pub url: Option<String>,
pub image: Option<String>,
pub same_as: Vec<String>,
pub main_entity_of_page: Option<String>,
pub additional_types: Vec<String>,
pub subject_of: Vec<String>,
pub owner: Option<String>,
pub local_id: Option<String>,
}
impl Thing {
pub fn builder() -> ThingBuilder {
ThingBuilder::default()
}
pub fn validate(&self) -> crate::Result<()> {
if self.name.is_none() {
return Err(crate::MatchingError::MissingField(
"name is required".to_string(),
));
}
Ok(())
}
}
#[derive(Default)]
pub struct ThingBuilder {
name: Option<String>,
alternate_names: Vec<String>,
description: Option<String>,
disambiguating_description: Option<String>,
identifiers: Vec<Identifier>,
url: Option<String>,
image: Option<String>,
same_as: Vec<String>,
main_entity_of_page: Option<String>,
additional_types: Vec<String>,
subject_of: Vec<String>,
owner: Option<String>,
local_id: Option<String>,
}
impl ThingBuilder {
pub fn name<S: Into<String>>(mut self, value: S) -> Self {
self.name = Some(value.into());
self
}
pub fn alternate_names(mut self, value: Vec<String>) -> Self {
self.alternate_names = value;
self
}
pub fn add_alternate_name<S: Into<String>>(mut self, value: S) -> Self {
self.alternate_names.push(value.into());
self
}
pub fn description<S: Into<String>>(mut self, value: S) -> Self {
self.description = Some(value.into());
self
}
pub fn disambiguating_description<S: Into<String>>(mut self, value: S) -> Self {
self.disambiguating_description = Some(value.into());
self
}
pub fn identifiers(mut self, value: Vec<Identifier>) -> Self {
self.identifiers = value;
self
}
pub fn add_identifier(mut self, value: Identifier) -> Self {
self.identifiers.push(value);
self
}
pub fn url<S: Into<String>>(mut self, value: S) -> Self {
self.url = Some(value.into());
self
}
pub fn image<S: Into<String>>(mut self, value: S) -> Self {
self.image = Some(value.into());
self
}
pub fn same_as(mut self, value: Vec<String>) -> Self {
self.same_as = value;
self
}
pub fn add_same_as<S: Into<String>>(mut self, value: S) -> Self {
self.same_as.push(value.into());
self
}
pub fn main_entity_of_page<S: Into<String>>(mut self, value: S) -> Self {
self.main_entity_of_page = Some(value.into());
self
}
pub fn additional_types(mut self, value: Vec<String>) -> Self {
self.additional_types = value;
self
}
pub fn add_additional_type<S: Into<String>>(mut self, value: S) -> Self {
self.additional_types.push(value.into());
self
}
pub fn subject_of(mut self, value: Vec<String>) -> Self {
self.subject_of = value;
self
}
pub fn add_subject_of<S: Into<String>>(mut self, value: S) -> Self {
self.subject_of.push(value.into());
self
}
pub fn owner<S: Into<String>>(mut self, value: S) -> Self {
self.owner = Some(value.into());
self
}
pub fn local_id<S: Into<String>>(mut self, value: S) -> Self {
self.local_id = Some(value.into());
self
}
pub fn build(self) -> Thing {
Thing {
name: self.name,
alternate_names: self.alternate_names,
description: self.description,
disambiguating_description: self.disambiguating_description,
identifiers: self.identifiers,
url: self.url,
image: self.image,
same_as: self.same_as,
main_entity_of_page: self.main_entity_of_page,
additional_types: self.additional_types,
subject_of: self.subject_of,
owner: self.owner,
local_id: self.local_id,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thing_builder_starts_empty() {
let t = Thing::builder().build();
assert!(t.name.is_none());
assert!(t.alternate_names.is_empty());
assert!(t.description.is_none());
assert!(t.disambiguating_description.is_none());
assert!(t.identifiers.is_empty());
assert!(t.url.is_none());
assert!(t.image.is_none());
assert!(t.same_as.is_empty());
assert!(t.main_entity_of_page.is_none());
assert!(t.additional_types.is_empty());
assert!(t.subject_of.is_empty());
assert!(t.owner.is_none());
assert!(t.local_id.is_none());
}
#[test]
fn thing_builder_accepts_str_and_string() {
let t = Thing::builder()
.name("Eiffel Tower")
.add_alternate_name(String::from("La Tour Eiffel"))
.build();
assert_eq!(t.name.as_deref(), Some("Eiffel Tower"));
assert_eq!(t.alternate_names, vec!["La Tour Eiffel".to_string()]);
}
#[test]
fn thing_validate_requires_a_name() {
assert!(
Thing::builder()
.name("Eiffel Tower")
.build()
.validate()
.is_ok()
);
let err = Thing::builder()
.build()
.validate()
.expect_err("should be missing");
assert!(matches!(err, crate::MatchingError::MissingField(_)));
}
#[test]
fn thing_round_trips_through_serde() {
let t = Thing::builder()
.name("Eiffel Tower")
.add_alternate_name("La Tour Eiffel")
.description("Iron tower in Paris.")
.url("https://www.toureiffel.paris/")
.add_identifier(Identifier::new("wikidata", "Q243").unwrap())
.add_same_as("https://www.wikidata.org/wiki/Q243")
.add_additional_type("https://schema.org/Landmark")
.build();
let json = serde_json::to_string(&t).expect("serialise");
let back: Thing = serde_json::from_str(&json).expect("deserialise");
assert_eq!(t, back);
}
#[test]
fn alternate_names_setter_replaces_vec() {
let t = Thing::builder()
.alternate_names(vec!["X".into(), "Y".into()])
.build();
assert_eq!(t.alternate_names, vec!["X".to_string(), "Y".to_string()]);
}
#[test]
fn identifier_trims_value_and_property_id() {
let id = Identifier::new(" wikidata ", " Q243 ").unwrap();
assert_eq!(id.property_id, "wikidata");
assert_eq!(id.value, "Q243");
}
#[test]
fn identifier_rejects_empty_components() {
assert!(Identifier::new("wikidata", "").is_none());
assert!(Identifier::new("wikidata", " ").is_none());
assert!(Identifier::new("", "Q243").is_none());
assert!(Identifier::new(" ", "Q243").is_none());
}
#[test]
fn identifier_equality_is_property_scoped() {
let g = Identifier::new("google", "X").unwrap();
let w = Identifier::new("wikidata", "X").unwrap();
assert_ne!(g, w);
}
#[test]
fn identifier_round_trips_through_serde() {
let id = Identifier::new("custom", "abc-123").unwrap();
let json = serde_json::to_string(&id).expect("serialise");
let back: Identifier = serde_json::from_str(&json).expect("deserialise");
assert_eq!(id, back);
}
}