Skip to main content

lemma/serialization/
json.rs

1use crate::planning::semantics::{FactData, FactPath, LiteralValue, ValueKind};
2use crate::Error;
3use indexmap::IndexMap;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use serde_json::Value;
7use std::collections::HashMap;
8
9/// Parse JSON to string values for use with ExecutionPlan::with_values().
10///
11/// - `null` values are skipped
12/// - All other values are converted to their string representation
13pub fn from_json(json: &[u8]) -> Result<HashMap<String, String>, Error> {
14    let map: HashMap<String, Value> = serde_json::from_slice(json)
15        .map_err(|e| Error::validation(format!("JSON parse error: {}", e), None, None::<String>))?;
16
17    Ok(fact_values_from_map(map))
18}
19
20/// Same string coercion as [`from_json`], for maps already parsed as JSON values (e.g. WASM).
21pub fn fact_values_from_map(map: HashMap<String, Value>) -> HashMap<String, String> {
22    map.into_iter()
23        .filter(|(_, v)| !v.is_null())
24        .map(|(k, v)| (k, json_value_to_string(&v)))
25        .collect()
26}
27
28fn json_value_to_string(value: &Value) -> String {
29    match value {
30        Value::String(s) => s.clone(),
31        Value::Number(n) => n.to_string(),
32        Value::Bool(b) => b.to_string(),
33        Value::Array(_) | Value::Object(_) => serde_json::to_string(value)
34            .expect("BUG: serde_json::to_string failed on a serde_json::Value"),
35        Value::Null => unreachable!(
36            "null JSON values are filtered in fact_values_from_map before json_value_to_string"
37        ),
38    }
39}
40
41// -----------------------------------------------------------------------------
42// Output: Lemma values → JSON (for evaluation responses)
43// -----------------------------------------------------------------------------
44
45/// Convert a Lemma literal value to a JSON value and optional unit string.
46///
47/// Used when serializing evaluation results (e.g. CLI `run --output json`, HTTP API).
48/// Returns `(value, unit)` where `unit` is present for scale and duration.
49pub fn literal_value_to_json(v: &LiteralValue) -> (Value, Option<String>) {
50    match &v.value {
51        ValueKind::Boolean(b) => (Value::Bool(*b), None),
52        ValueKind::Number(n) => (decimal_to_json(n), None),
53        ValueKind::Scale(n, unit) => (decimal_to_json(n), Some(unit.clone())),
54        ValueKind::Ratio(r, _) => (decimal_to_json(r), None),
55        ValueKind::Duration(n, unit) => (decimal_to_json(n), Some(unit.to_string())),
56        ValueKind::Text(_) | ValueKind::Date(_) | ValueKind::Time(_) => {
57            (Value::String(v.display_value()), None)
58        }
59    }
60}
61
62/// Convert a decimal to a JSON number when in range; otherwise serialize as string.
63///
64/// Avoids panics for decimals outside i64 (integer case) or f64 (fractional case).
65fn decimal_to_json(d: &Decimal) -> Value {
66    if d.fract().is_zero() {
67        match i64::try_from(d.trunc()) {
68            Ok(n) => Value::Number(n.into()),
69            Err(_) => Value::String(d.to_string()),
70        }
71    } else {
72        let s = d.to_string();
73        let Ok(f) = s.parse::<f64>() else {
74            return Value::String(s);
75        };
76        match serde_json::Number::from_f64(f) {
77            Some(n) => Value::Number(n),
78            None => Value::String(s),
79        }
80    }
81}
82
83// -----------------------------------------------------------------------------
84// Serde helpers for FactPath / FactData
85// -----------------------------------------------------------------------------
86
87/// Serializes IndexMap<FactPath, FactData> as array of [FactPath, FactData] tuples.
88pub fn serialize_resolved_fact_value_map<S>(
89    map: &IndexMap<FactPath, FactData>,
90    serializer: S,
91) -> Result<S::Ok, S::Error>
92where
93    S: Serializer,
94{
95    let entries: Vec<(&FactPath, &FactData)> = map.iter().collect();
96    entries.serialize(serializer)
97}
98
99/// Deserializes from array of [FactPath, FactData] tuples, preserving order.
100pub fn deserialize_resolved_fact_value_map<'de, D>(
101    deserializer: D,
102) -> Result<IndexMap<FactPath, FactData>, D::Error>
103where
104    D: Deserializer<'de>,
105{
106    let entries: Vec<(FactPath, FactData)> = Vec::deserialize(deserializer)?;
107    Ok(entries.into_iter().collect())
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_json_string_to_string() {
116        let json = br#"{"name": "Alice"}"#;
117        let result = from_json(json).unwrap();
118        assert_eq!(result.get("name"), Some(&"Alice".to_string()));
119    }
120
121    #[test]
122    fn test_json_number_to_string() {
123        let json = br#"{"name": 42}"#;
124        let result = from_json(json).unwrap();
125        assert_eq!(result.get("name"), Some(&"42".to_string()));
126    }
127
128    #[test]
129    fn test_json_boolean_to_string() {
130        let json = br#"{"name": true}"#;
131        let result = from_json(json).unwrap();
132        assert_eq!(result.get("name"), Some(&"true".to_string()));
133    }
134
135    #[test]
136    fn test_json_array_to_string() {
137        let json = br#"{"data": [1, 2, 3]}"#;
138        let result = from_json(json).unwrap();
139        assert_eq!(result.get("data"), Some(&"[1,2,3]".to_string()));
140    }
141
142    #[test]
143    fn test_json_object_to_string() {
144        let json = br#"{"config": {"key": "value"}}"#;
145        let result = from_json(json).unwrap();
146        assert_eq!(
147            result.get("config"),
148            Some(&"{\"key\":\"value\"}".to_string())
149        );
150    }
151
152    #[test]
153    fn test_null_value_skipped() {
154        let json = br#"{"name": null, "age": 30}"#;
155        let result = from_json(json).unwrap();
156        assert_eq!(result.len(), 1);
157        assert!(!result.contains_key("name"));
158        assert_eq!(result.get("age"), Some(&"30".to_string()));
159    }
160
161    #[test]
162    fn test_all_null_values() {
163        let json = br#"{"name": null}"#;
164        let result = from_json(json).unwrap();
165        assert!(result.is_empty());
166    }
167
168    #[test]
169    fn test_mixed_valid_types() {
170        let json = br#"{"name": "Test", "count": 5, "active": true, "discount": 21}"#;
171        let result = from_json(json).unwrap();
172        assert_eq!(result.len(), 4);
173        assert_eq!(result.get("name"), Some(&"Test".to_string()));
174        assert_eq!(result.get("count"), Some(&"5".to_string()));
175        assert_eq!(result.get("active"), Some(&"true".to_string()));
176        assert_eq!(result.get("discount"), Some(&"21".to_string()));
177    }
178
179    #[test]
180    fn test_invalid_json_syntax() {
181        let json = br#"{"name": }"#;
182        let result = from_json(json);
183        assert!(result.is_err());
184        let error_message = result.unwrap_err().to_string();
185        assert!(error_message.contains("JSON parse error"));
186    }
187
188    // --- literal_value_to_json / decimal_to_json ---
189
190    #[test]
191    fn test_literal_value_to_json_number() {
192        use crate::planning::semantics::LiteralValue;
193        use std::str::FromStr;
194        let v = LiteralValue::number(rust_decimal::Decimal::from_str("42").unwrap());
195        let (val, unit) = literal_value_to_json(&v);
196        assert!(val.is_number());
197        assert_eq!(val.as_i64(), Some(42));
198        assert!(unit.is_none());
199    }
200
201    #[test]
202    fn test_literal_value_to_json_scale() {
203        use crate::planning::semantics::{primitive_scale, LiteralValue};
204        use std::str::FromStr;
205        let v = LiteralValue::scale_with_type(
206            rust_decimal::Decimal::from_str("99.50").unwrap(),
207            "eur".to_string(),
208            primitive_scale().clone(),
209        );
210        let (val, unit) = literal_value_to_json(&v);
211        assert!(val.is_number());
212        assert_eq!(unit.as_deref(), Some("eur"));
213    }
214
215    #[test]
216    fn test_literal_value_to_json_boolean() {
217        use crate::planning::semantics::LiteralValue;
218        let (val, unit) = literal_value_to_json(&LiteralValue::from_bool(true));
219        assert_eq!(val.as_bool(), Some(true));
220        assert!(unit.is_none());
221    }
222
223    #[test]
224    fn test_decimal_to_json_out_of_i64_fallback() {
225        use crate::planning::semantics::LiteralValue;
226        use std::str::FromStr;
227        // One more than i64::MAX; fits in Decimal but not i64
228        let huge = rust_decimal::Decimal::from_str("9223372036854775808").unwrap();
229        let v = LiteralValue::number(huge);
230        let (val, _) = literal_value_to_json(&v);
231        assert!(val.is_string());
232        assert_eq!(val.as_str(), Some("9223372036854775808"));
233    }
234}