Skip to main content

stepflow_flow/values/
value_expr_serde.rs

1// Copyright 2025 DataStax Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
4// in compliance with the License. You may obtain a copy of the License at
5//
6//     http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software distributed under the License
9// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
10// or implied. See the License for the specific language governing permissions and limitations under
11// the License.
12
13// Custom deserialization for ValueExpr
14//
15// We implement custom deserialization by first deserializing to serde_json::Value,
16// then parsing that into ValueExpr. This avoids the trial-and-error overhead of
17// untagged enums and gives us precise control over parsing.
18//
19// We could further speed this up by writing our own deserializer directly to ValueExpr
20// but that would require more special handling.
21
22use super::value_expr::ValueExpr;
23use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
24use serde_json::Value;
25
26impl Serialize for ValueExpr {
27    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
28    where
29        S: Serializer,
30    {
31        use serde::ser::SerializeMap as _;
32
33        match self {
34            ValueExpr::Step { step, path } => {
35                let mut map = serializer.serialize_map(Some(2))?;
36                map.serialize_entry("$step", step)?;
37                if !path.is_empty() {
38                    map.serialize_entry("path", path)?;
39                }
40                map.end()
41            }
42            ValueExpr::Input { input } => {
43                let mut map = serializer.serialize_map(Some(1))?;
44                map.serialize_entry("$input", input)?;
45                map.end()
46            }
47            ValueExpr::Variable { variable, default } => {
48                let mut map = serializer.serialize_map(Some(2))?;
49                map.serialize_entry("$variable", variable)?;
50                if let Some(def) = default {
51                    map.serialize_entry("default", def)?;
52                }
53                map.end()
54            }
55            ValueExpr::EscapedLiteral { literal } => {
56                let mut map = serializer.serialize_map(Some(1))?;
57                map.serialize_entry("$literal", literal)?;
58                map.end()
59            }
60            ValueExpr::If {
61                condition,
62                then,
63                else_expr,
64            } => {
65                let mut map = serializer.serialize_map(Some(3))?;
66                map.serialize_entry("$if", condition)?;
67                map.serialize_entry("then", then)?;
68                if let Some(else_val) = else_expr {
69                    map.serialize_entry("else", else_val)?;
70                }
71                map.end()
72            }
73            ValueExpr::Coalesce { values } => {
74                let mut map = serializer.serialize_map(Some(1))?;
75                map.serialize_entry("$coalesce", values)?;
76                map.end()
77            }
78            ValueExpr::Array(items) => items.serialize(serializer),
79            ValueExpr::Object(fields) => {
80                let mut map = serializer.serialize_map(Some(fields.len()))?;
81                for (k, v) in fields {
82                    map.serialize_entry(k, v)?;
83                }
84                map.end()
85            }
86            ValueExpr::Literal(value) => value.serialize(serializer),
87        }
88    }
89}
90
91impl<'de> Deserialize<'de> for ValueExpr {
92    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
93    where
94        D: Deserializer<'de>,
95    {
96        let value = Value::deserialize(deserializer)?;
97        parse_value_expr(value).map_err(de::Error::custom)
98    }
99}
100
101/// Parse a serde_json::Value into a ValueExpr
102///
103/// Detection order:
104/// 1. Check for $ prefixed keys ($literal, $if, $coalesce, $from [rejected], $step, $input, $variable)
105/// 2. Recursively parse arrays/objects
106/// 3. Fall through to Literal for primitives
107fn parse_value_expr(value: Value) -> Result<ValueExpr, String> {
108    match value {
109        Value::Object(obj) if obj.contains_key("$literal") => {
110            // EscapedLiteral - extract the literal value
111            let literal = obj
112                .get("$literal")
113                .ok_or_else(|| "$literal key not found".to_string())?
114                .clone();
115            Ok(ValueExpr::EscapedLiteral { literal })
116        }
117        Value::Object(ref obj) if obj.contains_key("$if") => {
118            // Conditional expression
119            let condition = obj
120                .get("$if")
121                .ok_or_else(|| "$if key not found".to_string())?;
122            let then = obj
123                .get("then")
124                .ok_or_else(|| "then key required for $if".to_string())?;
125            let else_expr = obj.get("else");
126
127            let condition_expr = parse_value_expr(condition.clone())?;
128            let then_expr = parse_value_expr(then.clone())?;
129            let else_opt = else_expr.map(|e| parse_value_expr(e.clone())).transpose()?;
130
131            Ok(ValueExpr::If {
132                condition: Box::new(condition_expr),
133                then: Box::new(then_expr),
134                else_expr: else_opt.map(Box::new),
135            })
136        }
137        Value::Object(ref obj) if obj.contains_key("$coalesce") => {
138            // Coalesce expression
139            let values_val = obj
140                .get("$coalesce")
141                .ok_or_else(|| "$coalesce key not found".to_string())?;
142
143            let values_array = values_val
144                .as_array()
145                .ok_or_else(|| "$coalesce must be an array".to_string())?;
146
147            let mut values = Vec::new();
148            for v in values_array {
149                values.push(parse_value_expr(v.clone())?);
150            }
151
152            Ok(ValueExpr::Coalesce { values })
153        }
154        Value::Object(ref obj) if obj.contains_key("$from") => {
155            // Legacy syntax: {"$from": {"step": "id"}, "path": "..."} or {"$from": {"workflow": "input"}, "path": "..."}
156            // was replaced by $step/$input in the new syntax.
157            Err(
158                "Legacy '$from' syntax is no longer supported. \
159                 Migrate to the new syntax: use {\"$step\": \"step_id\", \"path\": \"...\"} \
160                 instead of {\"$from\": {\"step\": \"step_id\"}, \"path\": \"...\"}, \
161                 and {\"$input\": \"field\"} instead of {\"$from\": {\"workflow\": \"input\"}, \"path\": \"field\"}. \
162                 If you need a literal object containing a \"$from\" key, wrap it in \
163                 $literal: {\"$literal\": {\"$from\": ...}}"
164                    .to_string(),
165            )
166        }
167        Value::Object(ref obj) if obj.contains_key("$step") => {
168            // Step reference - extract step and optional path
169            let step = obj
170                .get("$step")
171                .and_then(|v| v.as_str())
172                .ok_or_else(|| "$step must be a string".to_string())?
173                .to_string();
174
175            let path = if let Some(path_value) = obj.get("path") {
176                serde_json::from_value(path_value.clone())
177                    .map_err(|e| format!("Invalid path: {}", e))?
178            } else {
179                super::JsonPath::default()
180            };
181
182            Ok(ValueExpr::Step { step, path })
183        }
184        Value::Object(ref obj) if obj.contains_key("$input") => {
185            // Workflow input - extract input path
186            let input = obj
187                .get("$input")
188                .ok_or_else(|| "$input key not found".to_string())?;
189
190            let input_path: super::JsonPath = serde_json::from_value(input.clone())
191                .map_err(|e| format!("Invalid $input: {}", e))?;
192
193            Ok(ValueExpr::Input { input: input_path })
194        }
195        Value::Object(ref obj) if obj.contains_key("$variable") => {
196            // Variable reference - extract variable name/path and optional default
197            let variable = obj
198                .get("$variable")
199                .ok_or_else(|| "$variable key not found".to_string())?;
200
201            let variable_path: super::JsonPath = serde_json::from_value(variable.clone())
202                .map_err(|e| format!("Invalid $variable: {}", e))?;
203
204            let default = if let Some(default_value) = obj.get("default") {
205                let default_expr = parse_value_expr(default_value.clone())?;
206                Some(Box::new(default_expr))
207            } else {
208                None
209            };
210
211            Ok(ValueExpr::Variable {
212                variable: variable_path,
213                default,
214            })
215        }
216        Value::Object(obj) => {
217            // Regular object - recurse on values
218            let mut fields = Vec::new();
219            for (k, v) in obj {
220                let expr = parse_value_expr(v)?;
221                fields.push((k, expr));
222            }
223            Ok(ValueExpr::Object(fields))
224        }
225        Value::Array(arr) => {
226            // Array - recurse on elements
227            let mut exprs = Vec::new();
228            for v in arr {
229                let expr = parse_value_expr(v)?;
230                exprs.push(expr);
231            }
232            Ok(ValueExpr::Array(exprs))
233        }
234        // Primitives: null, bool, number, string
235        primitive => Ok(ValueExpr::Literal(primitive)),
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use crate::values::JsonPath;
243    use serde_json::json;
244
245    #[test]
246    fn test_serde_step_reference() {
247        let expr = ValueExpr::step("my_step", JsonPath::default());
248        let json_str = serde_json::to_string(&expr).unwrap();
249        assert_eq!(json_str, r#"{"$step":"my_step"}"#);
250
251        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
252        assert_eq!(parsed, expr);
253    }
254
255    #[test]
256    fn test_serde_step_with_path() {
257        let expr = ValueExpr::step("my_step", JsonPath::from("result"));
258        let json_str = serde_json::to_string(&expr).unwrap();
259        // JsonPath normalizes "result" to "$.result"
260        assert_eq!(json_str, r#"{"$step":"my_step","path":"$.result"}"#);
261
262        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
263        assert_eq!(parsed, expr);
264
265        // Test that shorthand also parses correctly
266        let shorthand: ValueExpr =
267            serde_json::from_str(r#"{"$step":"my_step","path":"result"}"#).unwrap();
268        assert_eq!(shorthand, expr);
269    }
270
271    #[test]
272    fn test_serde_input() {
273        let expr = ValueExpr::workflow_input(JsonPath::from("name"));
274        let json_str = serde_json::to_string(&expr).unwrap();
275        // JsonPath normalizes "name" to "$.name"
276        assert_eq!(json_str, r#"{"$input":"$.name"}"#);
277
278        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
279        assert_eq!(parsed, expr);
280
281        // Test that shorthand also parses correctly
282        let shorthand: ValueExpr = serde_json::from_str(r#"{"$input":"name"}"#).unwrap();
283        assert_eq!(shorthand, expr);
284    }
285
286    #[test]
287    fn test_serde_input_root() {
288        let expr = ValueExpr::workflow_input(JsonPath::from("$"));
289        let json_str = serde_json::to_string(&expr).unwrap();
290        assert_eq!(json_str, r#"{"$input":"$"}"#);
291
292        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
293        assert_eq!(parsed, expr);
294    }
295
296    #[test]
297    fn test_serde_variable() {
298        let expr = ValueExpr::variable("api_key", None);
299        let json_str = serde_json::to_string(&expr).unwrap();
300        // JsonPath normalizes "api_key" to "$.api_key"
301        assert_eq!(json_str, r#"{"$variable":"$.api_key"}"#);
302
303        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
304        assert_eq!(parsed, expr);
305
306        // Test that shorthand also parses correctly
307        let shorthand: ValueExpr = serde_json::from_str(r#"{"$variable":"api_key"}"#).unwrap();
308        assert_eq!(shorthand, expr);
309    }
310
311    #[test]
312    fn test_serde_variable_with_default() {
313        let default = Box::new(ValueExpr::literal(json!("default_value")));
314        let expr = ValueExpr::variable("my_var", Some(default));
315        let json_str = serde_json::to_string(&expr).unwrap();
316        // JsonPath normalizes "my_var" to "$.my_var"
317        assert!(json_str.contains(r#""$variable":"$.my_var""#));
318        assert!(json_str.contains(r#""default":"default_value""#));
319
320        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
321        assert_eq!(parsed, expr);
322    }
323
324    #[test]
325    fn test_serde_escaped_literal() {
326        let expr = ValueExpr::escaped_literal(json!({"step": "not_a_ref"}));
327        let json_str = serde_json::to_string(&expr).unwrap();
328        assert_eq!(json_str, r#"{"$literal":{"step":"not_a_ref"}}"#);
329
330        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
331        assert_eq!(parsed, expr);
332    }
333
334    #[test]
335    fn test_serde_primitives() {
336        // Null
337        let expr = ValueExpr::Literal(json!(null));
338        let json_str = serde_json::to_string(&expr).unwrap();
339        assert_eq!(json_str, "null");
340        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
341        assert_eq!(parsed, expr);
342
343        // Bool
344        let expr = ValueExpr::Literal(json!(true));
345        let json_str = serde_json::to_string(&expr).unwrap();
346        assert_eq!(json_str, "true");
347        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
348        assert_eq!(parsed, expr);
349
350        // Number
351        let expr = ValueExpr::Literal(json!(42));
352        let json_str = serde_json::to_string(&expr).unwrap();
353        assert_eq!(json_str, "42");
354        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
355        assert_eq!(parsed, expr);
356
357        // String
358        let expr = ValueExpr::Literal(json!("hello"));
359        let json_str = serde_json::to_string(&expr).unwrap();
360        assert_eq!(json_str, r#""hello""#);
361        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
362        assert_eq!(parsed, expr);
363    }
364
365    #[test]
366    fn test_serde_array() {
367        let expr = ValueExpr::Array(vec![
368            ValueExpr::literal(json!(1)),
369            ValueExpr::literal(json!("two")),
370            ValueExpr::step("step1", JsonPath::default()),
371        ]);
372        let json_str = serde_json::to_string(&expr).unwrap();
373        assert_eq!(json_str, r#"[1,"two",{"$step":"step1"}]"#);
374
375        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
376        assert_eq!(parsed, expr);
377    }
378
379    #[test]
380    fn test_serde_object() {
381        let expr = ValueExpr::Object(vec![
382            ("a".to_string(), ValueExpr::literal(json!(1))),
383            (
384                "b".to_string(),
385                ValueExpr::step("step1", JsonPath::default()),
386            ),
387        ]);
388        let json_str = serde_json::to_string(&expr).unwrap();
389        assert_eq!(json_str, r#"{"a":1,"b":{"$step":"step1"}}"#);
390
391        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
392        assert_eq!(parsed, expr);
393    }
394
395    #[test]
396    fn test_serde_nested_structures() {
397        // Complex nested structure
398        let expr = ValueExpr::Object(vec![
399            (
400                "input".to_string(),
401                ValueExpr::workflow_input(JsonPath::from("data")),
402            ),
403            (
404                "steps".to_string(),
405                ValueExpr::Array(vec![
406                    ValueExpr::step("step1", JsonPath::from("result")),
407                    ValueExpr::step("step2", JsonPath::default()),
408                ]),
409            ),
410            (
411                "config".to_string(),
412                ValueExpr::Object(vec![
413                    ("enabled".to_string(), ValueExpr::literal(json!(true))),
414                    ("count".to_string(), ValueExpr::literal(json!(5))),
415                ]),
416            ),
417        ]);
418
419        let json_str = serde_json::to_string(&expr).unwrap();
420        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
421        assert_eq!(parsed, expr);
422    }
423
424    #[test]
425    fn test_parse_error_invalid_step() {
426        // Missing required field
427        let json_str = r#"{"$step": 123}"#; // step must be string
428        let result: Result<ValueExpr, _> = serde_json::from_str(json_str);
429        assert!(result.is_err());
430    }
431
432    #[test]
433    fn test_parse_error_legacy_from_syntax() {
434        // Legacy $from syntax should produce a clear error
435        let json_str = r#"{"$from": {"step": "my_step"}, "path": "result"}"#;
436        let result: Result<ValueExpr, _> = serde_json::from_str(json_str);
437        assert!(result.is_err());
438        let err_msg = result.unwrap_err().to_string();
439        assert!(
440            err_msg.contains("$from"),
441            "Error should mention $from: {}",
442            err_msg
443        );
444        assert!(
445            err_msg.contains("no longer supported"),
446            "Error should say no longer supported: {}",
447            err_msg
448        );
449
450        // Also test the workflow input variant
451        let json_str = r#"{"$from": {"workflow": "input"}, "path": "name"}"#;
452        let result: Result<ValueExpr, _> = serde_json::from_str(json_str);
453        assert!(result.is_err());
454    }
455
456    #[test]
457    fn test_parse_object_without_dollar_keys() {
458        // Regular object without $ keys should parse as Object variant
459        let json_str = r#"{"a": 1, "b": "hello"}"#;
460        let parsed: ValueExpr = serde_json::from_str(json_str).unwrap();
461
462        match parsed {
463            ValueExpr::Object(fields) => {
464                assert_eq!(fields.len(), 2);
465                assert!(
466                    fields
467                        .iter()
468                        .any(|(k, v)| k == "a" && *v == ValueExpr::Literal(json!(1)))
469                );
470                assert!(
471                    fields
472                        .iter()
473                        .any(|(k, v)| k == "b" && *v == ValueExpr::Literal(json!("hello")))
474                );
475            }
476            _ => panic!("Expected Object variant, got {:?}", parsed),
477        }
478    }
479
480    #[test]
481    fn test_deep_nesting() {
482        // Test deeply nested structures with mixed references and literals
483        let json_str = r#"{
484            "level1": {
485                "level2": {
486                    "level3": {
487                        "ref": {"$step": "deep_step"},
488                        "literal": "value",
489                        "array": [
490                            1,
491                            {"$input": "nested"},
492                            {
493                                "level4": {
494                                    "level5": {"$variable": "deep_var"}
495                                }
496                            }
497                        ]
498                    }
499                }
500            }
501        }"#;
502
503        let parsed: ValueExpr = serde_json::from_str(json_str).unwrap();
504
505        // Verify it round-trips correctly
506        let serialized = serde_json::to_string(&parsed).unwrap();
507        let reparsed: ValueExpr = serde_json::from_str(&serialized).unwrap();
508        assert_eq!(parsed, reparsed);
509
510        // Verify structure
511        match parsed {
512            ValueExpr::Object(fields) => {
513                assert_eq!(fields.len(), 1);
514                assert_eq!(fields[0].0, "level1");
515
516                match &fields[0].1 {
517                    ValueExpr::Object(level2_fields) => {
518                        assert_eq!(level2_fields.len(), 1);
519                        assert_eq!(level2_fields[0].0, "level2");
520                        // We have deep nesting
521                    }
522                    _ => panic!("Expected Object at level2"),
523                }
524            }
525            _ => panic!("Expected Object at top level"),
526        }
527    }
528
529    #[test]
530    fn test_escaped_literal_with_dollar_keys() {
531        // $literal should prevent parsing of nested $ keys
532        let json_str = r#"{"$literal": {"$step": "not_a_ref", "$input": "also_not_a_ref"}}"#;
533        let parsed: ValueExpr = serde_json::from_str(json_str).unwrap();
534
535        match &parsed {
536            ValueExpr::EscapedLiteral { literal } => {
537                // The literal should contain the raw object
538                assert_eq!(
539                    *literal,
540                    json!({"$step": "not_a_ref", "$input": "also_not_a_ref"})
541                );
542            }
543            _ => panic!("Expected EscapedLiteral, got {:?}", parsed),
544        }
545
546        // Verify round-trip
547        let serialized = serde_json::to_string(&parsed).unwrap();
548        let reparsed: ValueExpr = serde_json::from_str(&serialized).unwrap();
549        assert_eq!(parsed, reparsed);
550    }
551
552    #[test]
553    fn test_escaped_literal_with_nested_literal() {
554        // $literal containing another $literal structure (but as raw data)
555        let json_str = r#"{"$literal": {"$literal": "inner"}}"#;
556        let parsed: ValueExpr = serde_json::from_str(json_str).unwrap();
557
558        match &parsed {
559            ValueExpr::EscapedLiteral { literal } => {
560                // The outer $literal escapes everything, so inner $literal is just data
561                assert_eq!(*literal, json!({"$literal": "inner"}));
562            }
563            _ => panic!("Expected EscapedLiteral, got {:?}", parsed),
564        }
565    }
566
567    #[test]
568    fn test_literal_containing_dollar_prefixed_fields() {
569        // Regular object (not $literal) with fields that happen to start with $
570        // but aren't our special keys
571        let json_str = r#"{"$custom": "value", "$other": 123, "normal": true}"#;
572        let parsed: ValueExpr = serde_json::from_str(json_str).unwrap();
573
574        match parsed {
575            ValueExpr::Object(fields) => {
576                assert_eq!(fields.len(), 3);
577                // These should be treated as regular fields since they're not $step, $input, $variable, or $literal
578                assert!(
579                    fields
580                        .iter()
581                        .any(|(k, v)| k == "$custom" && *v == ValueExpr::Literal(json!("value")))
582                );
583                assert!(
584                    fields
585                        .iter()
586                        .any(|(k, v)| k == "$other" && *v == ValueExpr::Literal(json!(123)))
587                );
588                assert!(
589                    fields
590                        .iter()
591                        .any(|(k, v)| k == "normal" && *v == ValueExpr::Literal(json!(true)))
592                );
593            }
594            _ => panic!("Expected Object, got {:?}", parsed),
595        }
596    }
597
598    #[test]
599    fn test_array_of_escaped_literals() {
600        // Array containing multiple escaped literals
601        let json_str = r#"[
602            {"$literal": {"$step": "fake1"}},
603            {"$literal": {"$input": "fake2"}},
604            "normal_string"
605        ]"#;
606        let parsed: ValueExpr = serde_json::from_str(json_str).unwrap();
607
608        match parsed {
609            ValueExpr::Array(items) => {
610                assert_eq!(items.len(), 3);
611
612                // First item should be EscapedLiteral
613                match &items[0] {
614                    ValueExpr::EscapedLiteral { literal } => {
615                        assert_eq!(*literal, json!({"$step": "fake1"}));
616                    }
617                    _ => panic!("Expected EscapedLiteral at index 0"),
618                }
619
620                // Second item should be EscapedLiteral
621                match &items[1] {
622                    ValueExpr::EscapedLiteral { literal } => {
623                        assert_eq!(*literal, json!({"$input": "fake2"}));
624                    }
625                    _ => panic!("Expected EscapedLiteral at index 1"),
626                }
627
628                // Third item should be regular Literal
629                assert_eq!(items[2], ValueExpr::Literal(json!("normal_string")));
630            }
631            _ => panic!("Expected Array, got {:?}", parsed),
632        }
633    }
634
635    #[test]
636    fn test_complex_mixed_nesting() {
637        // Complex structure mixing all expression types
638        let json_str = r#"{
639            "steps_array": [
640                {"$step": "step1"},
641                {"$step": "step2", "path": "result"}
642            ],
643            "inputs": {
644                "user_input": {"$input": "name"},
645                "workflow_root": {"$input": "$"}
646            },
647            "config": {
648                "api_key": {"$variable": "api_key", "default": "default_key"},
649                "nested_config": {
650                    "escaped": {"$literal": {"$step": "not_real"}},
651                    "real_ref": {"$step": "real_step"},
652                    "values": [1, 2, {"$variable": "count"}]
653                }
654            }
655        }"#;
656
657        let parsed: ValueExpr = serde_json::from_str(json_str).unwrap();
658
659        // Verify round-trip
660        let serialized = serde_json::to_string(&parsed).unwrap();
661        let reparsed: ValueExpr = serde_json::from_str(&serialized).unwrap();
662        assert_eq!(parsed, reparsed);
663
664        // Verify top-level structure
665        match parsed {
666            ValueExpr::Object(fields) => {
667                assert_eq!(fields.len(), 3);
668                assert!(fields.iter().any(|(k, _)| k == "steps_array"));
669                assert!(fields.iter().any(|(k, _)| k == "inputs"));
670                assert!(fields.iter().any(|(k, _)| k == "config"));
671            }
672            _ => panic!("Expected Object at top level"),
673        }
674    }
675
676    #[test]
677    fn test_escaped_literal_with_complex_value() {
678        // $literal with complex nested structure
679        let complex_value = json!({
680            "nested": {
681                "arrays": [1, 2, 3],
682                "objects": {"a": "b"},
683                "fake_refs": {
684                    "$step": "not_expanded",
685                    "$input": "not_expanded",
686                    "$variable": "not_expanded"
687                }
688            }
689        });
690
691        let expr = ValueExpr::escaped_literal(complex_value.clone());
692        let json_str = serde_json::to_string(&expr).unwrap();
693        let parsed: ValueExpr = serde_json::from_str(&json_str).unwrap();
694
695        match &parsed {
696            ValueExpr::EscapedLiteral { literal } => {
697                assert_eq!(*literal, complex_value);
698            }
699            _ => panic!("Expected EscapedLiteral"),
700        }
701    }
702
703    #[test]
704    fn test_mixed_literal_and_expression_object() {
705        // Test pattern: { messages: {$step: create_messages}, model: "gpt-3.5-turbo", max_tokens: 150 }
706        // This should work WITHOUT requiring $literal on each literal value
707        let json_str = r#"{
708            "messages": {"$step": "create_messages"},
709            "model": "gpt-3.5-turbo",
710            "max_tokens": 150
711        }"#;
712
713        let parsed: ValueExpr = serde_json::from_str(json_str).unwrap();
714
715        // Verify structure
716        match &parsed {
717            ValueExpr::Object(fields) => {
718                assert_eq!(fields.len(), 3);
719
720                // "messages" should be a Step reference
721                let messages = fields.iter().find(|(k, _)| k == "messages").unwrap();
722                match &messages.1 {
723                    ValueExpr::Step { step, .. } => {
724                        assert_eq!(step, "create_messages");
725                    }
726                    _ => panic!("Expected Step for messages field, got {:?}", messages.1),
727                }
728
729                // "model" should be a Literal string
730                let model = fields.iter().find(|(k, _)| k == "model").unwrap();
731                assert_eq!(model.1, ValueExpr::Literal(json!("gpt-3.5-turbo")));
732
733                // "max_tokens" should be a Literal number
734                let max_tokens = fields.iter().find(|(k, _)| k == "max_tokens").unwrap();
735                assert_eq!(max_tokens.1, ValueExpr::Literal(json!(150)));
736            }
737            _ => panic!("Expected Object, got {:?}", parsed),
738        }
739
740        // Verify round-trip serialization
741        let serialized = serde_json::to_string(&parsed).unwrap();
742        let reparsed: ValueExpr = serde_json::from_str(&serialized).unwrap();
743        assert_eq!(parsed, reparsed);
744    }
745
746    #[test]
747    fn test_yaml_integer_preserved_in_value_expr() {
748        // Verify that YAML integers survive deserialization as integers, not floats.
749        // Regression test for #866: ensures the YAML→ValueExpr path preserves types.
750        let yaml = r#"
751            duration_ms: 10
752            name: test
753        "#;
754        let expr: ValueExpr = serde_yaml_ng::from_str(yaml).unwrap();
755        match &expr {
756            ValueExpr::Object(fields) => {
757                let duration = fields.iter().find(|(k, _)| k == "duration_ms").unwrap();
758                match &duration.1 {
759                    ValueExpr::Literal(val) => {
760                        assert!(
761                            val.is_i64() || val.is_u64(),
762                            "Expected integer, got {:?} (is_f64={})",
763                            val,
764                            val.is_f64()
765                        );
766                        assert_eq!(val.as_i64(), Some(10));
767                    }
768                    other => panic!("Expected Literal, got {:?}", other),
769                }
770            }
771            other => panic!("Expected Object, got {:?}", other),
772        }
773    }
774
775    #[test]
776    fn test_mixed_literal_and_expression_with_nested_objects() {
777        // More complex mixed pattern with nested objects
778        let json_str = r#"{
779            "config": {
780                "api_key": {"$variable": "openai_api_key"},
781                "timeout": 30,
782                "options": {
783                    "stream": true,
784                    "user_data": {"$input": "user"}
785                }
786            },
787            "messages": [
788                {"$step": "system_prompt"},
789                {"role": "user", "content": {"$input": "message"}}
790            ],
791            "temperature": 0.7
792        }"#;
793
794        let parsed: ValueExpr = serde_json::from_str(json_str).unwrap();
795
796        // Verify round-trip
797        let serialized = serde_json::to_string(&parsed).unwrap();
798        let reparsed: ValueExpr = serde_json::from_str(&serialized).unwrap();
799        assert_eq!(parsed, reparsed);
800
801        // Verify structure is correctly parsed
802        match &parsed {
803            ValueExpr::Object(fields) => {
804                assert_eq!(fields.len(), 3);
805                assert!(fields.iter().any(|(k, _)| k == "config"));
806                assert!(fields.iter().any(|(k, _)| k == "messages"));
807                assert!(fields.iter().any(|(k, _)| k == "temperature"));
808
809                // Verify temperature is a literal
810                let temp = fields.iter().find(|(k, _)| k == "temperature").unwrap();
811                assert_eq!(temp.1, ValueExpr::Literal(json!(0.7)));
812            }
813            _ => panic!("Expected Object"),
814        }
815    }
816}