grc_20/model/
edit.rs

1//! Edit structure for batched operations.
2//!
3//! Edits are standalone patches containing a batch of ops with metadata.
4
5use std::borrow::Cow;
6
7use rustc_hash::FxHashMap;
8
9use crate::codec::primitives::Writer;
10use crate::model::{DataType, Id, Op};
11
12/// An edge in a context path (spec Section 4.5).
13///
14/// Represents a step in the path from the root entity to the changed entity.
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct ContextEdge {
17    /// The relation type ID for this edge (e.g., BLOCKS_ID).
18    pub type_id: Id,
19    /// The target entity ID at this edge.
20    pub to_entity_id: Id,
21}
22
23/// Context metadata for grouping changes (spec Section 4.5).
24///
25/// Provides the path from a root entity to the changed entity,
26/// enabling context-aware change grouping (e.g., grouping block changes
27/// under their parent entity).
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct Context {
30    /// The root entity for this context.
31    pub root_id: Id,
32    /// Path from root to the changed entity.
33    pub edges: Vec<ContextEdge>,
34}
35
36/// A batch of operations with metadata (spec Section 4.1).
37///
38/// Edits are standalone patches. They contain no parent references;
39/// ordering is provided by on-chain governance.
40#[derive(Debug, Clone, PartialEq)]
41pub struct Edit<'a> {
42    /// The edit's unique identifier.
43    pub id: Id,
44    /// Optional human-readable name.
45    pub name: Cow<'a, str>,
46    /// Author entity IDs.
47    pub authors: Vec<Id>,
48    /// Creation timestamp (metadata only, not used for conflict resolution).
49    pub created_at: i64,
50    /// Operations in this edit.
51    pub ops: Vec<Op<'a>>,
52}
53
54impl<'a> Edit<'a> {
55    /// Creates a new empty edit with the given ID.
56    pub fn new(id: Id) -> Self {
57        Self {
58            id,
59            name: Cow::Borrowed(""),
60            authors: Vec::new(),
61            created_at: 0,
62            ops: Vec::new(),
63        }
64    }
65
66    /// Creates a new empty edit with the given ID and name.
67    pub fn with_name(id: Id, name: impl Into<Cow<'a, str>>) -> Self {
68        Self {
69            id,
70            name: name.into(),
71            authors: Vec::new(),
72            created_at: 0,
73            ops: Vec::new(),
74        }
75    }
76}
77
78/// Wire-format dictionaries for encoding/decoding.
79///
80/// These dictionaries map between full IDs and compact indices
81/// within an edit.
82#[derive(Debug, Clone, Default)]
83pub struct WireDictionaries {
84    /// Properties dictionary: (ID, DataType) pairs.
85    pub properties: Vec<(Id, DataType)>,
86    /// Relation type IDs.
87    pub relation_types: Vec<Id>,
88    /// Language entity IDs for localized TEXT values.
89    pub languages: Vec<Id>,
90    /// Unit entity IDs for numerical values.
91    pub units: Vec<Id>,
92    /// Object IDs (entities and relations).
93    pub objects: Vec<Id>,
94    /// Context IDs (root_ids and edge to_entity_ids) - used during encoding/decoding.
95    pub context_ids: Vec<Id>,
96    /// Decoded contexts array - used by op decoders to resolve context_ref to Context.
97    pub contexts: Vec<Context>,
98}
99
100impl WireDictionaries {
101    /// Creates empty dictionaries.
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    /// Looks up a property ID by index.
107    pub fn get_property(&self, index: usize) -> Option<&(Id, DataType)> {
108        self.properties.get(index)
109    }
110
111    /// Looks up a relation type ID by index.
112    pub fn get_relation_type(&self, index: usize) -> Option<&Id> {
113        self.relation_types.get(index)
114    }
115
116    /// Looks up a language ID by index.
117    ///
118    /// Index 0 means default (no language), returns None.
119    /// Index 1+ maps to languages[index-1].
120    pub fn get_language(&self, index: usize) -> Option<&Id> {
121        if index == 0 {
122            None
123        } else {
124            self.languages.get(index - 1)
125        }
126    }
127
128    /// Looks up a unit ID by index.
129    ///
130    /// Index 0 means no unit, returns None.
131    /// Index 1+ maps to units[index-1].
132    pub fn get_unit(&self, index: usize) -> Option<&Id> {
133        if index == 0 {
134            None
135        } else {
136            self.units.get(index - 1)
137        }
138    }
139
140    /// Looks up an object ID by index.
141    pub fn get_object(&self, index: usize) -> Option<&Id> {
142        self.objects.get(index)
143    }
144
145    /// Looks up a context ID by index.
146    pub fn get_context_id(&self, index: usize) -> Option<&Id> {
147        self.context_ids.get(index)
148    }
149
150    /// Looks up a context by index.
151    pub fn get_context(&self, index: usize) -> Option<&Context> {
152        self.contexts.get(index)
153    }
154}
155
156/// Builder for constructing wire dictionaries during encoding.
157///
158/// Uses FxHashMap for faster hashing of 16-byte IDs.
159#[derive(Debug, Clone, Default)]
160pub struct DictionaryBuilder {
161    properties: Vec<(Id, DataType)>,
162    property_indices: FxHashMap<Id, usize>,
163    relation_types: Vec<Id>,
164    relation_type_indices: FxHashMap<Id, usize>,
165    languages: Vec<Id>,
166    language_indices: FxHashMap<Id, usize>,
167    units: Vec<Id>,
168    unit_indices: FxHashMap<Id, usize>,
169    objects: Vec<Id>,
170    object_indices: FxHashMap<Id, usize>,
171    context_ids: Vec<Id>,
172    context_id_indices: FxHashMap<Id, usize>,
173    contexts: Vec<Context>,
174    context_indices: FxHashMap<Context, usize>,
175}
176
177impl DictionaryBuilder {
178    /// Creates a new empty builder.
179    pub fn new() -> Self {
180        Self::default()
181    }
182
183    /// Creates a new builder with pre-allocated capacity.
184    ///
185    /// `estimated_ops` is used to estimate dictionary sizes:
186    /// - properties: ~estimated_ops / 4 (entities average ~4 properties)
187    /// - relation_types: ~estimated_ops / 20 (fewer unique relation types)
188    /// - languages: 4 (typically few languages per edit)
189    /// - units: 4 (typically few units per edit)
190    /// - objects: ~estimated_ops / 2 (many ops reference existing objects)
191    /// - context_ids: 8 (typically few context IDs per edit)
192    /// - contexts: 4 (typically few unique contexts per edit)
193    pub fn with_capacity(estimated_ops: usize) -> Self {
194        let prop_cap = estimated_ops / 4 + 1;
195        let rel_cap = estimated_ops / 20 + 1;
196        let lang_cap = 4;
197        let unit_cap = 4;
198        let obj_cap = estimated_ops / 2 + 1;
199        let ctx_id_cap = 8;
200        let ctx_cap = 4;
201
202        Self {
203            properties: Vec::with_capacity(prop_cap),
204            property_indices: FxHashMap::with_capacity_and_hasher(prop_cap, Default::default()),
205            relation_types: Vec::with_capacity(rel_cap),
206            relation_type_indices: FxHashMap::with_capacity_and_hasher(rel_cap, Default::default()),
207            languages: Vec::with_capacity(lang_cap),
208            language_indices: FxHashMap::with_capacity_and_hasher(lang_cap, Default::default()),
209            units: Vec::with_capacity(unit_cap),
210            unit_indices: FxHashMap::with_capacity_and_hasher(unit_cap, Default::default()),
211            objects: Vec::with_capacity(obj_cap),
212            object_indices: FxHashMap::with_capacity_and_hasher(obj_cap, Default::default()),
213            context_ids: Vec::with_capacity(ctx_id_cap),
214            context_id_indices: FxHashMap::with_capacity_and_hasher(ctx_id_cap, Default::default()),
215            contexts: Vec::with_capacity(ctx_cap),
216            context_indices: FxHashMap::with_capacity_and_hasher(ctx_cap, Default::default()),
217        }
218    }
219
220    /// Adds or gets the index for a property.
221    pub fn add_property(&mut self, id: Id, data_type: DataType) -> usize {
222        if let Some(&idx) = self.property_indices.get(&id) {
223            idx
224        } else {
225            let idx = self.properties.len();
226            self.properties.push((id, data_type));
227            self.property_indices.insert(id, idx);
228            idx
229        }
230    }
231
232    /// Adds or gets the index for a relation type.
233    pub fn add_relation_type(&mut self, id: Id) -> usize {
234        if let Some(&idx) = self.relation_type_indices.get(&id) {
235            idx
236        } else {
237            let idx = self.relation_types.len();
238            self.relation_types.push(id);
239            self.relation_type_indices.insert(id, idx);
240            idx
241        }
242    }
243
244    /// Adds or gets the index for a language.
245    ///
246    /// Returns 0 for default (no language), 1+ for actual languages.
247    pub fn add_language(&mut self, id: Option<Id>) -> usize {
248        match id {
249            None => 0,
250            Some(lang_id) => {
251                if let Some(&idx) = self.language_indices.get(&lang_id) {
252                    idx + 1
253                } else {
254                    let idx = self.languages.len();
255                    self.languages.push(lang_id);
256                    self.language_indices.insert(lang_id, idx);
257                    idx + 1
258                }
259            }
260        }
261    }
262
263    /// Adds or gets the index for a unit.
264    ///
265    /// Returns 0 for no unit, 1+ for actual units.
266    pub fn add_unit(&mut self, id: Option<Id>) -> usize {
267        match id {
268            None => 0,
269            Some(unit_id) => {
270                if let Some(&idx) = self.unit_indices.get(&unit_id) {
271                    idx + 1
272                } else {
273                    let idx = self.units.len();
274                    self.units.push(unit_id);
275                    self.unit_indices.insert(unit_id, idx);
276                    idx + 1
277                }
278            }
279        }
280    }
281
282    /// Adds or gets the index for an object.
283    pub fn add_object(&mut self, id: Id) -> usize {
284        if let Some(&idx) = self.object_indices.get(&id) {
285            idx
286        } else {
287            let idx = self.objects.len();
288            self.objects.push(id);
289            self.object_indices.insert(id, idx);
290            idx
291        }
292    }
293
294    /// Adds or gets the index for a context ID.
295    pub fn add_context_id(&mut self, id: Id) -> usize {
296        if let Some(&idx) = self.context_id_indices.get(&id) {
297            idx
298        } else {
299            let idx = self.context_ids.len();
300            self.context_ids.push(id);
301            self.context_id_indices.insert(id, idx);
302            idx
303        }
304    }
305
306    /// Adds or gets the index for a context.
307    ///
308    /// If the context is new, registers all its IDs to the context_ids dictionary.
309    /// Returns the index into the contexts array.
310    pub fn add_context(&mut self, context: &Context) -> usize {
311        if let Some(&idx) = self.context_indices.get(context) {
312            idx
313        } else {
314            // Register all IDs in the context to context_ids dictionary
315            self.add_context_id(context.root_id);
316            for edge in &context.edges {
317                self.add_context_id(edge.type_id);
318                self.add_context_id(edge.to_entity_id);
319            }
320
321            // Add context to contexts array
322            let idx = self.contexts.len();
323            self.contexts.push(context.clone());
324            self.context_indices.insert(context.clone(), idx);
325            idx
326        }
327    }
328
329    /// Gets the index for an existing context (for encoding).
330    pub fn get_context_index(&self, context: &Context) -> Option<usize> {
331        self.context_indices.get(context).copied()
332    }
333
334    /// Builds the final wire dictionaries (consumes the builder).
335    pub fn build(self) -> WireDictionaries {
336        WireDictionaries {
337            properties: self.properties,
338            relation_types: self.relation_types,
339            languages: self.languages,
340            units: self.units,
341            objects: self.objects,
342            context_ids: self.context_ids,
343            contexts: self.contexts,
344        }
345    }
346
347    /// Returns a reference to wire dictionaries without consuming the builder.
348    /// This allows continued use of the builder for encoding while having the dictionaries.
349    pub fn as_wire_dicts(&self) -> WireDictionaries {
350        WireDictionaries {
351            properties: self.properties.clone(),
352            relation_types: self.relation_types.clone(),
353            languages: self.languages.clone(),
354            units: self.units.clone(),
355            objects: self.objects.clone(),
356            context_ids: self.context_ids.clone(),
357            contexts: self.contexts.clone(),
358        }
359    }
360
361    /// Gets the index for an existing property (for encoding).
362    pub fn get_property_index(&self, id: &Id) -> Option<usize> {
363        self.property_indices.get(id).copied()
364    }
365
366    /// Gets the index for an existing relation type (for encoding).
367    pub fn get_relation_type_index(&self, id: &Id) -> Option<usize> {
368        self.relation_type_indices.get(id).copied()
369    }
370
371    /// Gets the index for an existing language (for encoding).
372    /// Returns 0 for None, 1+ for existing languages.
373    pub fn get_language_index(&self, id: Option<&Id>) -> Option<usize> {
374        match id {
375            None => Some(0),
376            Some(lang_id) => self.language_indices.get(lang_id).map(|idx| idx + 1),
377        }
378    }
379
380    /// Gets the index for an existing object (for encoding).
381    pub fn get_object_index(&self, id: &Id) -> Option<usize> {
382        self.object_indices.get(id).copied()
383    }
384
385    /// Gets the index for an existing context ID (for encoding).
386    pub fn get_context_id_index(&self, id: &Id) -> Option<usize> {
387        self.context_id_indices.get(id).copied()
388    }
389
390    /// Writes the dictionaries directly to a writer (avoids cloning).
391    pub fn write_dictionaries(&self, writer: &mut Writer) {
392        // Properties: count + (id, data_type) pairs
393        writer.write_varint(self.properties.len() as u64);
394        for (id, data_type) in &self.properties {
395            writer.write_id(id);
396            writer.write_byte(*data_type as u8);
397        }
398
399        // Relation types
400        writer.write_id_vec(&self.relation_types);
401
402        // Languages
403        writer.write_id_vec(&self.languages);
404
405        // Units
406        writer.write_id_vec(&self.units);
407
408        // Objects
409        writer.write_id_vec(&self.objects);
410
411        // Context IDs
412        writer.write_id_vec(&self.context_ids);
413    }
414
415    /// Writes the contexts array to the writer.
416    ///
417    /// Each context is encoded as:
418    /// - root_id: varint (index into context_ids)
419    /// - edge_count: varint
420    /// - edges: for each edge: type_id (varint), to_entity_id (varint)
421    pub fn write_contexts(&self, writer: &mut Writer) {
422        writer.write_varint(self.contexts.len() as u64);
423        for ctx in &self.contexts {
424            // Root ID as context_id index
425            let root_idx = self.context_id_indices.get(&ctx.root_id)
426                .copied()
427                .expect("context root_id must be in context_ids dictionary");
428            writer.write_varint(root_idx as u64);
429
430            // Edges
431            writer.write_varint(ctx.edges.len() as u64);
432            for edge in &ctx.edges {
433                let type_idx = self.context_id_indices.get(&edge.type_id)
434                    .copied()
435                    .expect("context edge type_id must be in context_ids dictionary");
436                let to_idx = self.context_id_indices.get(&edge.to_entity_id)
437                    .copied()
438                    .expect("context edge to_entity_id must be in context_ids dictionary");
439                writer.write_varint(type_idx as u64);
440                writer.write_varint(to_idx as u64);
441            }
442        }
443    }
444
445    /// Converts this builder into a sorted canonical form.
446    ///
447    /// All dictionaries are sorted by ID bytes (lexicographic order),
448    /// and the index maps are rebuilt to reflect the new ordering.
449    ///
450    /// This is used for canonical encoding to ensure deterministic output.
451    pub fn into_sorted(self) -> Self {
452        // Sort properties by ID
453        let mut properties = self.properties;
454        properties.sort_by(|a, b| a.0.cmp(&b.0));
455        let property_indices: FxHashMap<Id, usize> = properties
456            .iter()
457            .enumerate()
458            .map(|(i, (id, _))| (*id, i))
459            .collect();
460
461        // Sort relation types by ID
462        let mut relation_types = self.relation_types;
463        relation_types.sort();
464        let relation_type_indices: FxHashMap<Id, usize> = relation_types
465            .iter()
466            .enumerate()
467            .map(|(i, id)| (*id, i))
468            .collect();
469
470        // Sort languages by ID
471        let mut languages = self.languages;
472        languages.sort();
473        let language_indices: FxHashMap<Id, usize> = languages
474            .iter()
475            .enumerate()
476            .map(|(i, id)| (*id, i))
477            .collect();
478
479        // Sort units by ID
480        let mut units = self.units;
481        units.sort();
482        let unit_indices: FxHashMap<Id, usize> = units
483            .iter()
484            .enumerate()
485            .map(|(i, id)| (*id, i))
486            .collect();
487
488        // Sort objects by ID
489        let mut objects = self.objects;
490        objects.sort();
491        let object_indices: FxHashMap<Id, usize> = objects
492            .iter()
493            .enumerate()
494            .map(|(i, id)| (*id, i))
495            .collect();
496
497        // Sort context IDs by ID
498        let mut context_ids = self.context_ids;
499        context_ids.sort();
500        let context_id_indices: FxHashMap<Id, usize> = context_ids
501            .iter()
502            .enumerate()
503            .map(|(i, id)| (*id, i))
504            .collect();
505
506        // Sort contexts by root_id, then by edges (canonically)
507        let mut contexts = self.contexts;
508        contexts.sort_by(|a, b| {
509            // First compare by root_id
510            match a.root_id.cmp(&b.root_id) {
511                std::cmp::Ordering::Equal => {
512                    // Then compare edges lexicographically
513                    let a_edges: Vec<_> = a.edges.iter().map(|e| (e.type_id, e.to_entity_id)).collect();
514                    let b_edges: Vec<_> = b.edges.iter().map(|e| (e.type_id, e.to_entity_id)).collect();
515                    a_edges.cmp(&b_edges)
516                }
517                other => other,
518            }
519        });
520        let context_indices: FxHashMap<Context, usize> = contexts
521            .iter()
522            .enumerate()
523            .map(|(i, ctx)| (ctx.clone(), i))
524            .collect();
525
526        Self {
527            properties,
528            property_indices,
529            relation_types,
530            relation_type_indices,
531            languages,
532            language_indices,
533            units,
534            unit_indices,
535            objects,
536            object_indices,
537            context_ids,
538            context_id_indices,
539            contexts,
540            context_indices,
541        }
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548
549    #[test]
550    fn test_edit_new() {
551        let id = [1u8; 16];
552        let edit = Edit::new(id);
553        assert_eq!(edit.id, id);
554        assert!(edit.name.is_empty());
555        assert!(edit.authors.is_empty());
556        assert!(edit.ops.is_empty());
557    }
558
559    #[test]
560    fn test_dictionary_builder() {
561        let mut builder = DictionaryBuilder::new();
562
563        let prop1 = [1u8; 16];
564        let prop2 = [2u8; 16];
565
566        // First add returns 0
567        assert_eq!(builder.add_property(prop1, DataType::Text), 0);
568        // Second add of same ID returns same index
569        assert_eq!(builder.add_property(prop1, DataType::Text), 0);
570        // Different ID gets new index
571        assert_eq!(builder.add_property(prop2, DataType::Int64), 1);
572
573        let dicts = builder.build();
574        assert_eq!(dicts.properties.len(), 2);
575        assert_eq!(dicts.properties[0], (prop1, DataType::Text));
576        assert_eq!(dicts.properties[1], (prop2, DataType::Int64));
577    }
578
579    #[test]
580    fn test_language_indexing() {
581        let mut builder = DictionaryBuilder::new();
582
583        let lang1 = [10u8; 16];
584        let lang2 = [20u8; 16];
585
586        // None returns 0
587        assert_eq!(builder.add_language(None), 0);
588        // First language returns 1
589        assert_eq!(builder.add_language(Some(lang1)), 1);
590        // Same language returns same index
591        assert_eq!(builder.add_language(Some(lang1)), 1);
592        // Different language returns 2
593        assert_eq!(builder.add_language(Some(lang2)), 2);
594
595        let dicts = builder.build();
596        assert_eq!(dicts.languages.len(), 2);
597
598        // get_language(0) returns None (default)
599        assert!(dicts.get_language(0).is_none());
600        // get_language(1) returns lang1
601        assert_eq!(dicts.get_language(1), Some(&lang1));
602        // get_language(2) returns lang2
603        assert_eq!(dicts.get_language(2), Some(&lang2));
604    }
605}