lemma/serialization/
json.rs

1use crate::parsing::ast::Span;
2use crate::planning::ExecutionPlan;
3use crate::semantic::{
4    BooleanValue, FactPath, LemmaFact, LemmaType, LiteralValue, Value as LemmaValue,
5};
6use crate::LemmaError;
7use crate::Source;
8use rust_decimal::Decimal;
9use serde::{Deserialize, Deserializer, Serializer};
10use serde_json::Value;
11use std::collections::{HashMap, HashSet};
12use std::sync::Arc;
13
14/// Convert JSON values to typed Lemma values using the ExecutionPlan for type information.
15///
16/// This function converts JSON values to Lemma types with the following rules:
17///
18/// | Lemma Type | Valid JSON Types | Conversion |
19/// |------------|------------------|------------|
20/// | Text | any | Strings pass through; numbers/booleans/arrays/objects serialize to JSON string |
21/// | Number | number, string | Numbers pass through; strings are parsed as decimals |
22/// | Boolean | boolean, string | Booleans pass through; strings parsed as "true"/"false"/"yes"/"no"/"accept"/"reject" |
23/// | Percent | number, string | Numbers become percent; strings parsed (with or without %) |
24/// | Date | string | ISO format "2024-01-15" or "2024-01-15T14:30:00Z" |
25/// | Regex | string | Pattern string, optionally wrapped in /slashes/ |
26/// | Unit types | string | Format: "100 kilogram", "5 meter", etc. |
27///
28/// Special handling:
29/// - `null` values are skipped (treated as if the fact was not provided)
30/// - Unknown facts return an error
31/// - Unparseable values return an error with a descriptive message
32pub fn from_json(
33    json: &[u8],
34    plan: &ExecutionPlan,
35) -> Result<HashMap<String, LiteralValue>, LemmaError> {
36    let map: HashMap<String, Value> = serde_json::from_slice(json).map_err(|e| {
37        LemmaError::engine(
38            format!("JSON parse error: {}", e),
39            Span {
40                start: 0,
41                end: 0,
42                line: 1,
43                col: 0,
44            },
45            "<unknown>",
46            Arc::from(""),
47            "<unknown>",
48            1,
49            None::<String>,
50        )
51    })?;
52
53    let mut result = HashMap::new();
54
55    for (fact_name, json_value) in map {
56        if json_value.is_null() {
57            continue;
58        }
59
60        let fact_path = plan.get_fact_path_by_str(&fact_name).ok_or_else(|| {
61            let available: Vec<String> = plan.fact_schema.keys().map(|p| p.to_string()).collect();
62            LemmaError::engine(
63                format!(
64                    "Fact '{}' not found in document. Available facts: {}",
65                    fact_name,
66                    available.join(", ")
67                ),
68                Span {
69                    start: 0,
70                    end: 0,
71                    line: 1,
72                    col: 0,
73                },
74                "<unknown>",
75                Arc::from(""),
76                "<unknown>",
77                1,
78                None::<String>,
79            )
80        })?;
81
82        let expected_type = plan.fact_schema.get(fact_path).cloned().unwrap_or_else(|| {
83            unreachable!(
84                "BUG: get_fact_path_by_str returned a fact path that is not in fact_schema (fact={})",
85                fact_name
86            )
87        });
88        let literal_value = convert_json_value(&fact_name, &json_value, &expected_type)?;
89
90        result.insert(fact_name, literal_value);
91    }
92
93    Ok(result)
94}
95
96// Note: expected types come exclusively from `ExecutionPlan.fact_schema`.
97
98fn convert_json_value(
99    fact_name: &str,
100    json_value: &Value,
101    expected_type: &crate::semantic::LemmaType,
102) -> Result<LiteralValue, LemmaError> {
103    match &expected_type.specifications {
104        crate::semantic::TypeSpecification::Text { .. } => {
105            convert_to_text(fact_name, json_value, expected_type)
106        }
107        crate::semantic::TypeSpecification::Scale { .. } => {
108            convert_to_scale(fact_name, json_value, expected_type)
109        }
110        crate::semantic::TypeSpecification::Number { .. } => {
111            convert_to_number(fact_name, json_value, expected_type)
112        }
113        crate::semantic::TypeSpecification::Boolean { .. } => {
114            convert_to_boolean(fact_name, json_value, expected_type)
115        }
116        crate::semantic::TypeSpecification::Ratio { .. } => {
117            convert_to_ratio(fact_name, json_value, expected_type)
118        }
119        crate::semantic::TypeSpecification::Date { .. } => {
120            convert_to_date(fact_name, json_value, expected_type)
121        }
122        crate::semantic::TypeSpecification::Duration { .. } => {
123            convert_to_duration(fact_name, json_value, expected_type)
124        }
125        crate::semantic::TypeSpecification::Time { .. } => {
126            convert_to_time(fact_name, json_value, expected_type)
127        }
128        crate::semantic::TypeSpecification::Veto { .. } => Err(LemmaError::engine(
129            "Veto type is not a user-declarable type and cannot be converted from JSON",
130            Span {
131                start: 0,
132                end: 0,
133                line: 1,
134                col: 0,
135            },
136            "<unknown>",
137            Arc::from(""),
138            "<unknown>",
139            1,
140            None::<String>,
141        )),
142    }
143}
144
145fn convert_to_text(
146    fact_name: &str,
147    json_value: &Value,
148    expected_type: &crate::semantic::LemmaType,
149) -> Result<LiteralValue, LemmaError> {
150    let text = match json_value {
151        Value::String(s) => s.clone(),
152        Value::Number(n) => n.to_string(),
153        Value::Bool(b) => b.to_string(),
154        Value::Array(_) | Value::Object(_) => {
155            serde_json::to_string(json_value).unwrap_or_else(|_| json_value.to_string())
156        }
157        Value::Null => return Err(type_error(fact_name, "text", "null")),
158    };
159    Ok(LiteralValue::text_with_type(text, expected_type.clone()))
160}
161
162fn convert_to_number(
163    fact_name: &str,
164    json_value: &Value,
165    expected_type: &crate::semantic::LemmaType,
166) -> Result<LiteralValue, LemmaError> {
167    match json_value {
168        Value::Number(n) => {
169            let decimal = json_number_to_decimal(fact_name, n)?;
170            Ok(LiteralValue::number_with_type(
171                decimal,
172                expected_type.clone(),
173            ))
174        }
175        Value::String(s) => {
176            let clean = s.trim().replace(['_', ','], "");
177            let decimal = Decimal::from_str_exact(&clean).map_err(|_| {
178                LemmaError::engine(
179                    format!(
180                        "Invalid number string for fact '{}': '{}' is not a valid decimal",
181                        fact_name, s
182                    ),
183                    Span {
184                        start: 0,
185                        end: 0,
186                        line: 1,
187                        col: 0,
188                    },
189                    "<unknown>",
190                    Arc::from(s.as_str()),
191                    "<unknown>",
192                    1,
193                    None::<String>,
194                )
195            })?;
196            Ok(LiteralValue::number_with_type(
197                decimal,
198                expected_type.clone(),
199            ))
200        }
201        Value::Null => Err(type_error(fact_name, "number", "null")),
202        Value::Bool(_) => Err(type_error(fact_name, "number", "boolean")),
203        Value::Array(_) => Err(type_error(fact_name, "number", "array")),
204        Value::Object(_) => Err(type_error(fact_name, "number", "object")),
205    }
206}
207
208fn convert_to_scale(
209    fact_name: &str,
210    json_value: &Value,
211    expected_type: &crate::semantic::LemmaType,
212) -> Result<LiteralValue, LemmaError> {
213    match json_value {
214        Value::Number(n) => {
215            // JSON number (e.g., 50) -> Scale with no unit
216            let decimal = json_number_to_decimal(fact_name, n)?;
217            Ok(LiteralValue::scale_with_type(
218                decimal,
219                None,
220                expected_type.clone(),
221            ))
222        }
223        Value::String(s) => {
224            let trimmed = s.trim();
225
226            // Parse number and optional unit from string
227            // Handles: "50", "50 eur", "50eur", "1,234.56 usd", etc.
228
229            // First, try to find where the number part ends
230            // Numbers can contain: digits, decimal point, sign, underscore, comma
231            let mut number_end = 0;
232            let chars: Vec<char> = trimmed.chars().collect();
233            let mut has_decimal = false;
234
235            // Skip leading sign
236            let start = if chars.first().is_some_and(|c| *c == '+' || *c == '-') {
237                1
238            } else {
239                0
240            };
241
242            for (i, &ch) in chars.iter().enumerate().skip(start) {
243                match ch {
244                    '0'..='9' => number_end = i + 1,
245                    '.' if !has_decimal => {
246                        has_decimal = true;
247                        number_end = i + 1;
248                    }
249                    '_' | ',' => {
250                        // Thousand separators - continue scanning
251                        number_end = i + 1;
252                    }
253                    _ => {
254                        // Non-numeric character - number ends here
255                        break;
256                    }
257                }
258            }
259
260            // Extract number and unit parts
261            let number_part = trimmed[..number_end].trim();
262            let unit_part = trimmed[number_end..].trim();
263
264            // Clean number part (remove separators for parsing)
265            let clean_number = number_part.replace(['_', ','], "");
266            let decimal = Decimal::from_str_exact(&clean_number).map_err(|_| {
267                LemmaError::engine(
268                    format!(
269                        "Invalid scale string for fact '{}': '{}' is not a valid number",
270                        fact_name, s
271                    ),
272                    Span {
273                        start: 0,
274                        end: 0,
275                        line: 1,
276                        col: 0,
277                    },
278                    "<unknown>",
279                    Arc::from(s.as_str()),
280                    "<unknown>",
281                    1,
282                    None::<String>,
283                )
284            })?;
285
286            // Validate unit against type definition
287            let allowed_units = match &expected_type.specifications {
288                crate::semantic::TypeSpecification::Scale { units, .. } => units,
289                _ => {
290                    return Err(LemmaError::engine(
291                        format!(
292                            "Internal error: expected a scale type for fact '{}' but got {}",
293                            fact_name,
294                            expected_type.name()
295                        ),
296                        Span {
297                            start: 0,
298                            end: 0,
299                            line: 1,
300                            col: 0,
301                        },
302                        "<unknown>",
303                        Arc::from(""),
304                        "<unknown>",
305                        1,
306                        None::<String>,
307                    ));
308                }
309            };
310
311            let unit = if unit_part.is_empty() {
312                if !allowed_units.is_empty() {
313                    let valid: Vec<String> = allowed_units.iter().map(|u| u.name.clone()).collect();
314                    return Err(LemmaError::engine(
315                        format!(
316                            "Missing unit for fact '{}'. Valid units: {}",
317                            fact_name,
318                            valid.join(", ")
319                        ),
320                        Span {
321                            start: 0,
322                            end: 0,
323                            line: 1,
324                            col: 0,
325                        },
326                        "<unknown>",
327                        Arc::from(s.as_str()),
328                        "<unknown>",
329                        1,
330                        None::<String>,
331                    ));
332                }
333                None
334            } else {
335                let matched = allowed_units
336                    .iter()
337                    .find(|u| u.name.eq_ignore_ascii_case(unit_part));
338                match matched {
339                    Some(unit_def) => Some(unit_def.name.clone()),
340                    None => {
341                        let valid: Vec<String> =
342                            allowed_units.iter().map(|u| u.name.clone()).collect();
343                        let valid_str = if valid.is_empty() {
344                            "none".to_string()
345                        } else {
346                            valid.join(", ")
347                        };
348                        return Err(LemmaError::engine(
349                            format!(
350                                "Invalid unit '{}' for fact '{}'. Valid units: {}",
351                                unit_part, fact_name, valid_str
352                            ),
353                            Span {
354                                start: 0,
355                                end: 0,
356                                line: 1,
357                                col: 0,
358                            },
359                            "<unknown>",
360                            Arc::from(s.as_str()),
361                            "<unknown>",
362                            1,
363                            None::<String>,
364                        ));
365                    }
366                }
367            };
368
369            Ok(LiteralValue::scale_with_type(
370                decimal,
371                unit,
372                expected_type.clone(),
373            ))
374        }
375        Value::Null => Err(type_error(fact_name, "scale", "null")),
376        Value::Bool(_) => Err(type_error(fact_name, "scale", "boolean")),
377        Value::Array(_) => Err(type_error(fact_name, "scale", "array")),
378        Value::Object(_) => Err(type_error(fact_name, "scale", "object")),
379    }
380}
381
382fn convert_to_boolean(
383    fact_name: &str,
384    json_value: &Value,
385    expected_type: &crate::semantic::LemmaType,
386) -> Result<LiteralValue, LemmaError> {
387    match json_value {
388        Value::Bool(b) => {
389            let boolean_value = if *b {
390                BooleanValue::True
391            } else {
392                BooleanValue::False
393            };
394            Ok(LiteralValue::boolean_with_type(
395                boolean_value,
396                expected_type.clone(),
397            ))
398        }
399        Value::String(s) => {
400            let boolean_value: BooleanValue = s.parse().map_err(|_| {
401                LemmaError::engine(
402                    format!("Invalid boolean string for fact '{}': '{}'. Expected one of: true, false, yes, no, accept, reject", fact_name, s),
403                    Span { start: 0, end: 0, line: 1, col: 0 },
404                    "<unknown>",
405                    Arc::from(s.as_str()),
406                    "<unknown>",
407                    1,
408                    None::<String>,
409                )
410            })?;
411            Ok(LiteralValue::boolean_with_type(
412                boolean_value,
413                expected_type.clone(),
414            ))
415        }
416        Value::Null => Err(type_error(fact_name, "boolean", "null")),
417        Value::Number(_) => Err(type_error(fact_name, "boolean", "number")),
418        Value::Array(_) => Err(type_error(fact_name, "boolean", "array")),
419        Value::Object(_) => Err(type_error(fact_name, "boolean", "object")),
420    }
421}
422
423fn convert_to_ratio(
424    fact_name: &str,
425    json_value: &Value,
426    expected_type: &crate::semantic::LemmaType,
427) -> Result<LiteralValue, LemmaError> {
428    match json_value {
429        Value::Number(n) => {
430            // JSON number (e.g., 0.10) -> ratio with no unit
431            let decimal = json_number_to_decimal(fact_name, n)?;
432            Ok(LiteralValue::ratio_with_type(
433                decimal,
434                None,
435                expected_type.clone(),
436            ))
437        }
438        Value::String(s) => {
439            let trimmed = s.trim();
440            let trimmed_lower = trimmed.to_lowercase();
441
442            // Determine unit and extract number part
443            let (number_part, unit) = if let Some(stripped) = trimmed.strip_suffix("%%") {
444                // "10%%" -> ratio with "permille" unit
445                (stripped.trim(), Some("permille".to_string()))
446            } else if let Some(stripped) = trimmed.strip_suffix('%') {
447                // "10%" -> ratio with "percent" unit
448                (stripped.trim(), Some("percent".to_string()))
449            } else if trimmed_lower.ends_with("permille") {
450                // "10permille" or "10 PERMILLE" -> ratio with "permille" unit
451                (
452                    trimmed[..trimmed.len() - 8].trim(),
453                    Some("permille".to_string()),
454                )
455            } else if trimmed_lower.ends_with("percent") {
456                // "10percent" or "10PERCENT" or "10 percent" or "10 PERCENT" -> ratio with "percent" unit
457                (
458                    trimmed[..trimmed.len() - 7].trim(),
459                    Some("percent".to_string()),
460                )
461            } else {
462                // "0.10" -> ratio with no unit
463                (trimmed, None)
464            };
465
466            let clean_number = number_part.replace(['_', ','], "");
467            let decimal = Decimal::from_str_exact(&clean_number).map_err(|_| {
468                LemmaError::engine(
469                    format!(
470                        "Invalid ratio string for fact '{}': '{}' is not a valid number",
471                        fact_name, s
472                    ),
473                    Span {
474                        start: 0,
475                        end: 0,
476                        line: 1,
477                        col: 0,
478                    },
479                    "<unknown>",
480                    Arc::from(s.as_str()),
481                    "<unknown>",
482                    1,
483                    None::<String>,
484                )
485            })?;
486
487            // Convert percent/permille values to ratio (e.g., 10 -> 0.10 for percent, 10 -> 0.01 for permille)
488            let ratio_value = if let Some(ref unit_name) = unit {
489                if unit_name == "percent" {
490                    decimal / Decimal::from(100)
491                } else if unit_name == "permille" {
492                    decimal / Decimal::from(1000)
493                } else {
494                    decimal
495                }
496            } else {
497                decimal
498            };
499
500            Ok(LiteralValue::ratio_with_type(
501                ratio_value,
502                unit,
503                expected_type.clone(),
504            ))
505        }
506        Value::Null => Err(type_error(fact_name, "ratio", "null")),
507        Value::Bool(_) => Err(type_error(fact_name, "ratio", "boolean")),
508        Value::Array(_) => Err(type_error(fact_name, "ratio", "array")),
509        Value::Object(_) => Err(type_error(fact_name, "ratio", "object")),
510    }
511}
512
513fn convert_to_date(
514    fact_name: &str,
515    json_value: &Value,
516    expected_type: &crate::semantic::LemmaType,
517) -> Result<LiteralValue, LemmaError> {
518    match json_value {
519        Value::String(s) => expected_type.parse_value(s).map_err(|e| {
520            LemmaError::engine(
521                format!("Invalid date for fact '{}': {}", fact_name, e),
522                Span {
523                    start: 0,
524                    end: 0,
525                    line: 1,
526                    col: 0,
527                },
528                "<unknown>",
529                Arc::from(s.as_str()),
530                "<unknown>",
531                1,
532                None::<String>,
533            )
534        }),
535        Value::Null => Err(type_error(fact_name, "date", "null")),
536        Value::Bool(_) => Err(type_error(fact_name, "date", "boolean")),
537        Value::Number(_) => Err(type_error(fact_name, "date", "number")),
538        Value::Array(_) => Err(type_error(fact_name, "date", "array")),
539        Value::Object(_) => Err(type_error(fact_name, "date", "object")),
540    }
541}
542
543fn convert_to_duration(
544    fact_name: &str,
545    json_value: &Value,
546    expected_type: &crate::semantic::LemmaType,
547) -> Result<LiteralValue, LemmaError> {
548    match json_value {
549        Value::String(s) => expected_type.parse_value(s).map_err(|e| {
550            LemmaError::engine(
551                format!("Invalid duration value for fact '{}': {}", fact_name, e),
552                Span { start: 0, end: 0, line: 1, col: 0 },
553                "<unknown>",
554                Arc::from(s.as_str()),
555                "<unknown>",
556                1,
557                None::<String>,
558            )
559        }),
560        Value::Null => Err(type_error(fact_name, "duration", "null")),
561        Value::Bool(_) => Err(type_error(fact_name, "duration", "boolean")),
562        Value::Number(_) => Err(LemmaError::engine(
563            format!("Invalid JSON type for fact '{}': expected duration (as string like '5 days'), got number. Duration values must include the unit name.", fact_name),
564            Span { start: 0, end: 0, line: 1, col: 0 },
565            "<unknown>",
566            Arc::from(""),
567            "<unknown>",
568            1,
569            None::<String>,
570        )),
571        Value::Array(_) => Err(type_error(fact_name, "duration", "array")),
572        Value::Object(_) => Err(type_error(fact_name, "duration", "object")),
573    }
574}
575
576fn convert_to_time(
577    fact_name: &str,
578    json_value: &Value,
579    expected_type: &crate::semantic::LemmaType,
580) -> Result<LiteralValue, LemmaError> {
581    match json_value {
582        Value::String(s) => expected_type.parse_value(s).map_err(|e| {
583            LemmaError::engine(
584                format!("Invalid time value for fact '{}': {}", fact_name, e),
585                Span {
586                    start: 0,
587                    end: 0,
588                    line: 1,
589                    col: 0,
590                },
591                "<unknown>",
592                Arc::from(s.as_str()),
593                "<unknown>",
594                1,
595                None::<String>,
596            )
597        }),
598        Value::Null => Err(type_error(fact_name, "time", "null")),
599        Value::Bool(_) => Err(type_error(fact_name, "time", "boolean")),
600        Value::Number(_) => Err(type_error(fact_name, "time", "number")),
601        Value::Array(_) => Err(type_error(fact_name, "time", "array")),
602        Value::Object(_) => Err(type_error(fact_name, "time", "object")),
603    }
604}
605
606fn json_number_to_decimal(fact_name: &str, n: &serde_json::Number) -> Result<Decimal, LemmaError> {
607    if let Some(i) = n.as_i64() {
608        Ok(Decimal::from(i))
609    } else if let Some(u) = n.as_u64() {
610        Ok(Decimal::from(u))
611    } else if let Some(f) = n.as_f64() {
612        Decimal::try_from(f).map_err(|_| {
613            LemmaError::engine(
614                format!(
615                    "Invalid number for fact '{}': {} cannot be represented as a decimal",
616                    fact_name, n
617                ),
618                Span {
619                    start: 0,
620                    end: 0,
621                    line: 1,
622                    col: 0,
623                },
624                "<unknown>",
625                Arc::from(""),
626                "<unknown>",
627                1,
628                None::<String>,
629            )
630        })
631    } else {
632        Err(LemmaError::engine(
633            format!(
634                "Invalid number for fact '{}': {} is not a valid number",
635                fact_name, n
636            ),
637            Span {
638                start: 0,
639                end: 0,
640                line: 1,
641                col: 0,
642            },
643            "<unknown>",
644            Arc::from(""),
645            "<unknown>",
646            1,
647            None::<String>,
648        ))
649    }
650}
651
652fn type_error(fact_name: &str, expected: &str, got: &str) -> LemmaError {
653    LemmaError::engine(
654        format!(
655            "Invalid JSON type for fact '{}': expected {}, got {}",
656            fact_name, expected, got
657        ),
658        Span {
659            start: 0,
660            end: 0,
661            line: 1,
662            col: 0,
663        },
664        "<unknown>",
665        Arc::from(""),
666        "<unknown>",
667        1,
668        None::<String>,
669    )
670}
671
672// Custom JSON serializers for Response types
673
674/// Custom serializer for LiteralValue that outputs type and value
675pub fn serialize_literal_value<S>(value: &LiteralValue, serializer: S) -> Result<S::Ok, S::Error>
676where
677    S: serde::ser::Serializer,
678{
679    use serde::ser::SerializeMap;
680    use serde_json::Number;
681    use std::str::FromStr;
682
683    let mut map = serializer.serialize_map(Some(2))?;
684
685    match &value.value {
686        LemmaValue::Number(n) => {
687            map.serialize_entry("type", "number")?;
688            let num = Number::from_str(&n.to_string())
689                .map_err(|_| serde::ser::Error::custom("Failed to convert Decimal to Number"))?;
690            map.serialize_entry("value", &num)?;
691        }
692        LemmaValue::Scale(n, unit_opt) => {
693            map.serialize_entry("type", "scale")?;
694            let num = Number::from_str(&n.to_string())
695                .map_err(|_| serde::ser::Error::custom("Failed to convert Decimal to Number"))?;
696            map.serialize_entry("value", &num)?;
697            if let Some(unit) = unit_opt {
698                map.serialize_entry("unit", unit)?;
699            }
700        }
701        LemmaValue::Ratio(r, _) => {
702            map.serialize_entry("type", "ratio")?;
703            let num = Number::from_str(&r.to_string())
704                .map_err(|_| serde::ser::Error::custom("Failed to convert Decimal to Number"))?;
705            map.serialize_entry("value", &num)?;
706        }
707        LemmaValue::Boolean(b) => {
708            map.serialize_entry("type", "boolean")?;
709            map.serialize_entry("value", &bool::from(b.clone()))?;
710        }
711        LemmaValue::Text(s) => {
712            map.serialize_entry("type", "text")?;
713            map.serialize_entry("value", s)?;
714        }
715        LemmaValue::Date(dt) => {
716            map.serialize_entry("type", "date")?;
717            map.serialize_entry("value", &dt.to_string())?;
718        }
719        LemmaValue::Time(time) => {
720            map.serialize_entry("type", "time")?;
721            map.serialize_entry("value", &time.to_string())?;
722        }
723        LemmaValue::Duration(value, unit) => {
724            map.serialize_entry("type", "duration")?;
725            map.serialize_entry("value", &format!("{} {}", value, unit))?;
726        }
727    }
728
729    map.end()
730}
731
732/// Custom serializer for OperationResult
733pub fn serialize_operation_result<S>(
734    result: &crate::evaluation::operations::OperationResult,
735    serializer: S,
736) -> Result<S::Ok, S::Error>
737where
738    S: serde::ser::Serializer,
739{
740    use crate::evaluation::operations::OperationResult;
741    use serde::ser::SerializeMap;
742
743    match result {
744        OperationResult::Value(lit_val) => {
745            // Just serialize the literal value directly
746            serialize_literal_value(lit_val, serializer)
747        }
748        OperationResult::Veto(msg) => {
749            let mut map = serializer.serialize_map(Some(if msg.is_some() { 2 } else { 1 }))?;
750            map.serialize_entry("type", "veto")?;
751            if let Some(m) = msg {
752                map.serialize_entry("message", m)?;
753            }
754            map.end()
755        }
756    }
757}
758
759/// Custom serializer for HashMap<FactPath, LemmaFact>
760///
761/// JSON object keys must be strings, so FactPath keys are serialized as strings
762/// using their Display implementation (e.g., "age" or "employee.salary").
763pub fn serialize_fact_path_map<S>(
764    map: &HashMap<FactPath, LemmaFact>,
765    serializer: S,
766) -> Result<S::Ok, S::Error>
767where
768    S: Serializer,
769{
770    use serde::ser::SerializeMap;
771    let mut map_serializer = serializer.serialize_map(Some(map.len()))?;
772    for (key, value) in map {
773        map_serializer.serialize_entry(&key.to_string(), value)?;
774    }
775    map_serializer.end()
776}
777
778/// Custom deserializer for HashMap<FactPath, LemmaFact>
779///
780/// Deserializes string keys back to FactPath using FactPath::from_path().
781pub fn deserialize_fact_path_map<'de, D>(
782    deserializer: D,
783) -> Result<HashMap<FactPath, LemmaFact>, D::Error>
784where
785    D: Deserializer<'de>,
786{
787    let map: HashMap<String, LemmaFact> = HashMap::deserialize(deserializer)?;
788    let mut result = HashMap::new();
789    for (key_str, value) in map {
790        let path_parts: Vec<String> = key_str.split('.').map(|s| s.to_string()).collect();
791        let fact_path = FactPath::from_path(path_parts);
792        result.insert(fact_path, value);
793    }
794    Ok(result)
795}
796
797/// Custom serializer for HashMap<FactPath, LemmaType>
798///
799/// JSON object keys must be strings, so FactPath keys are serialized as strings
800/// using their Display implementation (e.g., "age" or "employee.salary").
801pub fn serialize_fact_type_map<S>(
802    map: &HashMap<FactPath, LemmaType>,
803    serializer: S,
804) -> Result<S::Ok, S::Error>
805where
806    S: Serializer,
807{
808    use serde::ser::SerializeMap;
809    let mut map_serializer = serializer.serialize_map(Some(map.len()))?;
810    for (key, value) in map {
811        map_serializer.serialize_entry(&key.to_string(), value)?;
812    }
813    map_serializer.end()
814}
815
816/// Custom deserializer for HashMap<FactPath, LemmaType>
817///
818/// Deserializes string keys back to FactPath using FactPath::from_path().
819pub fn deserialize_fact_type_map<'de, D>(
820    deserializer: D,
821) -> Result<HashMap<FactPath, LemmaType>, D::Error>
822where
823    D: Deserializer<'de>,
824{
825    let map: HashMap<String, LemmaType> = HashMap::deserialize(deserializer)?;
826    let mut result = HashMap::new();
827    for (key_str, value) in map {
828        let path_parts: Vec<String> = key_str.split('.').map(|s| s.to_string()).collect();
829        let fact_path = FactPath::from_path(path_parts);
830        result.insert(fact_path, value);
831    }
832    Ok(result)
833}
834
835/// Custom serializer for HashMap<FactPath, LiteralValue>
836///
837/// JSON object keys must be strings, so FactPath keys are serialized as strings
838/// using their Display implementation (e.g., "age" or "employee.salary").
839pub fn serialize_fact_value_map<S>(
840    map: &HashMap<FactPath, LiteralValue>,
841    serializer: S,
842) -> Result<S::Ok, S::Error>
843where
844    S: Serializer,
845{
846    use serde::ser::SerializeMap;
847    let mut map_serializer = serializer.serialize_map(Some(map.len()))?;
848    for (key, value) in map {
849        map_serializer.serialize_entry(&key.to_string(), value)?;
850    }
851    map_serializer.end()
852}
853
854/// Custom deserializer for HashMap<FactPath, LiteralValue>
855///
856/// Deserializes string keys back to FactPath using FactPath::from_path().
857pub fn deserialize_fact_value_map<'de, D>(
858    deserializer: D,
859) -> Result<HashMap<FactPath, LiteralValue>, D::Error>
860where
861    D: Deserializer<'de>,
862{
863    let map: HashMap<String, LiteralValue> = HashMap::deserialize(deserializer)?;
864    let mut result = HashMap::new();
865    for (key_str, value) in map {
866        let path_parts: Vec<String> = key_str.split('.').map(|s| s.to_string()).collect();
867        let fact_path = FactPath::from_path(path_parts);
868        result.insert(fact_path, value);
869    }
870    Ok(result)
871}
872
873/// Custom serializer for HashMap<FactPath, String> (document references)
874pub fn serialize_fact_doc_ref_map<S>(
875    map: &HashMap<FactPath, String>,
876    serializer: S,
877) -> Result<S::Ok, S::Error>
878where
879    S: Serializer,
880{
881    use serde::ser::SerializeMap;
882    let mut map_serializer = serializer.serialize_map(Some(map.len()))?;
883    for (key, value) in map {
884        map_serializer.serialize_entry(&key.to_string(), value)?;
885    }
886    map_serializer.end()
887}
888
889/// Custom deserializer for HashMap<FactPath, String> (document references)
890pub fn deserialize_fact_doc_ref_map<'de, D>(
891    deserializer: D,
892) -> Result<HashMap<FactPath, String>, D::Error>
893where
894    D: Deserializer<'de>,
895{
896    let map: HashMap<String, String> = HashMap::deserialize(deserializer)?;
897    let mut result = HashMap::new();
898    for (key_str, value) in map {
899        let path_parts: Vec<String> = key_str.split('.').map(|s| s.to_string()).collect();
900        let fact_path = FactPath::from_path(path_parts);
901        result.insert(fact_path, value);
902    }
903    Ok(result)
904}
905
906/// Custom serializer for HashMap<FactPath, Source> (fact sources)
907pub fn serialize_fact_source_map<S>(
908    map: &HashMap<FactPath, Source>,
909    serializer: S,
910) -> Result<S::Ok, S::Error>
911where
912    S: Serializer,
913{
914    use serde::ser::SerializeMap;
915    let mut map_serializer = serializer.serialize_map(Some(map.len()))?;
916    for (key, value) in map {
917        map_serializer.serialize_entry(&key.to_string(), value)?;
918    }
919    map_serializer.end()
920}
921
922/// Custom deserializer for HashMap<FactPath, Source> (fact sources)
923pub fn deserialize_fact_source_map<'de, D>(
924    deserializer: D,
925) -> Result<HashMap<FactPath, Source>, D::Error>
926where
927    D: Deserializer<'de>,
928{
929    let map: HashMap<String, Source> = HashMap::deserialize(deserializer)?;
930    let mut result = HashMap::new();
931    for (key_str, value) in map {
932        let path_parts: Vec<String> = key_str.split('.').map(|s| s.to_string()).collect();
933        let fact_path = FactPath::from_path(path_parts);
934        result.insert(fact_path, value);
935    }
936    Ok(result)
937}
938
939/// Custom serializer for HashSet<FactPath>
940///
941/// Serializes as a JSON array of strings using FactPath's Display implementation.
942pub fn serialize_fact_path_set<S>(set: &HashSet<FactPath>, serializer: S) -> Result<S::Ok, S::Error>
943where
944    S: Serializer,
945{
946    use serde::ser::SerializeSeq;
947    let mut seq = serializer.serialize_seq(Some(set.len()))?;
948    for item in set {
949        seq.serialize_element(&item.to_string())?;
950    }
951    seq.end()
952}
953
954/// Custom deserializer for HashSet<FactPath>
955///
956/// Deserializes array of strings back to FactPath using FactPath::from_path().
957pub fn deserialize_fact_path_set<'de, D>(deserializer: D) -> Result<HashSet<FactPath>, D::Error>
958where
959    D: Deserializer<'de>,
960{
961    let vec: Vec<String> = Vec::deserialize(deserializer)?;
962    let mut result = HashSet::new();
963    for key_str in vec {
964        let path_parts: Vec<String> = key_str.split('.').map(|s| s.to_string()).collect();
965        let fact_path = FactPath::from_path(path_parts);
966        result.insert(fact_path);
967    }
968    Ok(result)
969}
970
971#[cfg(test)]
972mod tests {
973    use super::*;
974    use crate::semantic::{
975        standard_boolean, standard_date, standard_duration, standard_number, standard_ratio,
976        standard_text, standard_time, FactPath, LemmaType, LiteralValue,
977    };
978    use rust_decimal::Decimal;
979
980    fn create_test_plan(facts: Vec<(&str, LemmaType)>) -> ExecutionPlan {
981        let mut fact_schema = HashMap::new();
982        for (name, lemma_type) in facts {
983            let fact_path = FactPath {
984                segments: vec![],
985                fact: name.to_string(),
986            };
987            fact_schema.insert(fact_path, lemma_type);
988        }
989        ExecutionPlan {
990            doc_name: "test".to_string(),
991            fact_schema,
992            fact_values: HashMap::new(),
993            doc_refs: HashMap::new(),
994            fact_sources: HashMap::new(),
995            rules: vec![],
996            sources: HashMap::new(),
997        }
998    }
999
1000    fn create_text_literal(s: String) -> LiteralValue {
1001        LiteralValue::text(s)
1002    }
1003
1004    fn create_number_literal(n: Decimal) -> LiteralValue {
1005        LiteralValue::number(n)
1006    }
1007
1008    fn create_percentage_literal(p: Decimal) -> LiteralValue {
1009        // Convert percent (e.g., 50) to ratio (0.50) with "percent" unit
1010        // Note: This function is for tests that expect the old behavior where bare numbers
1011        // were treated as percentages. New code should use explicit "10%" format.
1012        LiteralValue::ratio(p / Decimal::from(100), Some("percent".to_string()))
1013    }
1014
1015    #[test]
1016    fn test_text_from_string() {
1017        let plan = create_test_plan(vec![("name", standard_text().clone())]);
1018        let json = br#"{"name": "Alice"}"#;
1019        let result = from_json(json, &plan).unwrap();
1020        assert_eq!(
1021            result.get("name"),
1022            Some(&create_text_literal("Alice".to_string()))
1023        );
1024    }
1025
1026    #[test]
1027    fn test_text_from_number() {
1028        let plan = create_test_plan(vec![("name", standard_text().clone())]);
1029        let json = br#"{"name": 42}"#;
1030        let result = from_json(json, &plan).unwrap();
1031        assert_eq!(
1032            result.get("name"),
1033            Some(&create_text_literal("42".to_string()))
1034        );
1035    }
1036
1037    #[test]
1038    fn test_text_from_boolean() {
1039        let plan = create_test_plan(vec![("name", standard_text().clone())]);
1040        let json = br#"{"name": true}"#;
1041        let result = from_json(json, &plan).unwrap();
1042        assert_eq!(
1043            result.get("name"),
1044            Some(&create_text_literal("true".to_string()))
1045        );
1046    }
1047
1048    #[test]
1049    fn test_text_from_array() {
1050        let plan = create_test_plan(vec![("data", standard_text().clone())]);
1051        let json = br#"{"data": [1, 2, 3]}"#;
1052        let result = from_json(json, &plan).unwrap();
1053        assert_eq!(
1054            result.get("data"),
1055            Some(&create_text_literal("[1,2,3]".to_string()))
1056        );
1057    }
1058
1059    #[test]
1060    fn test_text_from_object() {
1061        let plan = create_test_plan(vec![("config", standard_text().clone())]);
1062        let json = br#"{"config": {"key": "value"}}"#;
1063        let result = from_json(json, &plan).unwrap();
1064        assert_eq!(
1065            result.get("config"),
1066            Some(&create_text_literal("{\"key\":\"value\"}".to_string()))
1067        );
1068    }
1069
1070    #[test]
1071    fn test_number_from_integer() {
1072        let plan = create_test_plan(vec![("count", standard_number().clone())]);
1073        let json = br#"{"count": 42}"#;
1074        let result = from_json(json, &plan).unwrap();
1075        assert_eq!(
1076            result.get("count"),
1077            Some(&create_number_literal(Decimal::from(42)))
1078        );
1079    }
1080
1081    #[test]
1082    fn test_number_from_decimal() {
1083        let plan = create_test_plan(vec![("price", standard_number().clone())]);
1084        let json = br#"{"price": 99.95}"#;
1085        let result = from_json(json, &plan).unwrap();
1086        match result.get("price") {
1087            Some(lit) => {
1088                if let LemmaValue::Number(n) = &lit.value {
1089                    let expected = Decimal::try_from(99.95).unwrap();
1090                    let tolerance = Decimal::try_from(0.001).unwrap();
1091                    assert!((*n - expected).abs() < tolerance);
1092                } else {
1093                    panic!("Expected Number, got {:?}", lit);
1094                }
1095            }
1096            other => panic!("Expected Number, got {:?}", other),
1097        }
1098    }
1099
1100    #[test]
1101    fn test_number_from_string() {
1102        let plan = create_test_plan(vec![("count", standard_number().clone())]);
1103        let json = br#"{"count": "42"}"#;
1104        let result = from_json(json, &plan).unwrap();
1105        assert_eq!(
1106            result.get("count"),
1107            Some(&create_number_literal(Decimal::from(42)))
1108        );
1109    }
1110
1111    #[test]
1112    fn test_number_from_string_with_formatting() {
1113        let plan = create_test_plan(vec![("price", standard_number().clone())]);
1114        let json = br#"{"price": "1,234.56"}"#;
1115        let result = from_json(json, &plan).unwrap();
1116        match result.get("price") {
1117            Some(lit) => {
1118                if let LemmaValue::Number(n) = &lit.value {
1119                    let expected = Decimal::try_from(1234.56).unwrap();
1120                    let tolerance = Decimal::try_from(0.001).unwrap();
1121                    assert!((*n - expected).abs() < tolerance);
1122                } else {
1123                    panic!("Expected Number, got {:?}", lit);
1124                }
1125            }
1126            other => panic!("Expected Number, got {:?}", other),
1127        }
1128    }
1129
1130    #[test]
1131    fn test_number_from_invalid_string() {
1132        let plan = create_test_plan(vec![("count", standard_number().clone())]);
1133        let json = br#"{"count": "hello"}"#;
1134        let result = from_json(json, &plan);
1135        assert!(result.is_err());
1136        let error_message = result.unwrap_err().to_string();
1137        assert!(error_message.contains("Invalid number string"));
1138    }
1139
1140    #[test]
1141    fn test_number_rejects_boolean() {
1142        let plan = create_test_plan(vec![("count", standard_number().clone())]);
1143        let json = br#"{"count": true}"#;
1144        let result = from_json(json, &plan);
1145        assert!(result.is_err());
1146        let error_message = result.unwrap_err().to_string();
1147        assert!(error_message.contains("expected number"));
1148        assert!(error_message.contains("got boolean"));
1149    }
1150
1151    #[test]
1152    fn test_boolean_from_true() {
1153        let plan = create_test_plan(vec![("active", standard_boolean().clone())]);
1154        let json = br#"{"active": true}"#;
1155        let result = from_json(json, &plan).unwrap();
1156        match result.get("active") {
1157            Some(lit) => {
1158                if let LemmaValue::Boolean(b) = &lit.value {
1159                    assert!(bool::from(b));
1160                } else {
1161                    panic!("Expected Boolean, got {:?}", lit);
1162                }
1163            }
1164            other => panic!("Expected Boolean, got {:?}", other),
1165        }
1166    }
1167
1168    #[test]
1169    fn test_boolean_from_false() {
1170        let plan = create_test_plan(vec![("active", standard_boolean().clone())]);
1171        let json = br#"{"active": false}"#;
1172        let result = from_json(json, &plan).unwrap();
1173        match result.get("active") {
1174            Some(lit) => {
1175                if let LemmaValue::Boolean(b) = &lit.value {
1176                    assert!(!bool::from(b));
1177                } else {
1178                    panic!("Expected Boolean, got {:?}", lit);
1179                }
1180            }
1181            other => panic!("Expected Boolean, got {:?}", other),
1182        }
1183    }
1184
1185    #[test]
1186    fn test_boolean_from_string_yes() {
1187        let plan = create_test_plan(vec![("active", standard_boolean().clone())]);
1188        let json = br#"{"active": "yes"}"#;
1189        let result = from_json(json, &plan).unwrap();
1190        match result.get("active") {
1191            Some(lit) => {
1192                if let LemmaValue::Boolean(b) = &lit.value {
1193                    assert!(bool::from(b));
1194                } else {
1195                    panic!("Expected Boolean, got {:?}", lit);
1196                }
1197            }
1198            other => panic!("Expected Boolean, got {:?}", other),
1199        }
1200    }
1201
1202    #[test]
1203    fn test_boolean_from_string_no() {
1204        let plan = create_test_plan(vec![("active", standard_boolean().clone())]);
1205        let json = br#"{"active": "no"}"#;
1206        let result = from_json(json, &plan).unwrap();
1207        match result.get("active") {
1208            Some(lit) => {
1209                if let LemmaValue::Boolean(b) = &lit.value {
1210                    assert!(!bool::from(b));
1211                } else {
1212                    panic!("Expected Boolean, got {:?}", lit);
1213                }
1214            }
1215            other => panic!("Expected Boolean, got {:?}", other),
1216        }
1217    }
1218
1219    #[test]
1220    fn test_boolean_rejects_number() {
1221        let plan = create_test_plan(vec![("active", standard_boolean().clone())]);
1222        let json = br#"{"active": 1}"#;
1223        let result = from_json(json, &plan);
1224        assert!(result.is_err());
1225        let error_message = result.unwrap_err().to_string();
1226        assert!(error_message.contains("expected boolean"));
1227        assert!(error_message.contains("got number"));
1228    }
1229
1230    #[test]
1231    fn test_boolean_rejects_invalid_string() {
1232        let plan = create_test_plan(vec![("active", standard_boolean().clone())]);
1233        let json = br#"{"active": "maybe"}"#;
1234        let result = from_json(json, &plan);
1235        assert!(result.is_err());
1236        let error_message = result.unwrap_err().to_string();
1237        assert!(error_message.contains("Invalid boolean string"));
1238    }
1239
1240    #[test]
1241    fn test_percentage_from_number() {
1242        // JSON number 21 for ratio type is now treated as ratio 21, not percentage
1243        let plan = create_test_plan(vec![("discount", standard_ratio().clone())]);
1244        let json = br#"{"discount": 21}"#;
1245        let result = from_json(json, &plan).unwrap();
1246        assert_eq!(
1247            result.get("discount"),
1248            Some(&LiteralValue::ratio(Decimal::from(21), None))
1249        );
1250    }
1251
1252    #[test]
1253    fn test_percentage_from_string_with_percent_sign() {
1254        let plan = create_test_plan(vec![("discount", standard_ratio().clone())]);
1255        let json = br#"{"discount": "21%"}"#;
1256        let result = from_json(json, &plan).unwrap();
1257        assert_eq!(
1258            result.get("discount"),
1259            Some(&create_percentage_literal(Decimal::from(21)))
1260        );
1261    }
1262
1263    #[test]
1264    fn test_percentage_from_string_with_percent_word() {
1265        let plan = create_test_plan(vec![("discount", standard_ratio().clone())]);
1266        let json = br#"{"discount": "21 percent"}"#;
1267        let result = from_json(json, &plan).unwrap();
1268        assert_eq!(
1269            result.get("discount"),
1270            Some(&create_percentage_literal(Decimal::from(21)))
1271        );
1272    }
1273
1274    #[test]
1275    fn test_percentage_from_bare_string() {
1276        // Bare string "21" is now treated as ratio 21, not percentage 21%
1277        let plan = create_test_plan(vec![("discount", standard_ratio().clone())]);
1278        let json = br#"{"discount": "21"}"#;
1279        let result = from_json(json, &plan).unwrap();
1280        assert_eq!(
1281            result.get("discount"),
1282            Some(&LiteralValue::ratio(Decimal::from(21), None))
1283        );
1284    }
1285
1286    #[test]
1287    fn test_percentage_from_invalid_string() {
1288        let plan = create_test_plan(vec![("discount", standard_ratio().clone())]);
1289        let json = br#"{"discount": "hello"}"#;
1290        let result = from_json(json, &plan);
1291        assert!(result.is_err());
1292        let error_message = result.unwrap_err().to_string();
1293        assert!(error_message.contains("Invalid ratio string"));
1294    }
1295
1296    #[test]
1297    fn test_percentage_rejects_boolean() {
1298        let plan = create_test_plan(vec![("discount", standard_ratio().clone())]);
1299        let json = br#"{"discount": false}"#;
1300        let result = from_json(json, &plan);
1301        assert!(result.is_err());
1302        let error_message = result.unwrap_err().to_string();
1303        assert!(error_message.contains("expected ratio"));
1304        assert!(error_message.contains("got boolean"));
1305    }
1306
1307    #[test]
1308    fn test_date_from_string() {
1309        let plan = create_test_plan(vec![("start_date", standard_date().clone())]);
1310        let json = br#"{"start_date": "2024-01-15"}"#;
1311        let result = from_json(json, &plan).unwrap();
1312        match result.get("start_date") {
1313            Some(lit) => {
1314                if let LemmaValue::Date(dt) = &lit.value {
1315                    assert_eq!(dt.year, 2024);
1316                    assert_eq!(dt.month, 1);
1317                    assert_eq!(dt.day, 15);
1318                } else {
1319                    panic!("Expected Date, got {:?}", lit);
1320                }
1321            }
1322            other => panic!("Expected Date, got {:?}", other),
1323        }
1324    }
1325
1326    #[test]
1327    fn test_date_rejects_number() {
1328        let plan = create_test_plan(vec![("start_date", standard_date().clone())]);
1329        let json = br#"{"start_date": 20240115}"#;
1330        let result = from_json(json, &plan);
1331        assert!(result.is_err());
1332        let error_message = result.unwrap_err().to_string();
1333        assert!(error_message.contains("expected date"));
1334        assert!(error_message.contains("got number"));
1335    }
1336
1337    #[test]
1338    fn test_duration_from_string() {
1339        let plan = create_test_plan(vec![("duration", standard_duration().clone())]);
1340        let json = br#"{"duration": "5 days"}"#;
1341        let result = from_json(json, &plan).unwrap();
1342        match result.get("duration") {
1343            Some(lit) => {
1344                if let LemmaValue::Duration(value, unit) = &lit.value {
1345                    assert_eq!(*value, Decimal::from(5));
1346                    assert_eq!(*unit, crate::DurationUnit::Day);
1347                } else {
1348                    panic!("Expected Duration, got {:?}", lit);
1349                }
1350            }
1351            other => panic!("Expected Duration, got {:?}", other),
1352        }
1353    }
1354
1355    #[test]
1356    fn test_duration_rejects_number() {
1357        let plan = create_test_plan(vec![("duration", standard_duration().clone())]);
1358        let json = br#"{"duration": 100}"#;
1359        let result = from_json(json, &plan);
1360        assert!(result.is_err());
1361        let error_message = result.unwrap_err().to_string();
1362        assert!(error_message.contains("Duration values must include the unit name"));
1363    }
1364
1365    #[test]
1366    fn test_time_from_string_hhmm() {
1367        let plan = create_test_plan(vec![("start_time", standard_time().clone())]);
1368        let json = br#"{"start_time": "14:30"}"#;
1369        let result = from_json(json, &plan).unwrap();
1370        match result.get("start_time") {
1371            Some(lit) => {
1372                if let LemmaValue::Time(t) = &lit.value {
1373                    assert_eq!(t.hour, 14);
1374                    assert_eq!(t.minute, 30);
1375                    assert_eq!(t.second, 0);
1376                    assert_eq!(t.timezone, None);
1377                } else {
1378                    panic!("Expected Time, got {:?}", lit);
1379                }
1380            }
1381            other => panic!("Expected Time, got {:?}", other),
1382        }
1383    }
1384
1385    #[test]
1386    fn test_time_from_string_hhmmss() {
1387        let plan = create_test_plan(vec![("start_time", standard_time().clone())]);
1388        let json = br#"{"start_time": "14:30:45"}"#;
1389        let result = from_json(json, &plan).unwrap();
1390        match result.get("start_time") {
1391            Some(lit) => {
1392                if let LemmaValue::Time(t) = &lit.value {
1393                    assert_eq!(t.hour, 14);
1394                    assert_eq!(t.minute, 30);
1395                    assert_eq!(t.second, 45);
1396                    assert_eq!(t.timezone, None);
1397                } else {
1398                    panic!("Expected Time, got {:?}", lit);
1399                }
1400            }
1401            other => panic!("Expected Time, got {:?}", other),
1402        }
1403    }
1404
1405    #[test]
1406    fn test_time_from_string_with_timezone() {
1407        let plan = create_test_plan(vec![("start_time", standard_time().clone())]);
1408        // Test with Z timezone format (UTC)
1409        let json = br#"{"start_time": "14:30:00Z"}"#;
1410        let result = from_json(json, &plan);
1411        // Note: Timezone parsing may not work for all formats yet
1412        // This test verifies the conversion function is called and handles the input
1413        // If timezone parsing fails, it should return a proper error, not panic
1414        match result {
1415            Ok(values) => {
1416                // If parsing succeeds, verify it's a valid time
1417                if let Some(lit) = values.get("start_time") {
1418                    if let LemmaValue::Time(t) = &lit.value {
1419                        assert!(t.hour < 24 && t.minute < 60 && t.second < 60);
1420                    }
1421                }
1422            }
1423            Err(_) => {
1424                // Timezone parsing may not be fully supported yet, which is acceptable
1425                // The important thing is that convert_to_time is being called
1426            }
1427        }
1428    }
1429
1430    #[test]
1431    fn test_time_rejects_number() {
1432        let plan = create_test_plan(vec![("start_time", standard_time().clone())]);
1433        let json = br#"{"start_time": 1430}"#;
1434        let result = from_json(json, &plan);
1435        assert!(result.is_err());
1436        let error_message = result.unwrap_err().to_string();
1437        assert!(error_message.contains("expected time"));
1438        assert!(error_message.contains("got number"));
1439    }
1440
1441    #[test]
1442    fn test_time_rejects_invalid_format() {
1443        let plan = create_test_plan(vec![("start_time", standard_time().clone())]);
1444        let json = br#"{"start_time": "25:00"}"#;
1445        let result = from_json(json, &plan);
1446        assert!(result.is_err());
1447        let error_message = result.unwrap_err().to_string();
1448        assert!(error_message.contains("Invalid time"));
1449    }
1450
1451    #[test]
1452    fn test_unknown_fact_error() {
1453        let plan = create_test_plan(vec![("known", standard_text().clone())]);
1454        let json = br#"{"unknown": "value"}"#;
1455        let result = from_json(json, &plan);
1456        assert!(result.is_err());
1457        let error_message = result.unwrap_err().to_string();
1458        assert!(error_message.contains("Fact 'unknown' not found"));
1459        assert!(error_message.contains("Available facts"));
1460    }
1461
1462    #[test]
1463    fn test_null_value_skipped() {
1464        let plan = create_test_plan(vec![
1465            ("name", standard_text().clone()),
1466            ("age", standard_number().clone()),
1467        ]);
1468        let json = br#"{"name": null, "age": 30}"#;
1469        let result = from_json(json, &plan).unwrap();
1470        assert_eq!(result.len(), 1);
1471        assert!(!result.contains_key("name"));
1472        assert_eq!(
1473            result.get("age"),
1474            Some(&create_number_literal(Decimal::from(30)))
1475        );
1476    }
1477
1478    #[test]
1479    fn test_all_null_values() {
1480        let plan = create_test_plan(vec![("name", standard_text().clone())]);
1481        let json = br#"{"name": null}"#;
1482        let result = from_json(json, &plan).unwrap();
1483        assert!(result.is_empty());
1484    }
1485
1486    #[test]
1487    fn test_array_value_for_non_text() {
1488        let plan = create_test_plan(vec![("items", standard_number().clone())]);
1489        let json = br#"{"items": [1, 2, 3]}"#;
1490        let result = from_json(json, &plan);
1491        assert!(result.is_err());
1492        let error_message = result.unwrap_err().to_string();
1493        assert!(error_message.contains("got array"));
1494    }
1495
1496    #[test]
1497    fn test_object_value_for_non_text() {
1498        let plan = create_test_plan(vec![("config", standard_number().clone())]);
1499        let json = br#"{"config": {"key": "value"}}"#;
1500        let result = from_json(json, &plan);
1501        assert!(result.is_err());
1502        let error_message = result.unwrap_err().to_string();
1503        assert!(error_message.contains("got object"));
1504    }
1505
1506    #[test]
1507    fn test_mixed_valid_types() {
1508        let plan = create_test_plan(vec![
1509            ("name", standard_text().clone()),
1510            ("count", standard_number().clone()),
1511            ("active", standard_boolean().clone()),
1512            ("discount", standard_ratio().clone()),
1513        ]);
1514        let json = br#"{"name": "Test", "count": 5, "active": true, "discount": 21}"#;
1515        let result = from_json(json, &plan).unwrap();
1516        assert_eq!(result.len(), 4);
1517        assert_eq!(
1518            result.get("name"),
1519            Some(&create_text_literal("Test".to_string()))
1520        );
1521        assert_eq!(
1522            result.get("count"),
1523            Some(&create_number_literal(Decimal::from(5)))
1524        );
1525        // JSON number 21 for ratio type is treated as ratio 21, not percentage
1526        assert_eq!(
1527            result.get("discount"),
1528            Some(&LiteralValue::ratio(Decimal::from(21), None))
1529        );
1530    }
1531
1532    #[test]
1533    fn test_invalid_json_syntax() {
1534        let plan = create_test_plan(vec![("name", standard_text().clone())]);
1535        let json = br#"{"name": }"#;
1536        let result = from_json(json, &plan);
1537        assert!(result.is_err());
1538        let error_message = result.unwrap_err().to_string();
1539        assert!(error_message.contains("JSON parse error"));
1540    }
1541}