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/// A batch of operations with metadata (spec Section 4.1).
13///
14/// Edits are standalone patches. They contain no parent references;
15/// ordering is provided by on-chain governance.
16#[derive(Debug, Clone, PartialEq)]
17pub struct Edit<'a> {
18    /// The edit's unique identifier.
19    pub id: Id,
20    /// Optional human-readable name.
21    pub name: Cow<'a, str>,
22    /// Author entity IDs.
23    pub authors: Vec<Id>,
24    /// Creation timestamp (metadata only, not used for conflict resolution).
25    pub created_at: i64,
26    /// Operations in this edit.
27    pub ops: Vec<Op<'a>>,
28}
29
30impl<'a> Edit<'a> {
31    /// Creates a new empty edit with the given ID.
32    pub fn new(id: Id) -> Self {
33        Self {
34            id,
35            name: Cow::Borrowed(""),
36            authors: Vec::new(),
37            created_at: 0,
38            ops: Vec::new(),
39        }
40    }
41
42    /// Creates a new empty edit with the given ID and name.
43    pub fn with_name(id: Id, name: impl Into<Cow<'a, str>>) -> Self {
44        Self {
45            id,
46            name: name.into(),
47            authors: Vec::new(),
48            created_at: 0,
49            ops: Vec::new(),
50        }
51    }
52}
53
54/// Wire-format dictionaries for encoding/decoding.
55///
56/// These dictionaries map between full IDs and compact indices
57/// within an edit.
58#[derive(Debug, Clone, Default)]
59pub struct WireDictionaries {
60    /// Properties dictionary: (ID, DataType) pairs.
61    pub properties: Vec<(Id, DataType)>,
62    /// Relation type IDs.
63    pub relation_types: Vec<Id>,
64    /// Language entity IDs for localized TEXT values.
65    pub languages: Vec<Id>,
66    /// Unit entity IDs for numerical values.
67    pub units: Vec<Id>,
68    /// Object IDs (entities and relations).
69    pub objects: Vec<Id>,
70}
71
72impl WireDictionaries {
73    /// Creates empty dictionaries.
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Looks up a property ID by index.
79    pub fn get_property(&self, index: usize) -> Option<&(Id, DataType)> {
80        self.properties.get(index)
81    }
82
83    /// Looks up a relation type ID by index.
84    pub fn get_relation_type(&self, index: usize) -> Option<&Id> {
85        self.relation_types.get(index)
86    }
87
88    /// Looks up a language ID by index.
89    ///
90    /// Index 0 means default (no language), returns None.
91    /// Index 1+ maps to languages[index-1].
92    pub fn get_language(&self, index: usize) -> Option<&Id> {
93        if index == 0 {
94            None
95        } else {
96            self.languages.get(index - 1)
97        }
98    }
99
100    /// Looks up a unit ID by index.
101    ///
102    /// Index 0 means no unit, returns None.
103    /// Index 1+ maps to units[index-1].
104    pub fn get_unit(&self, index: usize) -> Option<&Id> {
105        if index == 0 {
106            None
107        } else {
108            self.units.get(index - 1)
109        }
110    }
111
112    /// Looks up an object ID by index.
113    pub fn get_object(&self, index: usize) -> Option<&Id> {
114        self.objects.get(index)
115    }
116}
117
118/// Builder for constructing wire dictionaries during encoding.
119///
120/// Uses FxHashMap for faster hashing of 16-byte IDs.
121#[derive(Debug, Clone, Default)]
122pub struct DictionaryBuilder {
123    properties: Vec<(Id, DataType)>,
124    property_indices: FxHashMap<Id, usize>,
125    relation_types: Vec<Id>,
126    relation_type_indices: FxHashMap<Id, usize>,
127    languages: Vec<Id>,
128    language_indices: FxHashMap<Id, usize>,
129    units: Vec<Id>,
130    unit_indices: FxHashMap<Id, usize>,
131    objects: Vec<Id>,
132    object_indices: FxHashMap<Id, usize>,
133}
134
135impl DictionaryBuilder {
136    /// Creates a new empty builder.
137    pub fn new() -> Self {
138        Self::default()
139    }
140
141    /// Creates a new builder with pre-allocated capacity.
142    ///
143    /// `estimated_ops` is used to estimate dictionary sizes:
144    /// - properties: ~estimated_ops / 4 (entities average ~4 properties)
145    /// - relation_types: ~estimated_ops / 20 (fewer unique relation types)
146    /// - languages: 4 (typically few languages per edit)
147    /// - units: 4 (typically few units per edit)
148    /// - objects: ~estimated_ops / 2 (many ops reference existing objects)
149    pub fn with_capacity(estimated_ops: usize) -> Self {
150        let prop_cap = estimated_ops / 4 + 1;
151        let rel_cap = estimated_ops / 20 + 1;
152        let lang_cap = 4;
153        let unit_cap = 4;
154        let obj_cap = estimated_ops / 2 + 1;
155
156        Self {
157            properties: Vec::with_capacity(prop_cap),
158            property_indices: FxHashMap::with_capacity_and_hasher(prop_cap, Default::default()),
159            relation_types: Vec::with_capacity(rel_cap),
160            relation_type_indices: FxHashMap::with_capacity_and_hasher(rel_cap, Default::default()),
161            languages: Vec::with_capacity(lang_cap),
162            language_indices: FxHashMap::with_capacity_and_hasher(lang_cap, Default::default()),
163            units: Vec::with_capacity(unit_cap),
164            unit_indices: FxHashMap::with_capacity_and_hasher(unit_cap, Default::default()),
165            objects: Vec::with_capacity(obj_cap),
166            object_indices: FxHashMap::with_capacity_and_hasher(obj_cap, Default::default()),
167        }
168    }
169
170    /// Adds or gets the index for a property.
171    pub fn add_property(&mut self, id: Id, data_type: DataType) -> usize {
172        if let Some(&idx) = self.property_indices.get(&id) {
173            idx
174        } else {
175            let idx = self.properties.len();
176            self.properties.push((id, data_type));
177            self.property_indices.insert(id, idx);
178            idx
179        }
180    }
181
182    /// Adds or gets the index for a relation type.
183    pub fn add_relation_type(&mut self, id: Id) -> usize {
184        if let Some(&idx) = self.relation_type_indices.get(&id) {
185            idx
186        } else {
187            let idx = self.relation_types.len();
188            self.relation_types.push(id);
189            self.relation_type_indices.insert(id, idx);
190            idx
191        }
192    }
193
194    /// Adds or gets the index for a language.
195    ///
196    /// Returns 0 for default (no language), 1+ for actual languages.
197    pub fn add_language(&mut self, id: Option<Id>) -> usize {
198        match id {
199            None => 0,
200            Some(lang_id) => {
201                if let Some(&idx) = self.language_indices.get(&lang_id) {
202                    idx + 1
203                } else {
204                    let idx = self.languages.len();
205                    self.languages.push(lang_id);
206                    self.language_indices.insert(lang_id, idx);
207                    idx + 1
208                }
209            }
210        }
211    }
212
213    /// Adds or gets the index for a unit.
214    ///
215    /// Returns 0 for no unit, 1+ for actual units.
216    pub fn add_unit(&mut self, id: Option<Id>) -> usize {
217        match id {
218            None => 0,
219            Some(unit_id) => {
220                if let Some(&idx) = self.unit_indices.get(&unit_id) {
221                    idx + 1
222                } else {
223                    let idx = self.units.len();
224                    self.units.push(unit_id);
225                    self.unit_indices.insert(unit_id, idx);
226                    idx + 1
227                }
228            }
229        }
230    }
231
232    /// Adds or gets the index for an object.
233    pub fn add_object(&mut self, id: Id) -> usize {
234        if let Some(&idx) = self.object_indices.get(&id) {
235            idx
236        } else {
237            let idx = self.objects.len();
238            self.objects.push(id);
239            self.object_indices.insert(id, idx);
240            idx
241        }
242    }
243
244    /// Builds the final wire dictionaries (consumes the builder).
245    pub fn build(self) -> WireDictionaries {
246        WireDictionaries {
247            properties: self.properties,
248            relation_types: self.relation_types,
249            languages: self.languages,
250            units: self.units,
251            objects: self.objects,
252        }
253    }
254
255    /// Returns a reference to wire dictionaries without consuming the builder.
256    /// This allows continued use of the builder for encoding while having the dictionaries.
257    pub fn as_wire_dicts(&self) -> WireDictionaries {
258        WireDictionaries {
259            properties: self.properties.clone(),
260            relation_types: self.relation_types.clone(),
261            languages: self.languages.clone(),
262            units: self.units.clone(),
263            objects: self.objects.clone(),
264        }
265    }
266
267    /// Gets the index for an existing property (for encoding).
268    pub fn get_property_index(&self, id: &Id) -> Option<usize> {
269        self.property_indices.get(id).copied()
270    }
271
272    /// Gets the index for an existing relation type (for encoding).
273    pub fn get_relation_type_index(&self, id: &Id) -> Option<usize> {
274        self.relation_type_indices.get(id).copied()
275    }
276
277    /// Gets the index for an existing language (for encoding).
278    /// Returns 0 for None, 1+ for existing languages.
279    pub fn get_language_index(&self, id: Option<&Id>) -> Option<usize> {
280        match id {
281            None => Some(0),
282            Some(lang_id) => self.language_indices.get(lang_id).map(|idx| idx + 1),
283        }
284    }
285
286    /// Gets the index for an existing object (for encoding).
287    pub fn get_object_index(&self, id: &Id) -> Option<usize> {
288        self.object_indices.get(id).copied()
289    }
290
291    /// Writes the dictionaries directly to a writer (avoids cloning).
292    pub fn write_dictionaries(&self, writer: &mut Writer) {
293        // Properties: count + (id, data_type) pairs
294        writer.write_varint(self.properties.len() as u64);
295        for (id, data_type) in &self.properties {
296            writer.write_id(id);
297            writer.write_byte(*data_type as u8);
298        }
299
300        // Relation types
301        writer.write_id_vec(&self.relation_types);
302
303        // Languages
304        writer.write_id_vec(&self.languages);
305
306        // Units
307        writer.write_id_vec(&self.units);
308
309        // Objects
310        writer.write_id_vec(&self.objects);
311    }
312
313    /// Converts this builder into a sorted canonical form.
314    ///
315    /// All dictionaries are sorted by ID bytes (lexicographic order),
316    /// and the index maps are rebuilt to reflect the new ordering.
317    ///
318    /// This is used for canonical encoding to ensure deterministic output.
319    pub fn into_sorted(self) -> Self {
320        // Sort properties by ID
321        let mut properties = self.properties;
322        properties.sort_by(|a, b| a.0.cmp(&b.0));
323        let property_indices: FxHashMap<Id, usize> = properties
324            .iter()
325            .enumerate()
326            .map(|(i, (id, _))| (*id, i))
327            .collect();
328
329        // Sort relation types by ID
330        let mut relation_types = self.relation_types;
331        relation_types.sort();
332        let relation_type_indices: FxHashMap<Id, usize> = relation_types
333            .iter()
334            .enumerate()
335            .map(|(i, id)| (*id, i))
336            .collect();
337
338        // Sort languages by ID
339        let mut languages = self.languages;
340        languages.sort();
341        let language_indices: FxHashMap<Id, usize> = languages
342            .iter()
343            .enumerate()
344            .map(|(i, id)| (*id, i))
345            .collect();
346
347        // Sort units by ID
348        let mut units = self.units;
349        units.sort();
350        let unit_indices: FxHashMap<Id, usize> = units
351            .iter()
352            .enumerate()
353            .map(|(i, id)| (*id, i))
354            .collect();
355
356        // Sort objects by ID
357        let mut objects = self.objects;
358        objects.sort();
359        let object_indices: FxHashMap<Id, usize> = objects
360            .iter()
361            .enumerate()
362            .map(|(i, id)| (*id, i))
363            .collect();
364
365        Self {
366            properties,
367            property_indices,
368            relation_types,
369            relation_type_indices,
370            languages,
371            language_indices,
372            units,
373            unit_indices,
374            objects,
375            object_indices,
376        }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn test_edit_new() {
386        let id = [1u8; 16];
387        let edit = Edit::new(id);
388        assert_eq!(edit.id, id);
389        assert!(edit.name.is_empty());
390        assert!(edit.authors.is_empty());
391        assert!(edit.ops.is_empty());
392    }
393
394    #[test]
395    fn test_dictionary_builder() {
396        let mut builder = DictionaryBuilder::new();
397
398        let prop1 = [1u8; 16];
399        let prop2 = [2u8; 16];
400
401        // First add returns 0
402        assert_eq!(builder.add_property(prop1, DataType::Text), 0);
403        // Second add of same ID returns same index
404        assert_eq!(builder.add_property(prop1, DataType::Text), 0);
405        // Different ID gets new index
406        assert_eq!(builder.add_property(prop2, DataType::Int64), 1);
407
408        let dicts = builder.build();
409        assert_eq!(dicts.properties.len(), 2);
410        assert_eq!(dicts.properties[0], (prop1, DataType::Text));
411        assert_eq!(dicts.properties[1], (prop2, DataType::Int64));
412    }
413
414    #[test]
415    fn test_language_indexing() {
416        let mut builder = DictionaryBuilder::new();
417
418        let lang1 = [10u8; 16];
419        let lang2 = [20u8; 16];
420
421        // None returns 0
422        assert_eq!(builder.add_language(None), 0);
423        // First language returns 1
424        assert_eq!(builder.add_language(Some(lang1)), 1);
425        // Same language returns same index
426        assert_eq!(builder.add_language(Some(lang1)), 1);
427        // Different language returns 2
428        assert_eq!(builder.add_language(Some(lang2)), 2);
429
430        let dicts = builder.build();
431        assert_eq!(dicts.languages.len(), 2);
432
433        // get_language(0) returns None (default)
434        assert!(dicts.get_language(0).is_none());
435        // get_language(1) returns lang1
436        assert_eq!(dicts.get_language(1), Some(&lang1));
437        // get_language(2) returns lang2
438        assert_eq!(dicts.get_language(2), Some(&lang2));
439    }
440}