Skip to main content

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)]
140pub struct DeleteEntity {
141    /// The entity to delete.
142    pub id: Id,
143    /// Optional context for grouping changes (spec Section 4.5).
144    pub context: Option<Context>,
145}
146
147/// Restores a deleted entity (spec Section 3.2).
148///
149/// Transitions a DELETED entity back to ACTIVE state.
150/// If the entity is ACTIVE or does not exist, this is a no-op.
151#[derive(Debug, Clone, PartialEq)]
152pub struct RestoreEntity {
153    /// The entity to restore.
154    pub id: Id,
155    /// Optional context for grouping changes (spec Section 4.5).
156    pub context: Option<Context>,
157}
158
159/// Creates a new relation (spec Section 3.3).
160///
161/// Also implicitly creates the reified entity if it doesn't exist.
162#[derive(Debug, Clone, PartialEq)]
163pub struct CreateRelation<'a> {
164    /// The relation's unique identifier.
165    pub id: Id,
166    /// The relation type entity ID.
167    pub relation_type: Id,
168    /// Source entity or value ref ID.
169    pub from: Id,
170    /// If true, `from` is a value ref ID (inline encoding).
171    /// If false, `from` is an entity ID (ObjectRef encoding).
172    pub from_is_value_ref: bool,
173    /// Optional space pin for source entity.
174    pub from_space: Option<Id>,
175    /// Optional version (edit ID) to pin source entity.
176    pub from_version: Option<Id>,
177    /// Target entity or value ref ID.
178    pub to: Id,
179    /// If true, `to` is a value ref ID (inline encoding).
180    /// If false, `to` is an entity ID (ObjectRef encoding).
181    pub to_is_value_ref: bool,
182    /// Optional space pin for target entity.
183    pub to_space: Option<Id>,
184    /// Optional version (edit ID) to pin target entity.
185    pub to_version: Option<Id>,
186    /// Explicit reified entity ID.
187    /// If None, entity ID is auto-derived from the relation ID.
188    pub entity: Option<Id>,
189    /// Optional ordering position (fractional indexing).
190    pub position: Option<Cow<'a, str>>,
191    /// Optional context for grouping changes (spec Section 4.5).
192    pub context: Option<Context>,
193}
194
195impl CreateRelation<'_> {
196    /// Computes the reified entity ID.
197    ///
198    /// If explicit entity is provided, returns it.
199    /// Otherwise, derives it from the relation ID.
200    pub fn entity_id(&self) -> Id {
201        use crate::model::id::relation_entity_id;
202        match self.entity {
203            Some(id) => id,
204            None => relation_entity_id(&self.id),
205        }
206    }
207
208    /// Returns true if this relation has an explicit entity ID.
209    pub fn has_explicit_entity(&self) -> bool {
210        self.entity.is_some()
211    }
212}
213
214/// Fields that can be unset on a relation.
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
216pub enum UnsetRelationField {
217    FromSpace,
218    FromVersion,
219    ToSpace,
220    ToVersion,
221    Position,
222}
223
224/// Updates a relation's mutable fields (spec Section 3.3).
225///
226/// The structural fields (entity, type, from, to) are immutable.
227/// The space pins, version pins, and position can be updated or unset.
228#[derive(Debug, Clone, PartialEq, Default)]
229pub struct UpdateRelation<'a> {
230    /// The relation to update.
231    pub id: Id,
232    /// Set space pin for source entity.
233    pub from_space: Option<Id>,
234    /// Set version pin for source entity.
235    pub from_version: Option<Id>,
236    /// Set space pin for target entity.
237    pub to_space: Option<Id>,
238    /// Set version pin for target entity.
239    pub to_version: Option<Id>,
240    /// Set position for ordering.
241    pub position: Option<Cow<'a, str>>,
242    /// Fields to clear/unset.
243    pub unset: Vec<UnsetRelationField>,
244    /// Optional context for grouping changes (spec Section 4.5).
245    pub context: Option<Context>,
246}
247
248impl UpdateRelation<'_> {
249    /// Creates a new UpdateRelation for the given relation ID.
250    pub fn new(id: Id) -> Self {
251        Self {
252            id,
253            from_space: None,
254            from_version: None,
255            to_space: None,
256            to_version: None,
257            position: None,
258            unset: Vec::new(),
259            context: None,
260        }
261    }
262
263    /// Returns true if this update has no actual changes.
264    pub fn is_empty(&self) -> bool {
265        self.from_space.is_none()
266            && self.from_version.is_none()
267            && self.to_space.is_none()
268            && self.to_version.is_none()
269            && self.position.is_none()
270            && self.unset.is_empty()
271    }
272}
273
274/// Deletes a relation (spec Section 3.3).
275///
276/// Transitions the relation to DELETED state. Does NOT delete the reified entity.
277/// Subsequent updates are ignored until restored via RestoreRelation.
278#[derive(Debug, Clone, PartialEq)]
279pub struct DeleteRelation {
280    /// The relation to delete.
281    pub id: Id,
282    /// Optional context for grouping changes (spec Section 4.5).
283    pub context: Option<Context>,
284}
285
286/// Restores a deleted relation (spec Section 3.3).
287///
288/// Transitions a DELETED relation back to ACTIVE state.
289/// If the relation is ACTIVE or does not exist, this is a no-op.
290#[derive(Debug, Clone, PartialEq)]
291pub struct RestoreRelation {
292    /// The relation to restore.
293    pub id: Id,
294    /// Optional context for grouping changes (spec Section 4.5).
295    pub context: Option<Context>,
296}
297
298/// Creates a referenceable ID for a value slot (spec Section 3.4).
299///
300/// This enables relations to target specific values for provenance,
301/// confidence, attribution, or other qualifiers.
302#[derive(Debug, Clone, PartialEq, Eq)]
303pub struct CreateValueRef {
304    /// The value ref's unique identifier.
305    pub id: Id,
306    /// The entity holding the value.
307    pub entity: Id,
308    /// The property of the value.
309    pub property: Id,
310    /// The language (TEXT values only).
311    pub language: Option<Id>,
312    /// The space containing the value (default: current space).
313    pub space: Option<Id>,
314}
315
316/// Validates a position string according to spec rules.
317///
318/// Position strings must:
319/// - Not be empty
320/// - Only contain characters 0-9, A-Z, a-z (62 chars, ASCII order)
321/// - Not exceed 64 characters
322pub fn validate_position(pos: &str) -> Result<(), &'static str> {
323    if pos.is_empty() {
324        return Err("position cannot be empty");
325    }
326    if pos.len() > 64 {
327        return Err("position exceeds 64 characters");
328    }
329    for c in pos.chars() {
330        if !c.is_ascii_alphanumeric() {
331            return Err("position contains invalid character");
332        }
333    }
334    Ok(())
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_op_type_codes() {
343        assert_eq!(
344            Op::CreateEntity(CreateEntity {
345                id: [0; 16],
346                values: vec![],
347                context: None,
348            })
349            .op_type(),
350            1
351        );
352        assert_eq!(Op::UpdateEntity(UpdateEntity::new([0; 16])).op_type(), 2);
353        assert_eq!(Op::DeleteEntity(DeleteEntity { id: [0; 16], context: None }).op_type(), 3);
354    }
355
356    #[test]
357    fn test_validate_position() {
358        assert!(validate_position("abc123").is_ok());
359        assert!(validate_position("aV").is_ok());
360        assert!(validate_position("a").is_ok());
361
362        // Empty is not allowed
363        assert!(validate_position("").is_err());
364
365        // Invalid characters
366        assert!(validate_position("abc-123").is_err());
367        assert!(validate_position("abc_123").is_err());
368        assert!(validate_position("abc 123").is_err());
369
370        // Too long (65 chars)
371        let long = "a".repeat(65);
372        assert!(validate_position(&long).is_err());
373
374        // Exactly 64 chars is ok
375        let exact = "a".repeat(64);
376        assert!(validate_position(&exact).is_ok());
377    }
378
379    #[test]
380    fn test_update_entity_is_empty() {
381        let update = UpdateEntity::new([0; 16]);
382        assert!(update.is_empty());
383
384        let mut update2 = UpdateEntity::new([0; 16]);
385        update2.set_properties.push(PropertyValue {
386            property: [1; 16],
387            value: crate::model::Value::Bool(true),
388        });
389        assert!(!update2.is_empty());
390    }
391
392    #[test]
393    fn test_entity_id_derivation() {
394        use crate::model::id::relation_entity_id;
395
396        let rel_id = [5u8; 16];
397        let from = [1u8; 16];
398        let to = [2u8; 16];
399        let rel_type = [3u8; 16];
400
401        // Auto-derived entity (entity = None)
402        let rel_auto = 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: None,
410            position: None,
411            from_space: None,
412            from_version: None,
413            to_space: None,
414            to_version: None,
415            context: None,
416        };
417        assert_eq!(rel_auto.entity_id(), relation_entity_id(&rel_id));
418        assert!(!rel_auto.has_explicit_entity());
419
420        // Explicit entity
421        let explicit_entity = [6u8; 16];
422        let rel_explicit = CreateRelation {
423            id: rel_id,
424            relation_type: rel_type,
425            from,
426            from_is_value_ref: false,
427            to,
428            to_is_value_ref: false,
429            entity: Some(explicit_entity),
430            position: None,
431            from_space: None,
432            from_version: None,
433            to_space: None,
434            to_version: None,
435            context: None,
436        };
437        assert_eq!(rel_explicit.entity_id(), explicit_entity);
438        assert!(rel_explicit.has_explicit_entity());
439    }
440
441    #[test]
442    fn test_update_relation_is_empty() {
443        let update = UpdateRelation::new([0; 16]);
444        assert!(update.is_empty());
445
446        let mut update2 = UpdateRelation::new([0; 16]);
447        update2.from_space = Some([1; 16]);
448        assert!(!update2.is_empty());
449
450        let mut update3 = UpdateRelation::new([0; 16]);
451        update3.unset.push(UnsetRelationField::Position);
452        assert!(!update3.is_empty());
453    }
454}