Skip to main content

gram_codec/
json.rs

1//! JSON interchange functions for the gram codec.
2//!
3//! These functions form the stable contract between the Rust gram-codec and
4//! native TypeScript/Python implementations. All cross-boundary communication
5//! uses the JSON interchange format documented in the data-model spec.
6//!
7//! # JSON Interchange Format
8//!
9//! The format is an array of `AstPattern` objects:
10//! ```json
11//! [
12//!   {
13//!     "subject": {
14//!       "identity": "alice",
15//!       "labels": ["Person"],
16//!       "properties": { "name": "Alice" }
17//!     },
18//!     "elements": []
19//!   }
20//! ]
21//! ```
22//!
23//! Property values use mixed serialization:
24//! - Primitives: native JSON (string, number, boolean)
25//! - Complex types: tagged objects `{ "type": "symbol"|"range"|"tagged"|"measurement", ... }`
26
27use crate::ast::AstPattern;
28use pattern_core::{Pattern, RangeValue, Subject, Symbol, Value};
29use std::collections::{HashMap, HashSet};
30
31/// Parse gram notation and return a JSON array string of `AstPattern` objects.
32///
33/// # Arguments
34///
35/// * `input` - Gram notation text
36///
37/// # Returns
38///
39/// * `Ok(String)` - JSON array of AstPattern objects
40/// * `Err(String)` - Parse error message
41///
42/// # Examples
43///
44/// ```rust
45/// use gram_codec::json::gram_parse_to_json;
46///
47/// let json = gram_parse_to_json("(alice:Person)").unwrap();
48/// assert!(json.contains("alice"));
49/// assert!(json.contains("Person"));
50/// ```
51pub fn gram_parse_to_json(input: &str) -> Result<String, String> {
52    if input.trim().is_empty() {
53        return Ok("[]".to_string());
54    }
55    let patterns = crate::parse_gram(input).map_err(|e| e.to_string())?;
56    let asts: Vec<AstPattern> = patterns.iter().map(AstPattern::from_pattern).collect();
57    serde_json::to_string(&asts).map_err(|e| e.to_string())
58}
59
60/// Serialize a JSON array of `AstPattern` objects back to gram notation.
61///
62/// # Arguments
63///
64/// * `input` - JSON array string of AstPattern objects
65///
66/// # Returns
67///
68/// * `Ok(String)` - Gram notation text
69/// * `Err(String)` - Serialization error message
70///
71/// # Examples
72///
73/// ```rust
74/// use gram_codec::json::gram_stringify_from_json;
75///
76/// let gram = gram_stringify_from_json(r#"[{"subject":{"identity":"alice","labels":["Person"],"properties":{}},"elements":[]}]"#).unwrap();
77/// assert!(gram.contains("alice"));
78/// ```
79pub fn gram_stringify_from_json(input: &str) -> Result<String, String> {
80    let asts: Vec<AstPattern> = serde_json::from_str(input).map_err(|e| e.to_string())?;
81    let patterns: Vec<Pattern<Subject>> = asts
82        .iter()
83        .map(ast_to_pattern)
84        .collect::<Result<Vec<_>, _>>()?;
85    let gram_parts: Result<Vec<String>, String> = patterns
86        .iter()
87        .map(|p| crate::to_gram_pattern(p).map_err(|e| e.to_string()))
88        .collect();
89    Ok(gram_parts?.join(" "))
90}
91
92/// Validate gram notation and return an empty string on success, or an error message.
93///
94/// # Arguments
95///
96/// * `input` - Gram notation text
97///
98/// # Returns
99///
100/// A JSON string with an array of error strings (empty array = valid).
101pub fn gram_validate_to_json(input: &str) -> String {
102    match crate::validate_gram(input) {
103        Ok(()) => "[]".to_string(),
104        Err(e) => {
105            let msg = e.to_string();
106            serde_json::to_string(&[msg]).unwrap_or_else(|_| "[]".to_string())
107        }
108    }
109}
110
111/// Convert an `AstPattern` back to a native `Pattern<Subject>`.
112fn ast_to_pattern(ast: &AstPattern) -> Result<Pattern<Subject>, String> {
113    let subject = Subject {
114        identity: Symbol(ast.subject.identity.clone()),
115        labels: ast.subject.labels.iter().cloned().collect::<HashSet<_>>(),
116        properties: ast
117            .subject
118            .properties
119            .iter()
120            .map(|(k, v)| json_to_value(v).map(|val| (k.clone(), val)))
121            .collect::<Result<HashMap<_, _>, _>>()?,
122    };
123    let elements: Vec<Pattern<Subject>> = ast
124        .elements
125        .iter()
126        .map(ast_to_pattern)
127        .collect::<Result<Vec<_>, _>>()?;
128    if elements.is_empty() {
129        Ok(Pattern::point(subject))
130    } else {
131        Ok(Pattern::pattern(subject, elements))
132    }
133}
134
135/// Convert a `serde_json::Value` back to a `pattern_core::Value`.
136fn json_to_value(v: &serde_json::Value) -> Result<Value, String> {
137    match v {
138        serde_json::Value::String(s) => Ok(Value::VString(s.clone())),
139        serde_json::Value::Bool(b) => Ok(Value::VBoolean(*b)),
140        serde_json::Value::Null => {
141            Err("JSON null is not representable as a gram value".to_string())
142        }
143        serde_json::Value::Number(n) => {
144            if let Some(i) = n.as_i64() {
145                Ok(Value::VInteger(i))
146            } else {
147                Ok(Value::VDecimal(n.as_f64().unwrap_or(0.0)))
148            }
149        }
150        serde_json::Value::Array(arr) => {
151            let items: Vec<Value> = arr
152                .iter()
153                .map(json_to_value)
154                .collect::<Result<Vec<_>, _>>()?;
155            Ok(Value::VArray(items))
156        }
157        serde_json::Value::Object(obj) => {
158            // Check for tagged objects (symbol, range, measurement, tagged string)
159            if let Some(type_tag) = obj.get("type").and_then(|t| t.as_str()) {
160                match type_tag {
161                    "symbol" => {
162                        let val = obj
163                            .get("value")
164                            .and_then(|v| v.as_str())
165                            .ok_or_else(|| "symbol value must be a string".to_string())?
166                            .to_string();
167                        Ok(Value::VSymbol(val))
168                    }
169                    "range" => {
170                        let lower = obj.get("lower").and_then(|v| v.as_f64());
171                        let upper = obj.get("upper").and_then(|v| v.as_f64());
172                        Ok(Value::VRange(RangeValue { lower, upper }))
173                    }
174                    "measurement" => {
175                        let unit = obj
176                            .get("unit")
177                            .and_then(|v| v.as_str())
178                            .ok_or_else(|| "measurement unit must be a string".to_string())?
179                            .to_string();
180                        let value = obj
181                            .get("value")
182                            .and_then(|v| v.as_f64())
183                            .ok_or_else(|| "measurement value must be a number".to_string())?;
184                        Ok(Value::VMeasurement { unit, value })
185                    }
186                    "tagged" => {
187                        let tag = obj
188                            .get("tag")
189                            .and_then(|v| v.as_str())
190                            .ok_or_else(|| "tagged value tag must be a string".to_string())?
191                            .to_string();
192                        let content = obj
193                            .get("content")
194                            .and_then(|v| v.as_str())
195                            .ok_or_else(|| "tagged value content must be a string".to_string())?
196                            .to_string();
197                        Ok(Value::VTaggedString { tag, content })
198                    }
199                    _ => Err(format!("unknown tagged value type: {}", type_tag)),
200                }
201            } else {
202                // Plain JSON object → VMap
203                let map: HashMap<String, Value> = obj
204                    .iter()
205                    .map(|(k, v)| json_to_value(v).map(|val| (k.clone(), val)))
206                    .collect::<Result<HashMap<_, _>, _>>()?;
207                Ok(Value::VMap(map))
208            }
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_parse_empty_input() {
219        assert_eq!(gram_parse_to_json("").unwrap(), "[]");
220        assert_eq!(gram_parse_to_json("   ").unwrap(), "[]");
221    }
222
223    #[test]
224    fn test_parse_simple_node() {
225        let json = gram_parse_to_json("(alice:Person)").unwrap();
226        let parsed: Vec<serde_json::Value> = serde_json::from_str(&json).unwrap();
227        assert_eq!(parsed.len(), 1);
228        assert_eq!(parsed[0]["subject"]["identity"], "alice");
229        assert_eq!(parsed[0]["subject"]["labels"][0], "Person");
230    }
231
232    #[test]
233    fn test_parse_node_with_properties() {
234        let json = gram_parse_to_json(r#"(a {name: "Alice", age: 30})"#).unwrap();
235        let parsed: Vec<serde_json::Value> = serde_json::from_str(&json).unwrap();
236        assert_eq!(parsed[0]["subject"]["properties"]["name"], "Alice");
237        assert_eq!(parsed[0]["subject"]["properties"]["age"], 30);
238    }
239
240    #[test]
241    fn test_parse_relationship() {
242        let json = gram_parse_to_json("(a)-->(b)").unwrap();
243        let parsed: Vec<serde_json::Value> = serde_json::from_str(&json).unwrap();
244        assert_eq!(parsed.len(), 1);
245        assert_eq!(parsed[0]["elements"].as_array().unwrap().len(), 2);
246    }
247
248    #[test]
249    fn test_stringify_round_trip() {
250        let original = "(alice:Person)";
251        let json = gram_parse_to_json(original).unwrap();
252        let gram = gram_stringify_from_json(&json).unwrap();
253        // Round-trip: re-parse and check identity
254        let json2 = gram_parse_to_json(&gram).unwrap();
255        let p1: Vec<serde_json::Value> = serde_json::from_str(&json).unwrap();
256        let p2: Vec<serde_json::Value> = serde_json::from_str(&json2).unwrap();
257        assert_eq!(p1[0]["subject"]["identity"], p2[0]["subject"]["identity"]);
258        assert_eq!(p1[0]["subject"]["labels"], p2[0]["subject"]["labels"]);
259    }
260
261    #[test]
262    fn test_validate_valid_input() {
263        let result = gram_validate_to_json("(alice:Person)");
264        let errors: Vec<String> = serde_json::from_str(&result).unwrap();
265        assert!(errors.is_empty());
266    }
267
268    #[test]
269    fn test_validate_invalid_input() {
270        let result = gram_validate_to_json("(((invalid");
271        let errors: Vec<String> = serde_json::from_str(&result).unwrap();
272        assert!(!errors.is_empty());
273    }
274
275    #[test]
276    fn test_json_interchange_format_subject_key() {
277        // Verifies the JSON uses "subject" key (not "value")
278        let json = gram_parse_to_json("(x)").unwrap();
279        assert!(json.contains("\"subject\""));
280        assert!(!json.contains("\"value\"") || json.contains("\"value\":"));
281    }
282
283    #[test]
284    fn test_value_types_in_json() {
285        let json = gram_parse_to_json(r#"(a {s: "hello", i: 42, f: 3.14, b: true})"#).unwrap();
286        let parsed: Vec<serde_json::Value> = serde_json::from_str(&json).unwrap();
287        let props = &parsed[0]["subject"]["properties"];
288        assert!(props["s"].is_string());
289        assert!(props["i"].is_number());
290        assert!(props["f"].is_number());
291        assert!(props["b"].is_boolean());
292    }
293
294    #[test]
295    fn test_json_to_value_tagged_types() {
296        // symbol
297        let v = json_to_value(&serde_json::json!({"type": "symbol", "value": "foo"})).unwrap();
298        assert!(matches!(v, Value::VSymbol(_)));
299
300        // measurement
301        let v =
302            json_to_value(&serde_json::json!({"type": "measurement", "unit": "kg", "value": 5.0}))
303                .unwrap();
304        assert!(matches!(v, Value::VMeasurement { .. }));
305
306        // tagged string
307        let v = json_to_value(
308            &serde_json::json!({"type": "tagged", "tag": "date", "content": "2024-01-01"}),
309        )
310        .unwrap();
311        assert!(matches!(v, Value::VTaggedString { .. }));
312
313        // range
314        let v = json_to_value(&serde_json::json!({"type": "range", "lower": 1.0, "upper": 10.0}))
315            .unwrap();
316        assert!(matches!(v, Value::VRange(_)));
317    }
318
319    #[test]
320    fn test_json_to_value_rejects_null() {
321        let err = json_to_value(&serde_json::Value::Null).unwrap_err();
322        assert!(err.contains("not representable"));
323    }
324
325    #[test]
326    fn test_json_to_value_rejects_unknown_tagged_type() {
327        let err = json_to_value(&serde_json::json!({"type": "unknown", "value": 1})).unwrap_err();
328        assert!(err.contains("unknown tagged value type"));
329    }
330
331    #[test]
332    fn test_stringify_rejects_null_property_in_json() {
333        let err = gram_stringify_from_json(
334            r#"[{"subject":{"identity":"alice","labels":["Person"],"properties":{"nickname":null}},"elements":[]}]"#,
335        )
336        .unwrap_err();
337        assert!(err.contains("not representable"));
338    }
339
340    #[test]
341    fn test_stringify_rejects_malformed_tagged_value() {
342        let err = gram_stringify_from_json(
343            r#"[{"subject":{"identity":"alice","labels":["Person"],"properties":{"code":{"type":"symbol"}}},"elements":[]}]"#,
344        )
345        .unwrap_err();
346        assert!(err.contains("symbol value must be a string"));
347    }
348}