Skip to main content

gram_codec/
ast.rs

1//! Abstract Syntax Tree (AST) types for gram notation
2//!
3//! The AST provides a language-agnostic, JSON-serializable representation
4//! of the `Pattern<Subject>` structure that gram notation describes.
5//!
6//! # Design Philosophy
7//!
8//! The AST mirrors the `Pattern<Subject>` structure exactly:
9//! - No graph-specific concepts (no "nodes", "edges", "relationships")
10//! - Path notation is already desugared by the parser
11//! - Just patterns and subjects - clean and conceptual
12//!
13//! # Usage
14//!
15//! ```rust
16//! use gram_codec::{parse_to_ast, AstPattern};
17//!
18//! let ast = parse_to_ast("(alice:Person {name: \"Alice\"})")?;
19//! println!("Identity: {}", ast.subject.identity);
20//! println!("Labels: {:?}", ast.subject.labels);
21//! # Ok::<(), Box<dyn std::error::Error>>(())
22//! ```
23//!
24//! # JSON Serialization
25//!
26//! The AST is designed to be JSON-serializable for cross-language use:
27//!
28//! ```rust
29//! use gram_codec::parse_to_ast;
30//! use serde_json;
31//!
32//! let ast = parse_to_ast("(alice:Person)")?;
33//! let json = serde_json::to_string(&ast)?;
34//! # Ok::<(), Box<dyn std::error::Error>>(())
35//! ```
36
37use serde::{Deserialize, Serialize};
38use std::collections::HashMap;
39
40/// Abstract Syntax Tree representation of a Pattern
41///
42/// This is the minimal, language-agnostic output format from pattern-rs.
43/// It mirrors the `Pattern<Subject>` structure exactly.
44///
45/// # Structure
46///
47/// - `subject`: The value of this pattern (identity, labels, properties)
48/// - `elements`: Child patterns (recursive structure)
49///
50/// # Examples
51///
52/// Simple node:
53/// ```json
54/// {
55///   "subject": {
56///     "identity": "alice",
57///     "labels": ["Person"],
58///     "properties": {"name": "Alice"}
59///   },
60///   "elements": []
61/// }
62/// ```
63///
64/// Subject pattern with elements:
65/// ```json
66/// {
67///   "subject": {
68///     "identity": "team",
69///     "labels": ["Team"],
70///     "properties": {}
71///   },
72///   "elements": [
73///     {"subject": {"identity": "alice", ...}, "elements": []},
74///     {"subject": {"identity": "bob", ...}, "elements": []}
75///   ]
76/// }
77/// ```
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct AstPattern {
81    /// The subject (value) of this pattern
82    pub subject: AstSubject,
83
84    /// Child patterns (elements)
85    pub elements: Vec<AstPattern>,
86}
87
88/// Subject data - identity, labels, and properties
89///
90/// The subject provides "information about the elements" in a pattern.
91///
92/// # Examples
93///
94/// Node with identity and label:
95/// ```json
96/// {
97///   "identity": "alice",
98///   "labels": ["Person"],
99///   "properties": {}
100/// }
101/// ```
102///
103/// Anonymous node with properties:
104/// ```json
105/// {
106///   "identity": "",
107///   "labels": [],
108///   "properties": {"name": "Alice", "age": 30}
109/// }
110/// ```
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub struct AstSubject {
114    /// Identity (empty string if not specified)
115    pub identity: String,
116
117    /// Labels (type tags)
118    pub labels: Vec<String>,
119
120    /// Properties (arbitrary JSON values)
121    ///
122    /// Values use mixed serialization:
123    /// - Simple types: native JSON (string, number, boolean, array, object)
124    /// - Complex types: tagged objects (Symbol, Integer, Decimal, Range, etc.)
125    pub properties: HashMap<String, serde_json::Value>,
126}
127
128impl AstPattern {
129    /// Create an empty pattern
130    ///
131    /// Useful for representing empty gram files or as a placeholder.
132    pub fn empty() -> Self {
133        AstPattern {
134            subject: AstSubject {
135                identity: String::new(),
136                labels: Vec::new(),
137                properties: HashMap::new(),
138            },
139            elements: Vec::new(),
140        }
141    }
142}
143
144/// Wire type for parse_with_header across FFI boundaries (WASM and PyO3).
145///
146/// Serialized with `serde-wasm-bindgen json_compatible()` to produce
147/// `{ header: Record<string, unknown> | null, patterns: AstPattern[] }` in JS,
148/// or with `pythonize` to produce `{"header": dict | None, "patterns": [...]}` in Python.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ParseWithHeaderResult {
151    pub header: Option<HashMap<String, serde_json::Value>>,
152    pub patterns: Vec<AstPattern>,
153}
154
155impl ParseWithHeaderResult {
156    /// Build from the output of `gram_codec::parse_gram_with_header`.
157    pub fn from_parts(
158        header: Option<pattern_core::PropertyRecord>,
159        patterns: Vec<pattern_core::Pattern<pattern_core::Subject>>,
160    ) -> Self {
161        ParseWithHeaderResult {
162            header: header.map(|h| {
163                h.iter()
164                    .map(|(k, v)| (k.clone(), value_to_json(v)))
165                    .collect()
166            }),
167            patterns: patterns.iter().map(AstPattern::from_pattern).collect(),
168        }
169    }
170
171    /// Reconstruct the header as a `PropertyRecord` for `to_gram_with_header`.
172    pub fn header_to_record(&self) -> Result<Option<pattern_core::PropertyRecord>, String> {
173        match &self.header {
174            None => Ok(None),
175            Some(map) => {
176                let record: pattern_core::PropertyRecord = map
177                    .iter()
178                    .map(|(k, v)| json_to_value(v).map(|val| (k.clone(), val)))
179                    .collect::<Result<_, _>>()?;
180                Ok(Some(record))
181            }
182        }
183    }
184}
185
186// Conversion from Pattern<Subject> to AST
187use pattern_core::{Pattern, Subject, Value};
188
189impl AstPattern {
190    /// Convert from native `Pattern<Subject>` to AST
191    ///
192    /// This is the core conversion function that transforms the Rust
193    /// Pattern structure into a JSON-serializable AST.
194    ///
195    /// # Examples
196    ///
197    /// ```rust
198    /// use gram_codec::AstPattern;
199    /// use pattern_core::{Pattern, Subject, Symbol};
200    /// use std::collections::{HashSet, HashMap};
201    ///
202    /// let subject = Subject {
203    ///     identity: Symbol("alice".to_string()),
204    ///     labels: HashSet::new(),
205    ///     properties: HashMap::new(),
206    /// };
207    /// let pattern = Pattern::point(subject);
208    /// let ast = AstPattern::from_pattern(&pattern);
209    /// assert_eq!(ast.subject.identity, "alice");
210    /// ```
211    /// Convert from AST back to native `Pattern<Subject>`.
212    pub fn to_pattern(&self) -> Result<Pattern<Subject>, String> {
213        crate::json::ast_to_pattern(self)
214    }
215
216    pub fn from_pattern(pattern: &Pattern<Subject>) -> Self {
217        let subject = pattern.value();
218
219        AstPattern {
220            subject: AstSubject {
221                identity: subject.identity.0.clone(),
222                labels: subject.labels.iter().cloned().collect(),
223                properties: subject
224                    .properties
225                    .iter()
226                    .map(|(k, v)| (k.clone(), value_to_json(v)))
227                    .collect(),
228            },
229            elements: pattern
230                .elements()
231                .iter()
232                .map(AstPattern::from_pattern)
233                .collect(),
234        }
235    }
236}
237
238/// Convert a Value to JSON using canonical format (aligned with gram-hs)
239///
240/// # Strategy
241///
242/// **Native JSON** (simple, common types):
243/// - VInteger, VDecimal, VBoolean, VString, VArray, VMap
244///
245/// **Tagged objects** (complex, need disambiguation):
246/// - VSymbol, VRange, VMeasurement, VTaggedString
247///
248/// This matches the gram-hs canonical format:
249/// - Numbers (integer/decimal) use native JSON
250/// - Complex types use lowercase type discriminators
251fn value_to_json(value: &Value) -> serde_json::Value {
252    match value {
253        // Native JSON types (simple, common)
254        Value::VInteger(i) => serde_json::Value::Number((*i).into()),
255
256        Value::VDecimal(d) => {
257            // Convert f64 to serde_json::Number
258            serde_json::Number::from_f64(*d)
259                .map(serde_json::Value::Number)
260                .unwrap_or_else(|| serde_json::Value::Null)
261        }
262
263        Value::VBoolean(b) => serde_json::Value::Bool(*b),
264
265        Value::VString(s) => serde_json::Value::String(s.clone()),
266
267        Value::VArray(arr) => serde_json::Value::Array(arr.iter().map(value_to_json).collect()),
268
269        Value::VMap(map) => serde_json::Value::Object(
270            map.iter()
271                .map(|(k, v)| (k.clone(), value_to_json(v)))
272                .collect(),
273        ),
274
275        // Tagged types (need disambiguation or structure)
276        // Use lowercase type discriminators to match gram-hs canonical format
277        Value::VSymbol(sym) => serde_json::json!({
278            "type": "symbol",
279            "value": sym.clone()
280        }),
281
282        Value::VRange(range) => serde_json::json!({
283            "type": "range",
284            "lower": range.lower,
285            "upper": range.upper
286        }),
287
288        Value::VMeasurement { unit, value } => serde_json::json!({
289            "type": "measurement",
290            "unit": unit,
291            "value": value
292        }),
293
294        Value::VTaggedString { tag, content } => serde_json::json!({
295            "type": "tagged",
296            "tag": tag,
297            "content": content
298        }),
299    }
300}
301
302/// Convert a `serde_json::Value` to a `pattern_core::Value`.
303pub(crate) fn json_to_value(v: &serde_json::Value) -> Result<Value, String> {
304    use pattern_core::RangeValue;
305    match v {
306        serde_json::Value::String(s) => Ok(Value::VString(s.clone())),
307        serde_json::Value::Bool(b) => Ok(Value::VBoolean(*b)),
308        serde_json::Value::Null => {
309            Err("JSON null is not representable as a gram value".to_string())
310        }
311        serde_json::Value::Number(n) => {
312            if let Some(i) = n.as_i64() {
313                Ok(Value::VInteger(i))
314            } else if let Some(f) = n.as_f64() {
315                Ok(Value::VDecimal(f))
316            } else {
317                Err(format!(
318                    "JSON number is not representable as a gram decimal value: {}",
319                    n
320                ))
321            }
322        }
323        serde_json::Value::Array(arr) => {
324            let items: Vec<Value> = arr
325                .iter()
326                .map(json_to_value)
327                .collect::<Result<Vec<_>, _>>()?;
328            Ok(Value::VArray(items))
329        }
330        serde_json::Value::Object(obj) => {
331            if let Some(type_tag) = obj.get("type").and_then(|t| t.as_str()) {
332                match type_tag {
333                    "symbol" => {
334                        let val = obj
335                            .get("value")
336                            .and_then(|v| v.as_str())
337                            .ok_or_else(|| "symbol value must be a string".to_string())?
338                            .to_string();
339                        Ok(Value::VSymbol(val))
340                    }
341                    "range" => {
342                        let lower = obj.get("lower").and_then(|v| v.as_f64());
343                        let upper = obj.get("upper").and_then(|v| v.as_f64());
344                        Ok(Value::VRange(RangeValue { lower, upper }))
345                    }
346                    "measurement" => {
347                        let unit = obj
348                            .get("unit")
349                            .and_then(|v| v.as_str())
350                            .ok_or_else(|| "measurement unit must be a string".to_string())?
351                            .to_string();
352                        let value = obj
353                            .get("value")
354                            .and_then(|v| v.as_f64())
355                            .ok_or_else(|| "measurement value must be a number".to_string())?;
356                        Ok(Value::VMeasurement { unit, value })
357                    }
358                    "tagged" => {
359                        let tag = obj
360                            .get("tag")
361                            .and_then(|v| v.as_str())
362                            .ok_or_else(|| "tagged value tag must be a string".to_string())?
363                            .to_string();
364                        let content = obj
365                            .get("content")
366                            .and_then(|v| v.as_str())
367                            .ok_or_else(|| "tagged value content must be a string".to_string())?
368                            .to_string();
369                        Ok(Value::VTaggedString { tag, content })
370                    }
371                    _ => Err(format!("unknown tagged value type: {}", type_tag)),
372                }
373            } else {
374                let map: std::collections::HashMap<String, Value> = obj
375                    .iter()
376                    .map(|(k, v)| json_to_value(v).map(|val| (k.clone(), val)))
377                    .collect::<Result<std::collections::HashMap<_, _>, _>>()?;
378                Ok(Value::VMap(map))
379            }
380        }
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387    use pattern_core::{Pattern, Subject, Symbol};
388    use std::collections::{HashMap, HashSet};
389
390    #[test]
391    fn test_empty_pattern() {
392        let pattern = AstPattern::empty();
393        assert_eq!(pattern.subject.identity, "");
394        assert_eq!(pattern.subject.labels.len(), 0);
395        assert_eq!(pattern.subject.properties.len(), 0);
396        assert_eq!(pattern.elements.len(), 0);
397    }
398
399    #[test]
400    fn test_json_serialization() {
401        let pattern = AstPattern {
402            subject: AstSubject {
403                identity: "alice".to_string(),
404                labels: vec!["Person".to_string()],
405                properties: {
406                    let mut props = HashMap::new();
407                    props.insert("name".to_string(), serde_json::json!("Alice"));
408                    props
409                },
410            },
411            elements: vec![],
412        };
413
414        // Serialize to JSON
415        let json = serde_json::to_string(&pattern).unwrap();
416        assert!(json.contains("alice"));
417        assert!(json.contains("Person"));
418
419        // Deserialize back
420        let deserialized: AstPattern = serde_json::from_str(&json).unwrap();
421        assert_eq!(deserialized, pattern);
422    }
423
424    #[test]
425    fn test_nested_patterns() {
426        let child1 = AstPattern {
427            subject: AstSubject {
428                identity: "child1".to_string(),
429                labels: vec![],
430                properties: HashMap::new(),
431            },
432            elements: vec![],
433        };
434
435        let child2 = AstPattern {
436            subject: AstSubject {
437                identity: "child2".to_string(),
438                labels: vec![],
439                properties: HashMap::new(),
440            },
441            elements: vec![],
442        };
443
444        let parent = AstPattern {
445            subject: AstSubject {
446                identity: "parent".to_string(),
447                labels: vec![],
448                properties: HashMap::new(),
449            },
450            elements: vec![child1, child2],
451        };
452
453        assert_eq!(parent.elements.len(), 2);
454        assert_eq!(parent.elements[0].subject.identity, "child1");
455        assert_eq!(parent.elements[1].subject.identity, "child2");
456
457        // Serialize and check it works
458        let json = serde_json::to_string(&parent).unwrap();
459        let deserialized: AstPattern = serde_json::from_str(&json).unwrap();
460        assert_eq!(deserialized.elements.len(), 2);
461    }
462
463    #[test]
464    fn test_from_pattern_simple() {
465        let subject = Subject {
466            identity: Symbol("alice".to_string()),
467            labels: {
468                let mut labels = HashSet::new();
469                labels.insert("Person".to_string());
470                labels
471            },
472            properties: HashMap::new(),
473        };
474        let pattern = Pattern::point(subject);
475
476        let ast = AstPattern::from_pattern(&pattern);
477
478        assert_eq!(ast.subject.identity, "alice");
479        assert_eq!(ast.subject.labels, vec!["Person"]);
480        assert_eq!(ast.elements.len(), 0);
481    }
482
483    #[test]
484    fn test_from_pattern_with_properties() {
485        let subject = Subject {
486            identity: Symbol("alice".to_string()),
487            labels: HashSet::new(),
488            properties: {
489                let mut props = HashMap::new();
490                props.insert("name".to_string(), Value::VString("Alice".to_string()));
491                props.insert("age".to_string(), Value::VInteger(30));
492                props
493            },
494        };
495        let pattern = Pattern::point(subject);
496
497        let ast = AstPattern::from_pattern(&pattern);
498
499        assert_eq!(ast.subject.identity, "alice");
500        assert_eq!(ast.subject.properties.len(), 2);
501
502        // Check native JSON for string
503        assert_eq!(ast.subject.properties.get("name").unwrap(), "Alice");
504
505        // Check native JSON for integer (canonical format)
506        let age_value = ast.subject.properties.get("age").unwrap();
507        assert_eq!(age_value, 30); // Native JSON number, not tagged
508    }
509
510    #[test]
511    fn test_value_serialization_simple_types() {
512        // Integer (native JSON)
513        let v = value_to_json(&Value::VInteger(42));
514        assert_eq!(v, serde_json::json!(42));
515        assert!(v.is_number());
516
517        // Decimal (native JSON)
518        let v = value_to_json(&Value::VDecimal(3.14));
519        assert_eq!(v, serde_json::json!(3.14));
520        assert!(v.is_number());
521
522        // Boolean
523        let v = value_to_json(&Value::VBoolean(true));
524        assert_eq!(v, serde_json::Value::Bool(true));
525
526        // String
527        let v = value_to_json(&Value::VString("hello".to_string()));
528        assert_eq!(v, serde_json::Value::String("hello".to_string()));
529
530        // Array (with native JSON integers)
531        let v = value_to_json(&Value::VArray(vec![Value::VInteger(1), Value::VInteger(2)]));
532        assert!(v.is_array());
533        assert_eq!(v.as_array().unwrap().len(), 2);
534        assert_eq!(v.as_array().unwrap()[0], serde_json::json!(1));
535        assert_eq!(v.as_array().unwrap()[1], serde_json::json!(2));
536    }
537
538    #[test]
539    fn test_value_serialization_tagged_types() {
540        // Symbol (lowercase type discriminator)
541        let v = value_to_json(&Value::VSymbol("user123".to_string()));
542        assert_eq!(v["type"], "symbol");
543        assert_eq!(v["value"], "user123");
544
545        // Range (lowercase type discriminator)
546        let v = value_to_json(&Value::VRange(pattern_core::RangeValue {
547            lower: Some(1.0),
548            upper: Some(10.0),
549        }));
550        assert_eq!(v["type"], "range");
551        assert_eq!(v["lower"], 1.0);
552        assert_eq!(v["upper"], 10.0);
553
554        // Measurement (lowercase type discriminator)
555        let v = value_to_json(&Value::VMeasurement {
556            unit: "cm".to_string(),
557            value: 168.0,
558        });
559        assert_eq!(v["type"], "measurement");
560        assert_eq!(v["value"], 168.0);
561        assert_eq!(v["unit"], "cm");
562
563        // Tagged string (lowercase type discriminator)
564        let v = value_to_json(&Value::VTaggedString {
565            tag: "date".to_string(),
566            content: "2024-01-09".to_string(),
567        });
568        assert_eq!(v["type"], "tagged");
569        assert_eq!(v["tag"], "date");
570        assert_eq!(v["content"], "2024-01-09");
571    }
572
573    #[test]
574    fn test_value_serialization_map() {
575        let mut map = HashMap::new();
576        map.insert("key1".to_string(), Value::VString("value1".to_string()));
577        map.insert("key2".to_string(), Value::VInteger(42));
578
579        let v = value_to_json(&Value::VMap(map));
580
581        assert!(v.is_object());
582        assert_eq!(v["key1"], "value1");
583        // Integer is now native JSON, not tagged
584        assert_eq!(v["key2"], 42);
585    }
586}