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>`.
112pub fn 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 if let Some(f) = n.as_f64() {
147                Ok(Value::VDecimal(f))
148            } else {
149                Err(format!(
150                    "JSON number is not representable as a gram decimal value: {}",
151                    n
152                ))
153            }
154        }
155        serde_json::Value::Array(arr) => {
156            let items: Vec<Value> = arr
157                .iter()
158                .map(json_to_value)
159                .collect::<Result<Vec<_>, _>>()?;
160            Ok(Value::VArray(items))
161        }
162        serde_json::Value::Object(obj) => {
163            // Check for tagged objects (symbol, range, measurement, tagged string)
164            if let Some(type_tag) = obj.get("type").and_then(|t| t.as_str()) {
165                match type_tag {
166                    "symbol" => {
167                        let val = obj
168                            .get("value")
169                            .and_then(|v| v.as_str())
170                            .ok_or_else(|| "symbol value must be a string".to_string())?
171                            .to_string();
172                        Ok(Value::VSymbol(val))
173                    }
174                    "range" => {
175                        let lower = obj.get("lower").and_then(|v| v.as_f64());
176                        let upper = obj.get("upper").and_then(|v| v.as_f64());
177                        Ok(Value::VRange(RangeValue { lower, upper }))
178                    }
179                    "measurement" => {
180                        let unit = obj
181                            .get("unit")
182                            .and_then(|v| v.as_str())
183                            .ok_or_else(|| "measurement unit must be a string".to_string())?
184                            .to_string();
185                        let value = obj
186                            .get("value")
187                            .and_then(|v| v.as_f64())
188                            .ok_or_else(|| "measurement value must be a number".to_string())?;
189                        Ok(Value::VMeasurement { unit, value })
190                    }
191                    "tagged" => {
192                        let tag = obj
193                            .get("tag")
194                            .and_then(|v| v.as_str())
195                            .ok_or_else(|| "tagged value tag must be a string".to_string())?
196                            .to_string();
197                        let content = obj
198                            .get("content")
199                            .and_then(|v| v.as_str())
200                            .ok_or_else(|| "tagged value content must be a string".to_string())?
201                            .to_string();
202                        Ok(Value::VTaggedString { tag, content })
203                    }
204                    _ => Err(format!("unknown tagged value type: {}", type_tag)),
205                }
206            } else {
207                // Plain JSON object → VMap
208                let map: HashMap<String, Value> = obj
209                    .iter()
210                    .map(|(k, v)| json_to_value(v).map(|val| (k.clone(), val)))
211                    .collect::<Result<HashMap<_, _>, _>>()?;
212                Ok(Value::VMap(map))
213            }
214        }
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_parse_empty_input() {
224        assert_eq!(gram_parse_to_json("").unwrap(), "[]");
225        assert_eq!(gram_parse_to_json("   ").unwrap(), "[]");
226    }
227
228    #[test]
229    fn test_parse_simple_node() {
230        let json = gram_parse_to_json("(alice:Person)").unwrap();
231        let parsed: Vec<serde_json::Value> = serde_json::from_str(&json).unwrap();
232        assert_eq!(parsed.len(), 1);
233        assert_eq!(parsed[0]["subject"]["identity"], "alice");
234        assert_eq!(parsed[0]["subject"]["labels"][0], "Person");
235    }
236
237    #[test]
238    fn test_parse_node_with_properties() {
239        let json = gram_parse_to_json(r#"(a {name: "Alice", age: 30})"#).unwrap();
240        let parsed: Vec<serde_json::Value> = serde_json::from_str(&json).unwrap();
241        assert_eq!(parsed[0]["subject"]["properties"]["name"], "Alice");
242        assert_eq!(parsed[0]["subject"]["properties"]["age"], 30);
243    }
244
245    #[test]
246    fn test_parse_relationship() {
247        let json = gram_parse_to_json("(a)-->(b)").unwrap();
248        let parsed: Vec<serde_json::Value> = serde_json::from_str(&json).unwrap();
249        assert_eq!(parsed.len(), 1);
250        assert_eq!(parsed[0]["elements"].as_array().unwrap().len(), 2);
251    }
252
253    #[test]
254    fn test_stringify_round_trip() {
255        let original = "(alice:Person)";
256        let json = gram_parse_to_json(original).unwrap();
257        let gram = gram_stringify_from_json(&json).unwrap();
258        // Round-trip: re-parse and check identity
259        let json2 = gram_parse_to_json(&gram).unwrap();
260        let p1: Vec<serde_json::Value> = serde_json::from_str(&json).unwrap();
261        let p2: Vec<serde_json::Value> = serde_json::from_str(&json2).unwrap();
262        assert_eq!(p1[0]["subject"]["identity"], p2[0]["subject"]["identity"]);
263        assert_eq!(p1[0]["subject"]["labels"], p2[0]["subject"]["labels"]);
264    }
265
266    #[test]
267    fn test_validate_valid_input() {
268        let result = gram_validate_to_json("(alice:Person)");
269        let errors: Vec<String> = serde_json::from_str(&result).unwrap();
270        assert!(errors.is_empty());
271    }
272
273    #[test]
274    fn test_validate_invalid_input() {
275        let result = gram_validate_to_json("(((invalid");
276        let errors: Vec<String> = serde_json::from_str(&result).unwrap();
277        assert!(!errors.is_empty());
278    }
279
280    #[test]
281    fn test_json_interchange_format_subject_key() {
282        // Verifies the JSON uses "subject" key (not "value")
283        let json = gram_parse_to_json("(x)").unwrap();
284        assert!(json.contains("\"subject\""));
285        assert!(!json.contains("\"value\"") || json.contains("\"value\":"));
286    }
287
288    #[test]
289    fn test_value_types_in_json() {
290        let json = gram_parse_to_json(r#"(a {s: "hello", i: 42, f: 3.14, b: true})"#).unwrap();
291        let parsed: Vec<serde_json::Value> = serde_json::from_str(&json).unwrap();
292        let props = &parsed[0]["subject"]["properties"];
293        assert!(props["s"].is_string());
294        assert!(props["i"].is_number());
295        assert!(props["f"].is_number());
296        assert!(props["b"].is_boolean());
297    }
298
299    #[test]
300    fn test_json_to_value_tagged_types() {
301        // symbol
302        let v = json_to_value(&serde_json::json!({"type": "symbol", "value": "foo"})).unwrap();
303        assert!(matches!(v, Value::VSymbol(_)));
304
305        // measurement
306        let v =
307            json_to_value(&serde_json::json!({"type": "measurement", "unit": "kg", "value": 5.0}))
308                .unwrap();
309        assert!(matches!(v, Value::VMeasurement { .. }));
310
311        // tagged string
312        let v = json_to_value(
313            &serde_json::json!({"type": "tagged", "tag": "date", "content": "2024-01-01"}),
314        )
315        .unwrap();
316        assert!(matches!(v, Value::VTaggedString { .. }));
317
318        // range
319        let v = json_to_value(&serde_json::json!({"type": "range", "lower": 1.0, "upper": 10.0}))
320            .unwrap();
321        assert!(matches!(v, Value::VRange(_)));
322    }
323
324    #[test]
325    fn test_json_to_value_rejects_null() {
326        let err = json_to_value(&serde_json::Value::Null).unwrap_err();
327        assert!(err.contains("not representable"));
328    }
329
330    #[test]
331    fn test_json_to_value_rejects_unknown_tagged_type() {
332        let err = json_to_value(&serde_json::json!({"type": "unknown", "value": 1})).unwrap_err();
333        assert!(err.contains("unknown tagged value type"));
334    }
335
336    #[test]
337    fn test_stringify_rejects_null_property_in_json() {
338        let err = gram_stringify_from_json(
339            r#"[{"subject":{"identity":"alice","labels":["Person"],"properties":{"nickname":null}},"elements":[]}]"#,
340        )
341        .unwrap_err();
342        assert!(err.contains("not representable"));
343    }
344
345    #[test]
346    fn test_stringify_rejects_malformed_tagged_value() {
347        let err = gram_stringify_from_json(
348            r#"[{"subject":{"identity":"alice","labels":["Person"],"properties":{"code":{"type":"symbol"}}},"elements":[]}]"#,
349        )
350        .unwrap_err();
351        assert!(err.contains("symbol value must be a string"));
352    }
353}