1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct EntityReference {
10 #[serde(rename = "@type")]
12 pub r#type: String,
13 pub entity: EntityId,
15 #[serde(rename = "entityType", skip_serializing_if = "Option::is_none")]
17 pub entity_type: Option<EntityType>,
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub name: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub role: Option<String>,
24}
25
26impl EntityReference {
27 #[must_use]
30 pub fn new(id: String) -> Self {
31 Self {
32 r#type: "EntityReference".to_string(),
33 entity: EntityId { id },
34 entity_type: None,
35 name: None,
36 role: None,
37 }
38 }
39
40 #[must_use]
42 pub fn with_entity_type(mut self, entity_type: EntityType) -> Self {
43 self.entity_type = Some(entity_type);
44 self
45 }
46
47 #[must_use]
49 pub fn with_name(mut self, name: String) -> Self {
50 self.name = Some(name);
51 self
52 }
53
54 #[must_use]
56 pub fn with_role(mut self, role: String) -> Self {
57 self.role = Some(role);
58 self
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct EntityId {
65 #[serde(rename = "@id")]
67 pub id: String,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(untagged)]
74pub enum EntityType {
75 Known(KnownEntityType),
77 Custom(String),
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83pub enum KnownEntityType {
84 Person,
86 Organization,
88 Technology,
90 Concept,
92 File,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct EntityData {
103 pub name: String,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub entity_type: Option<String>,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub entity_id: Option<String>,
111 #[serde(flatten)]
114 pub extra: serde_json::Map<String, serde_json::Value>,
115}
116
117impl EntityData {
118 #[must_use]
120 pub fn new(name: String) -> Self {
121 Self {
122 name,
123 entity_type: None,
124 entity_id: None,
125 extra: serde_json::Map::new(),
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::{EntityData, EntityReference, EntityType, KnownEntityType};
133
134 fn reference_with_type(entity_type: &str) -> String {
135 format!(
136 r#"{{"@type":"EntityReference","entity":{{"@id":"urn:mif:entity:person:jane-smith"}},"entityType":"{entity_type}"}}"#
137 )
138 }
139
140 #[test]
141 fn round_trips_known_entity_type() {
142 let json = reference_with_type("Person");
143 let parsed: EntityReference = serde_json::from_str(&json).unwrap();
144 assert_eq!(
145 parsed.entity_type,
146 Some(EntityType::Known(KnownEntityType::Person))
147 );
148 let reserialized = serde_json::to_string(&parsed).unwrap();
149 let reparsed: EntityReference = serde_json::from_str(&reserialized).unwrap();
150 assert_eq!(parsed, reparsed);
151 }
152
153 #[test]
154 fn preserves_custom_entity_type_string() {
155 let json = reference_with_type("grazing-plan");
156 let parsed: EntityReference = serde_json::from_str(&json).unwrap();
157 assert_eq!(
158 parsed.entity_type,
159 Some(EntityType::Custom("grazing-plan".to_string()))
160 );
161 }
162
163 #[test]
164 fn entity_data_flattens_unknown_fields_losslessly() {
165 let json = r#"{"name":"Jane Smith","entity_type":"person","herd_size":42}"#;
166 let parsed: EntityData = serde_json::from_str(json).unwrap();
167 assert_eq!(parsed.name, "Jane Smith");
168 assert_eq!(parsed.extra.get("herd_size"), Some(&serde_json::json!(42)));
169 let reserialized = serde_json::to_value(&parsed).unwrap();
170 assert_eq!(reserialized["herd_size"], serde_json::json!(42));
171 }
172
173 #[test]
174 fn entity_reference_builder_sets_all_optional_fields() {
175 let reference = EntityReference::new("urn:mif:entity:person:jane-smith".to_string())
176 .with_entity_type(EntityType::Known(KnownEntityType::Person))
177 .with_name("Jane Smith".to_string())
178 .with_role("author".to_string());
179
180 assert_eq!(reference.r#type, "EntityReference");
181 assert_eq!(reference.entity.id, "urn:mif:entity:person:jane-smith");
182 assert_eq!(
183 reference.entity_type,
184 Some(EntityType::Known(KnownEntityType::Person))
185 );
186 assert_eq!(reference.name, Some("Jane Smith".to_string()));
187 assert_eq!(reference.role, Some("author".to_string()));
188 }
189
190 #[test]
191 fn entity_reference_new_has_no_optional_fields_set() {
192 let reference = EntityReference::new("urn:mif:entity:file:readme".to_string());
193 assert!(reference.entity_type.is_none());
194 assert!(reference.name.is_none());
195 assert!(reference.role.is_none());
196 }
197
198 #[test]
199 fn entity_data_new_has_no_optional_fields_or_extras() {
200 let data = EntityData::new("Jane Smith".to_string());
201 assert_eq!(data.name, "Jane Smith");
202 assert!(data.entity_type.is_none());
203 assert!(data.entity_id.is_none());
204 assert!(data.extra.is_empty());
205 }
206
207 #[test]
208 fn known_entity_type_covers_every_closed_variant() {
209 for (variant, label) in [
210 (KnownEntityType::Person, "Person"),
211 (KnownEntityType::Organization, "Organization"),
212 (KnownEntityType::Technology, "Technology"),
213 (KnownEntityType::Concept, "Concept"),
214 (KnownEntityType::File, "File"),
215 ] {
216 let json = serde_json::to_string(&variant).unwrap();
217 assert_eq!(json, format!("\"{label}\""));
218 }
219 }
220}