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