grc_20/model/
op.rs

1//! Operation types for GRC-20 state changes.
2//!
3//! All state changes in GRC-20 are expressed as operations (ops).
4
5use std::borrow::Cow;
6
7use crate::model::{Context, Id, PropertyValue};
8
9/// An atomic operation that modifies graph state (spec Section 3.1).
10#[derive(Debug, Clone, PartialEq)]
11pub enum Op<'a> {
12    CreateEntity(CreateEntity<'a>),
13    UpdateEntity(UpdateEntity<'a>),
14    DeleteEntity(DeleteEntity),
15    RestoreEntity(RestoreEntity),
16    CreateRelation(CreateRelation<'a>),
17    UpdateRelation(UpdateRelation<'a>),
18    DeleteRelation(DeleteRelation),
19    RestoreRelation(RestoreRelation),
20    CreateValueRef(CreateValueRef),
21}
22
23impl Op<'_> {
24    /// Returns the op type code for wire encoding.
25    pub fn op_type(&self) -> u8 {
26        match self {
27            Op::CreateEntity(_) => 1,
28            Op::UpdateEntity(_) => 2,
29            Op::DeleteEntity(_) => 3,
30            Op::RestoreEntity(_) => 4,
31            Op::CreateRelation(_) => 5,
32            Op::UpdateRelation(_) => 6,
33            Op::DeleteRelation(_) => 7,
34            Op::RestoreRelation(_) => 8,
35            Op::CreateValueRef(_) => 9,
36        }
37    }
38}
39
40/// Creates a new entity (spec Section 3.2).
41///
42/// If the entity does not exist, creates it. If it already exists,
43/// this acts as an update: values are applied as set_properties (LWW).
44#[derive(Debug, Clone, PartialEq)]
45pub struct CreateEntity<'a> {
46    /// The entity's unique identifier.
47    pub id: Id,
48    /// Initial values for the entity.
49    pub values: Vec<PropertyValue<'a>>,
50    /// Optional context for grouping changes (spec Section 4.5).
51    pub context: Option<Context>,
52}
53
54/// Updates an existing entity (spec Section 3.2).
55///
56/// Application order within op:
57/// 1. unset_values
58/// 2. set_properties
59#[derive(Debug, Clone, PartialEq, Default)]
60pub struct UpdateEntity<'a> {
61    /// The entity to update.
62    pub id: Id,
63    /// Replace value for these properties (LWW).
64    pub set_properties: Vec<PropertyValue<'a>>,
65    /// Clear values for these properties (optionally specific language for TEXT).
66    pub unset_values: Vec<UnsetValue>,
67    /// Optional context for grouping changes (spec Section 4.5).
68    pub context: Option<Context>,
69}
70
71/// Specifies which language slot to clear for an UnsetValue.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum UnsetLanguage {
74    /// Clear all language slots (wire format: 0xFFFFFFFF).
75    All,
76    /// Clear only the English slot (wire format: 0).
77    English,
78    /// Clear a specific language slot (wire format: 1+).
79    Specific(Id),
80}
81
82impl Default for UnsetLanguage {
83    fn default() -> Self {
84        Self::All
85    }
86}
87
88/// Specifies a value to unset, with optional language targeting (TEXT only).
89#[derive(Debug, Clone, PartialEq, Eq, Default)]
90pub struct UnsetValue {
91    /// The property whose value to clear.
92    pub property: Id,
93    /// Which language slot(s) to clear.
94    /// For TEXT properties: All clears all slots, English clears English slot,
95    ///   Specific clears a specific language slot.
96    /// For non-TEXT properties: must be All.
97    pub language: UnsetLanguage,
98}
99
100impl UnsetValue {
101    /// Creates an UnsetValue that clears all values for a property.
102    pub fn all(property: Id) -> Self {
103        Self { property, language: UnsetLanguage::All }
104    }
105
106    /// Creates an UnsetValue that clears the English slot for a TEXT property.
107    pub fn english(property: Id) -> Self {
108        Self { property, language: UnsetLanguage::English }
109    }
110
111    /// Creates an UnsetValue that clears a specific language for a TEXT property.
112    pub fn language(property: Id, language: Id) -> Self {
113        Self { property, language: UnsetLanguage::Specific(language) }
114    }
115}
116
117impl<'a> UpdateEntity<'a> {
118    /// Creates a new UpdateEntity for the given entity ID.
119    pub fn new(id: Id) -> Self {
120        Self {
121            id,
122            set_properties: Vec::new(),
123            unset_values: Vec::new(),
124            context: None,
125        }
126    }
127
128    /// Returns true if this update has no actual changes.
129    pub fn is_empty(&self) -> bool {
130        self.set_properties.is_empty() && self.unset_values.is_empty()
131    }
132}
133
134
135/// Deletes an entity (spec Section 3.2).
136///
137/// Transitions the entity to DELETED state. Subsequent updates are ignored
138/// until restored via RestoreEntity.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct DeleteEntity {
141    /// The entity to delete.
142    pub id: Id,
143}
144
145/// Restores a deleted entity (spec Section 3.2).
146///
147/// Transitions a DELETED entity back to ACTIVE state.
148/// If the entity is ACTIVE or does not exist, this is a no-op.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct RestoreEntity {
151    /// The entity to restore.
152    pub id: Id,
153}
154
155/// Creates a new relation (spec Section 3.3).
156///
157/// Also implicitly creates the reified entity if it doesn't exist.
158#[derive(Debug, Clone, PartialEq)]
159pub struct CreateRelation<'a> {
160    /// The relation's unique identifier.
161    pub id: Id,
162    /// The relation type entity ID.
163    pub relation_type: Id,
164    /// Source entity or value ref ID.
165    pub from: Id,
166    /// If true, `from` is a value ref ID (inline encoding).
167    /// If false, `from` is an entity ID (ObjectRef encoding).
168    pub from_is_value_ref: bool,
169    /// Optional space pin for source entity.
170    pub from_space: Option<Id>,
171    /// Optional version (edit ID) to pin source entity.
172    pub from_version: Option<Id>,
173    /// Target entity or value ref ID.
174    pub to: Id,
175    /// If true, `to` is a value ref ID (inline encoding).
176    /// If false, `to` is an entity ID (ObjectRef encoding).
177    pub to_is_value_ref: bool,
178    /// Optional space pin for target entity.
179    pub to_space: Option<Id>,
180    /// Optional version (edit ID) to pin target entity.
181    pub to_version: Option<Id>,
182    /// Explicit reified entity ID.
183    /// If None, entity ID is auto-derived from the relation ID.
184    pub entity: Option<Id>,
185    /// Optional ordering position (fractional indexing).
186    pub position: Option<Cow<'a, str>>,
187    /// Optional context for grouping changes (spec Section 4.5).
188    pub context: Option<Context>,
189}
190
191impl CreateRelation<'_> {
192    /// Computes the reified entity ID.
193    ///
194    /// If explicit entity is provided, returns it.
195    /// Otherwise, derives it from the relation ID.
196    pub fn entity_id(&self) -> Id {
197        use crate::model::id::relation_entity_id;
198        match self.entity {
199            Some(id) => id,
200            None => relation_entity_id(&self.id),
201        }
202    }
203
204    /// Returns true if this relation has an explicit entity ID.
205    pub fn has_explicit_entity(&self) -> bool {
206        self.entity.is_some()
207    }
208}
209
210/// Fields that can be unset on a relation.
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
212pub enum UnsetRelationField {
213    FromSpace,
214    FromVersion,
215    ToSpace,
216    ToVersion,
217    Position,
218}
219
220/// Updates a relation's mutable fields (spec Section 3.3).
221///
222/// The structural fields (entity, type, from, to) are immutable.
223/// The space pins, version pins, and position can be updated or unset.
224#[derive(Debug, Clone, PartialEq, Default)]
225pub struct UpdateRelation<'a> {
226    /// The relation to update.
227    pub id: Id,
228    /// Set space pin for source entity.
229    pub from_space: Option<Id>,
230    /// Set version pin for source entity.
231    pub from_version: Option<Id>,
232    /// Set space pin for target entity.
233    pub to_space: Option<Id>,
234    /// Set version pin for target entity.
235    pub to_version: Option<Id>,
236    /// Set position for ordering.
237    pub position: Option<Cow<'a, str>>,
238    /// Fields to clear/unset.
239    pub unset: Vec<UnsetRelationField>,
240    /// Optional context for grouping changes (spec Section 4.5).
241    pub context: Option<Context>,
242}
243
244impl UpdateRelation<'_> {
245    /// Creates a new UpdateRelation for the given relation ID.
246    pub fn new(id: Id) -> Self {
247        Self {
248            id,
249            from_space: None,
250            from_version: None,
251            to_space: None,
252            to_version: None,
253            position: None,
254            unset: Vec::new(),
255            context: None,
256        }
257    }
258
259    /// Returns true if this update has no actual changes.
260    pub fn is_empty(&self) -> bool {
261        self.from_space.is_none()
262            && self.from_version.is_none()
263            && self.to_space.is_none()
264            && self.to_version.is_none()
265            && self.position.is_none()
266            && self.unset.is_empty()
267    }
268}
269
270/// Deletes a relation (spec Section 3.3).
271///
272/// Transitions the relation to DELETED state. Does NOT delete the reified entity.
273/// Subsequent updates are ignored until restored via RestoreRelation.
274#[derive(Debug, Clone, PartialEq, Eq)]
275pub struct DeleteRelation {
276    /// The relation to delete.
277    pub id: Id,
278}
279
280/// Restores a deleted relation (spec Section 3.3).
281///
282/// Transitions a DELETED relation back to ACTIVE state.
283/// If the relation is ACTIVE or does not exist, this is a no-op.
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct RestoreRelation {
286    /// The relation to restore.
287    pub id: Id,
288}
289
290/// Creates a referenceable ID for a value slot (spec Section 3.4).
291///
292/// This enables relations to target specific values for provenance,
293/// confidence, attribution, or other qualifiers.
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct CreateValueRef {
296    /// The value ref's unique identifier.
297    pub id: Id,
298    /// The entity holding the value.
299    pub entity: Id,
300    /// The property of the value.
301    pub property: Id,
302    /// The language (TEXT values only).
303    pub language: Option<Id>,
304    /// The space containing the value (default: current space).
305    pub space: Option<Id>,
306}
307
308/// Validates a position string according to spec rules.
309///
310/// Position strings must:
311/// - Not be empty
312/// - Only contain characters 0-9, A-Z, a-z (62 chars, ASCII order)
313/// - Not exceed 64 characters
314pub fn validate_position(pos: &str) -> Result<(), &'static str> {
315    if pos.is_empty() {
316        return Err("position cannot be empty");
317    }
318    if pos.len() > 64 {
319        return Err("position exceeds 64 characters");
320    }
321    for c in pos.chars() {
322        if !c.is_ascii_alphanumeric() {
323            return Err("position contains invalid character");
324        }
325    }
326    Ok(())
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_op_type_codes() {
335        assert_eq!(
336            Op::CreateEntity(CreateEntity {
337                id: [0; 16],
338                values: vec![],
339                context: None,
340            })
341            .op_type(),
342            1
343        );
344        assert_eq!(Op::UpdateEntity(UpdateEntity::new([0; 16])).op_type(), 2);
345        assert_eq!(Op::DeleteEntity(DeleteEntity { id: [0; 16] }).op_type(), 3);
346    }
347
348    #[test]
349    fn test_validate_position() {
350        assert!(validate_position("abc123").is_ok());
351        assert!(validate_position("aV").is_ok());
352        assert!(validate_position("a").is_ok());
353
354        // Empty is not allowed
355        assert!(validate_position("").is_err());
356
357        // Invalid characters
358        assert!(validate_position("abc-123").is_err());
359        assert!(validate_position("abc_123").is_err());
360        assert!(validate_position("abc 123").is_err());
361
362        // Too long (65 chars)
363        let long = "a".repeat(65);
364        assert!(validate_position(&long).is_err());
365
366        // Exactly 64 chars is ok
367        let exact = "a".repeat(64);
368        assert!(validate_position(&exact).is_ok());
369    }
370
371    #[test]
372    fn test_update_entity_is_empty() {
373        let update = UpdateEntity::new([0; 16]);
374        assert!(update.is_empty());
375
376        let mut update2 = UpdateEntity::new([0; 16]);
377        update2.set_properties.push(PropertyValue {
378            property: [1; 16],
379            value: crate::model::Value::Bool(true),
380        });
381        assert!(!update2.is_empty());
382    }
383
384    #[test]
385    fn test_entity_id_derivation() {
386        use crate::model::id::relation_entity_id;
387
388        let rel_id = [5u8; 16];
389        let from = [1u8; 16];
390        let to = [2u8; 16];
391        let rel_type = [3u8; 16];
392
393        // Auto-derived entity (entity = None)
394        let rel_auto = CreateRelation {
395            id: rel_id,
396            relation_type: rel_type,
397            from,
398            from_is_value_ref: false,
399            to,
400            to_is_value_ref: false,
401            entity: None,
402            position: None,
403            from_space: None,
404            from_version: None,
405            to_space: None,
406            to_version: None,
407            context: None,
408        };
409        assert_eq!(rel_auto.entity_id(), relation_entity_id(&rel_id));
410        assert!(!rel_auto.has_explicit_entity());
411
412        // Explicit entity
413        let explicit_entity = [6u8; 16];
414        let rel_explicit = CreateRelation {
415            id: rel_id,
416            relation_type: rel_type,
417            from,
418            from_is_value_ref: false,
419            to,
420            to_is_value_ref: false,
421            entity: Some(explicit_entity),
422            position: None,
423            from_space: None,
424            from_version: None,
425            to_space: None,
426            to_version: None,
427            context: None,
428        };
429        assert_eq!(rel_explicit.entity_id(), explicit_entity);
430        assert!(rel_explicit.has_explicit_entity());
431    }
432
433    #[test]
434    fn test_update_relation_is_empty() {
435        let update = UpdateRelation::new([0; 16]);
436        assert!(update.is_empty());
437
438        let mut update2 = UpdateRelation::new([0; 16]);
439        update2.from_space = Some([1; 16]);
440        assert!(!update2.is_empty());
441
442        let mut update3 = UpdateRelation::new([0; 16]);
443        update3.unset.push(UnsetRelationField::Position);
444        assert!(!update3.is_empty());
445    }
446}