Skip to main content

mif_core/
entity.rs

1use serde::{Deserialize, Serialize};
2
3/// Closed pointer to an entity mentioned by a MIF memory.
4///
5/// Corresponds to `schema/definitions/entity-reference.schema.json`. See
6/// [`EntityData`] for the open-payload counterpart used when a memory *is*
7/// an entity, rather than merely mentioning one.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct EntityReference {
10    /// JSON-LD type marker. Always `"EntityReference"`.
11    #[serde(rename = "@type")]
12    pub r#type: String,
13    /// The entity's identifier.
14    pub entity: EntityId,
15    /// Entity type classification.
16    #[serde(rename = "entityType", skip_serializing_if = "Option::is_none")]
17    pub entity_type: Option<EntityType>,
18    /// Display name for the entity.
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub name: Option<String>,
21    /// Role of the entity in the memory context (e.g. author, subject, topic).
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub role: Option<String>,
24}
25
26impl EntityReference {
27    /// Creates a new reference to the entity identified by `id`
28    /// (e.g. `urn:mif:entity:person:jane-smith`).
29    #[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    /// Sets the entity type classification.
41    #[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    /// Sets the display name.
48    #[must_use]
49    pub fn with_name(mut self, name: String) -> Self {
50        self.name = Some(name);
51        self
52    }
53
54    /// Sets the entity's role in the memory context.
55    #[must_use]
56    pub fn with_role(mut self, role: String) -> Self {
57        self.role = Some(role);
58        self
59    }
60}
61
62/// The entity identifier object nested inside an [`EntityReference`].
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct EntityId {
65    /// Entity URN identifier (`^urn:mif:entity:`).
66    #[serde(rename = "@id")]
67    pub id: String,
68}
69
70/// Entity type classification: a closed vocabulary of well-known types, or
71/// a custom ontology-defined type (`^[a-z][a-z0-9-]*$`) preserved verbatim.
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(untagged)]
74pub enum EntityType {
75    /// One of the schema's closed enum values.
76    Known(KnownEntityType),
77    /// A custom entity type from an ontology (e.g. `grazing-plan`, `soil-profile`).
78    Custom(String),
79}
80
81/// The closed set of well-known entity type values.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83pub enum KnownEntityType {
84    /// A person.
85    Person,
86    /// An organization.
87    Organization,
88    /// A technology.
89    Technology,
90    /// A concept.
91    Concept,
92    /// A file.
93    File,
94}
95
96/// Open, ontology-typed entity payload for a memory that *is* an entity.
97///
98/// Corresponds to `$defs.EntityData` in `mif.schema.json`, an open schema
99/// (`additionalProperties: true`) — see [`EntityReference`] for the closed
100/// pointer counterpart used when a memory merely mentions an entity.
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct EntityData {
103    /// The entity's name.
104    pub name: String,
105    /// The ontology-defined entity type (`^[a-z][a-z0-9-]*$`).
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub entity_type: Option<String>,
108    /// The entity's identifier.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub entity_id: Option<String>,
111    /// Additional ontology-defined fields, preserved losslessly across
112    /// round-trips since this schema is open (`additionalProperties: true`).
113    #[serde(flatten)]
114    pub extra: serde_json::Map<String, serde_json::Value>,
115}
116
117impl EntityData {
118    /// Creates a new entity payload with the given name and no extra fields.
119    #[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}