use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EntityReference {
#[serde(rename = "@type")]
pub r#type: String,
pub entity: EntityId,
#[serde(rename = "entityType", skip_serializing_if = "Option::is_none")]
pub entity_type: Option<EntityType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
}
impl EntityReference {
#[must_use]
pub fn new(id: String) -> Self {
Self {
r#type: "EntityReference".to_string(),
entity: EntityId { id },
entity_type: None,
name: None,
role: None,
}
}
#[must_use]
pub fn with_entity_type(mut self, entity_type: EntityType) -> Self {
self.entity_type = Some(entity_type);
self
}
#[must_use]
pub fn with_name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
#[must_use]
pub fn with_role(mut self, role: String) -> Self {
self.role = Some(role);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EntityId {
#[serde(rename = "@id")]
pub id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum EntityType {
Known(KnownEntityType),
Custom(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum KnownEntityType {
Person,
Organization,
Technology,
Concept,
File,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EntityData {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub entity_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entity_id: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl EntityData {
#[must_use]
pub fn new(name: String) -> Self {
Self {
name,
entity_type: None,
entity_id: None,
extra: serde_json::Map::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::{EntityData, EntityReference, EntityType, KnownEntityType};
fn reference_with_type(entity_type: &str) -> String {
format!(
r#"{{"@type":"EntityReference","entity":{{"@id":"urn:mif:entity:person:jane-smith"}},"entityType":"{entity_type}"}}"#
)
}
#[test]
fn round_trips_known_entity_type() {
let json = reference_with_type("Person");
let parsed: EntityReference = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed.entity_type,
Some(EntityType::Known(KnownEntityType::Person))
);
let reserialized = serde_json::to_string(&parsed).unwrap();
let reparsed: EntityReference = serde_json::from_str(&reserialized).unwrap();
assert_eq!(parsed, reparsed);
}
#[test]
fn preserves_custom_entity_type_string() {
let json = reference_with_type("grazing-plan");
let parsed: EntityReference = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed.entity_type,
Some(EntityType::Custom("grazing-plan".to_string()))
);
}
#[test]
fn entity_data_flattens_unknown_fields_losslessly() {
let json = r#"{"name":"Jane Smith","entity_type":"person","herd_size":42}"#;
let parsed: EntityData = serde_json::from_str(json).unwrap();
assert_eq!(parsed.name, "Jane Smith");
assert_eq!(parsed.extra.get("herd_size"), Some(&serde_json::json!(42)));
let reserialized = serde_json::to_value(&parsed).unwrap();
assert_eq!(reserialized["herd_size"], serde_json::json!(42));
}
#[test]
fn entity_reference_builder_sets_all_optional_fields() {
let reference = EntityReference::new("urn:mif:entity:person:jane-smith".to_string())
.with_entity_type(EntityType::Known(KnownEntityType::Person))
.with_name("Jane Smith".to_string())
.with_role("author".to_string());
assert_eq!(reference.r#type, "EntityReference");
assert_eq!(reference.entity.id, "urn:mif:entity:person:jane-smith");
assert_eq!(
reference.entity_type,
Some(EntityType::Known(KnownEntityType::Person))
);
assert_eq!(reference.name, Some("Jane Smith".to_string()));
assert_eq!(reference.role, Some("author".to_string()));
}
#[test]
fn entity_reference_new_has_no_optional_fields_set() {
let reference = EntityReference::new("urn:mif:entity:file:readme".to_string());
assert!(reference.entity_type.is_none());
assert!(reference.name.is_none());
assert!(reference.role.is_none());
}
#[test]
fn entity_data_new_has_no_optional_fields_or_extras() {
let data = EntityData::new("Jane Smith".to_string());
assert_eq!(data.name, "Jane Smith");
assert!(data.entity_type.is_none());
assert!(data.entity_id.is_none());
assert!(data.extra.is_empty());
}
#[test]
fn known_entity_type_covers_every_closed_variant() {
for (variant, label) in [
(KnownEntityType::Person, "Person"),
(KnownEntityType::Organization, "Organization"),
(KnownEntityType::Technology, "Technology"),
(KnownEntityType::Concept, "Concept"),
(KnownEntityType::File, "File"),
] {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, format!("\"{label}\""));
}
}
}