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