hedl_core/
document.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Document structure for parsed HEDL.
19
20use crate::Value;
21use std::collections::BTreeMap;
22
23/// A node in a matrix list.
24#[derive(Debug, Clone, PartialEq, Default)]
25pub struct Node {
26    /// The type name (from schema).
27    pub type_name: String,
28    /// The node's ID (first column value).
29    pub id: String,
30    /// Field values (aligned with schema columns).
31    pub fields: Vec<Value>,
32    /// Child nodes grouped by type (from NEST relationships).
33    pub children: BTreeMap<String, Vec<Node>>,
34    /// Optional count of direct children (for LLM comprehension hints).
35    pub child_count: Option<usize>,
36}
37
38impl Node {
39    /// Create a new node.
40    pub fn new(type_name: impl Into<String>, id: impl Into<String>, fields: Vec<Value>) -> Self {
41        Self {
42            type_name: type_name.into(),
43            id: id.into(),
44            fields,
45            children: BTreeMap::new(),
46            child_count: None,
47        }
48    }
49
50    /// Get a field value by column index.
51    pub fn get_field(&self, index: usize) -> Option<&Value> {
52        self.fields.get(index)
53    }
54
55    /// Add a child node.
56    pub fn add_child(&mut self, child_type: impl Into<String>, child: Node) {
57        self.children
58            .entry(child_type.into())
59            .or_default()
60            .push(child);
61    }
62
63    /// Set the child count hint (for LLM comprehension).
64    pub fn set_child_count(&mut self, count: usize) {
65        self.child_count = Some(count);
66    }
67
68    /// Create a new node with a child count hint.
69    pub fn with_child_count(
70        type_name: impl Into<String>,
71        id: impl Into<String>,
72        fields: Vec<Value>,
73        child_count: usize,
74    ) -> Self {
75        Self {
76            type_name: type_name.into(),
77            id: id.into(),
78            fields,
79            children: BTreeMap::new(),
80            child_count: Some(child_count),
81        }
82    }
83}
84
85/// A typed matrix list with schema.
86#[derive(Debug, Clone, PartialEq)]
87pub struct MatrixList {
88    /// The type name.
89    pub type_name: String,
90    /// Column names (schema).
91    pub schema: Vec<String>,
92    /// Row data as nodes.
93    pub rows: Vec<Node>,
94    /// Optional count hint for LLM comprehension (e.g., `teams(3): @Team`).
95    pub count_hint: Option<usize>,
96}
97
98impl MatrixList {
99    /// Create a new matrix list.
100    pub fn new(type_name: impl Into<String>, schema: Vec<String>) -> Self {
101        Self {
102            type_name: type_name.into(),
103            schema,
104            rows: Vec::new(),
105            count_hint: None,
106        }
107    }
108
109    /// Create a new matrix list with rows.
110    pub fn with_rows(
111        type_name: impl Into<String>,
112        schema: Vec<String>,
113        rows: Vec<Node>,
114    ) -> Self {
115        Self {
116            type_name: type_name.into(),
117            schema,
118            rows,
119            count_hint: None,
120        }
121    }
122
123    /// Create a new matrix list with a count hint.
124    pub fn with_count_hint(
125        type_name: impl Into<String>,
126        schema: Vec<String>,
127        count_hint: usize,
128    ) -> Self {
129        Self {
130            type_name: type_name.into(),
131            schema,
132            rows: Vec::new(),
133            count_hint: Some(count_hint),
134        }
135    }
136
137    /// Add a row/node to the list.
138    pub fn add_row(&mut self, node: Node) {
139        self.rows.push(node);
140    }
141
142    /// Get the number of columns.
143    pub fn column_count(&self) -> usize {
144        self.schema.len()
145    }
146}
147
148/// An item in the document body.
149#[derive(Debug, Clone, PartialEq)]
150pub enum Item {
151    /// A scalar value.
152    Scalar(Value),
153    /// A nested object.
154    Object(BTreeMap<String, Item>),
155    /// A matrix list.
156    List(MatrixList),
157}
158
159impl Item {
160    /// Try to get as a scalar value.
161    pub fn as_scalar(&self) -> Option<&Value> {
162        match self {
163            Self::Scalar(v) => Some(v),
164            _ => None,
165        }
166    }
167
168    /// Try to get as an object.
169    pub fn as_object(&self) -> Option<&BTreeMap<String, Item>> {
170        match self {
171            Self::Object(o) => Some(o),
172            _ => None,
173        }
174    }
175
176    /// Try to get as a matrix list.
177    pub fn as_list(&self) -> Option<&MatrixList> {
178        match self {
179            Self::List(l) => Some(l),
180            _ => None,
181        }
182    }
183}
184
185/// A parsed HEDL document.
186#[derive(Debug, Clone, PartialEq)]
187pub struct Document {
188    /// Version (major, minor).
189    pub version: (u32, u32),
190    /// Alias definitions.
191    pub aliases: BTreeMap<String, String>,
192    /// Struct definitions (type -> columns).
193    pub structs: BTreeMap<String, Vec<String>>,
194    /// Nest relationships (parent -> child).
195    pub nests: BTreeMap<String, String>,
196    /// Root body content.
197    pub root: BTreeMap<String, Item>,
198}
199
200impl Document {
201    /// Create a new empty document.
202    pub fn new(version: (u32, u32)) -> Self {
203        Self {
204            version,
205            aliases: BTreeMap::new(),
206            structs: BTreeMap::new(),
207            nests: BTreeMap::new(),
208            root: BTreeMap::new(),
209        }
210    }
211
212    /// Get an item from the root by key.
213    pub fn get(&self, key: &str) -> Option<&Item> {
214        self.root.get(key)
215    }
216
217    /// Get a struct schema by type name.
218    pub fn get_schema(&self, type_name: &str) -> Option<&Vec<String>> {
219        self.structs.get(type_name)
220    }
221
222    /// Get the child type for a parent type (from NEST).
223    pub fn get_child_type(&self, parent_type: &str) -> Option<&String> {
224        self.nests.get(parent_type)
225    }
226
227    /// Expand an alias key to its value.
228    pub fn expand_alias(&self, key: &str) -> Option<&String> {
229        self.aliases.get(key)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    // ==================== Node tests ====================
238
239    #[test]
240    fn test_node_new() {
241        let node = Node::new("User", "user-1", vec![Value::Int(42)]);
242        assert_eq!(node.type_name, "User");
243        assert_eq!(node.id, "user-1");
244        assert_eq!(node.fields.len(), 1);
245        assert!(node.children.is_empty());
246    }
247
248    #[test]
249    fn test_node_get_field() {
250        let node = Node::new(
251            "User",
252            "1",
253            vec![Value::Int(1), Value::String("name".to_string())],
254        );
255        assert_eq!(node.get_field(0), Some(&Value::Int(1)));
256        assert_eq!(node.get_field(1), Some(&Value::String("name".to_string())));
257        assert_eq!(node.get_field(2), None);
258    }
259
260    #[test]
261    fn test_node_add_child() {
262        let mut parent = Node::new("User", "1", vec![]);
263        let child = Node::new("Post", "p1", vec![]);
264        parent.add_child("Post", child);
265
266        assert!(parent.children.contains_key("Post"));
267        assert_eq!(parent.children["Post"].len(), 1);
268    }
269
270    #[test]
271    fn test_node_add_multiple_children_same_type() {
272        let mut parent = Node::new("User", "1", vec![]);
273        parent.add_child("Post", Node::new("Post", "p1", vec![]));
274        parent.add_child("Post", Node::new("Post", "p2", vec![]));
275
276        assert_eq!(parent.children["Post"].len(), 2);
277    }
278
279    #[test]
280    fn test_node_add_children_different_types() {
281        let mut parent = Node::new("User", "1", vec![]);
282        parent.add_child("Post", Node::new("Post", "p1", vec![]));
283        parent.add_child("Comment", Node::new("Comment", "c1", vec![]));
284
285        assert_eq!(parent.children.len(), 2);
286        assert!(parent.children.contains_key("Post"));
287        assert!(parent.children.contains_key("Comment"));
288    }
289
290    #[test]
291    fn test_node_equality() {
292        let a = Node::new("User", "1", vec![Value::Int(42)]);
293        let b = Node::new("User", "1", vec![Value::Int(42)]);
294        assert_eq!(a, b);
295    }
296
297    #[test]
298    fn test_node_clone() {
299        let mut original = Node::new("User", "1", vec![Value::Int(42)]);
300        original.add_child("Post", Node::new("Post", "p1", vec![]));
301        let cloned = original.clone();
302        assert_eq!(original, cloned);
303    }
304
305    #[test]
306    fn test_node_debug() {
307        let node = Node::new("User", "1", vec![]);
308        let debug = format!("{:?}", node);
309        assert!(debug.contains("User"));
310        assert!(debug.contains("type_name"));
311    }
312
313    // ==================== MatrixList tests ====================
314
315    #[test]
316    fn test_matrix_list_new() {
317        let list = MatrixList::new("User", vec!["id".to_string(), "name".to_string()]);
318        assert_eq!(list.type_name, "User");
319        assert_eq!(list.schema, vec!["id", "name"]);
320        assert!(list.rows.is_empty());
321    }
322
323    #[test]
324    fn test_matrix_list_add_row() {
325        let mut list = MatrixList::new("User", vec!["id".to_string()]);
326        list.add_row(Node::new("User", "1", vec![]));
327        assert_eq!(list.rows.len(), 1);
328    }
329
330    #[test]
331    fn test_matrix_list_column_count() {
332        let list = MatrixList::new(
333            "User",
334            vec!["a".to_string(), "b".to_string(), "c".to_string()],
335        );
336        assert_eq!(list.column_count(), 3);
337    }
338
339    #[test]
340    fn test_matrix_list_empty_schema() {
341        let list = MatrixList::new("Empty", vec![]);
342        assert_eq!(list.column_count(), 0);
343    }
344
345    #[test]
346    fn test_matrix_list_equality() {
347        let mut a = MatrixList::new("User", vec!["id".to_string()]);
348        a.add_row(Node::new("User", "1", vec![]));
349        let mut b = MatrixList::new("User", vec!["id".to_string()]);
350        b.add_row(Node::new("User", "1", vec![]));
351        assert_eq!(a, b);
352    }
353
354    #[test]
355    fn test_matrix_list_clone() {
356        let mut list = MatrixList::new("User", vec!["id".to_string()]);
357        list.add_row(Node::new("User", "1", vec![]));
358        let cloned = list.clone();
359        assert_eq!(list, cloned);
360    }
361
362    // ==================== Item tests ====================
363
364    #[test]
365    fn test_item_scalar() {
366        let item = Item::Scalar(Value::Int(42));
367        assert!(item.as_scalar().is_some());
368        assert!(item.as_object().is_none());
369        assert!(item.as_list().is_none());
370    }
371
372    #[test]
373    fn test_item_object() {
374        let mut obj = BTreeMap::new();
375        obj.insert("key".to_string(), Item::Scalar(Value::Int(1)));
376        let item = Item::Object(obj);
377        assert!(item.as_object().is_some());
378        assert!(item.as_scalar().is_none());
379    }
380
381    #[test]
382    fn test_item_list() {
383        let list = MatrixList::new("User", vec!["id".to_string()]);
384        let item = Item::List(list);
385        assert!(item.as_list().is_some());
386        assert!(item.as_scalar().is_none());
387    }
388
389    #[test]
390    fn test_item_as_scalar_returns_value() {
391        let item = Item::Scalar(Value::String("hello".to_string()));
392        let value = item.as_scalar().unwrap();
393        assert_eq!(value.as_str(), Some("hello"));
394    }
395
396    #[test]
397    fn test_item_as_object_returns_map() {
398        let mut obj = BTreeMap::new();
399        obj.insert("a".to_string(), Item::Scalar(Value::Int(1)));
400        let item = Item::Object(obj);
401        let map = item.as_object().unwrap();
402        assert!(map.contains_key("a"));
403    }
404
405    #[test]
406    fn test_item_equality() {
407        let a = Item::Scalar(Value::Int(42));
408        let b = Item::Scalar(Value::Int(42));
409        assert_eq!(a, b);
410    }
411
412    #[test]
413    fn test_item_clone() {
414        let item = Item::Scalar(Value::String("test".to_string()));
415        let cloned = item.clone();
416        assert_eq!(item, cloned);
417    }
418
419    // ==================== Document tests ====================
420
421    #[test]
422    fn test_document_new() {
423        let doc = Document::new((1, 0));
424        assert_eq!(doc.version, (1, 0));
425        assert!(doc.aliases.is_empty());
426        assert!(doc.structs.is_empty());
427        assert!(doc.nests.is_empty());
428        assert!(doc.root.is_empty());
429    }
430
431    #[test]
432    fn test_document_get() {
433        let mut doc = Document::new((1, 0));
434        doc.root
435            .insert("key".to_string(), Item::Scalar(Value::Int(42)));
436        assert!(doc.get("key").is_some());
437        assert!(doc.get("missing").is_none());
438    }
439
440    #[test]
441    fn test_document_get_schema() {
442        let mut doc = Document::new((1, 0));
443        doc.structs.insert(
444            "User".to_string(),
445            vec!["id".to_string(), "name".to_string()],
446        );
447        let schema = doc.get_schema("User").unwrap();
448        assert_eq!(schema, &vec!["id".to_string(), "name".to_string()]);
449        assert!(doc.get_schema("Missing").is_none());
450    }
451
452    #[test]
453    fn test_document_get_child_type() {
454        let mut doc = Document::new((1, 0));
455        doc.nests.insert("User".to_string(), "Post".to_string());
456        assert_eq!(doc.get_child_type("User"), Some(&"Post".to_string()));
457        assert!(doc.get_child_type("Post").is_none());
458    }
459
460    #[test]
461    fn test_document_expand_alias() {
462        let mut doc = Document::new((1, 0));
463        doc.aliases.insert("active".to_string(), "true".to_string());
464        assert_eq!(doc.expand_alias("active"), Some(&"true".to_string()));
465        assert!(doc.expand_alias("missing").is_none());
466    }
467
468    #[test]
469    fn test_document_equality() {
470        let a = Document::new((1, 0));
471        let b = Document::new((1, 0));
472        assert_eq!(a, b);
473    }
474
475    #[test]
476    fn test_document_clone() {
477        let mut doc = Document::new((1, 0));
478        doc.aliases.insert("key".to_string(), "value".to_string());
479        let cloned = doc.clone();
480        assert_eq!(doc, cloned);
481    }
482
483    #[test]
484    fn test_document_debug() {
485        let doc = Document::new((1, 0));
486        let debug = format!("{:?}", doc);
487        assert!(debug.contains("version"));
488        assert!(debug.contains("aliases"));
489    }
490
491    // ==================== Edge cases ====================
492
493    #[test]
494    fn test_node_empty_fields() {
495        let node = Node::new("Type", "id", vec![]);
496        assert!(node.fields.is_empty());
497        assert!(node.get_field(0).is_none());
498    }
499
500    #[test]
501    fn test_node_unicode_id() {
502        let node = Node::new("User", "日本語", vec![]);
503        assert_eq!(node.id, "日本語");
504    }
505
506    #[test]
507    fn test_document_version_zero() {
508        let doc = Document::new((0, 0));
509        assert_eq!(doc.version, (0, 0));
510    }
511
512    #[test]
513    fn test_document_large_version() {
514        let doc = Document::new((999, 999));
515        assert_eq!(doc.version, (999, 999));
516    }
517
518    #[test]
519    fn test_nested_items() {
520        let mut inner = BTreeMap::new();
521        inner.insert("nested".to_string(), Item::Scalar(Value::Int(42)));
522
523        let mut outer = BTreeMap::new();
524        outer.insert("inner".to_string(), Item::Object(inner));
525
526        let item = Item::Object(outer);
527        let obj = item.as_object().unwrap();
528        let inner_item = obj.get("inner").unwrap();
529        let inner_obj = inner_item.as_object().unwrap();
530        assert!(inner_obj.contains_key("nested"));
531    }
532
533    #[test]
534    fn test_deeply_nested_nodes() {
535        let mut root = Node::new("A", "a", vec![]);
536        let mut child = Node::new("B", "b", vec![]);
537        child.add_child("C", Node::new("C", "c", vec![]));
538        root.add_child("B", child);
539
540        assert_eq!(root.children["B"].len(), 1);
541        assert_eq!(root.children["B"][0].children["C"].len(), 1);
542    }
543}