1use rustc_hash::FxHashMap;
16use serde::de::{self, Deserializer, MapAccess, Visitor};
17use serde::Deserialize;
18use serde_json::Value;
19use std::fmt;
20
21use crate::error::CoreError;
22
23use super::transform::TransformExpr;
24use super::types::{BindingPath, BindingType};
25
26pub type BindingSpec = FxHashMap<String, BindingEntry>;
28
29#[derive(Debug, Clone, PartialEq)]
40pub struct BindingEntry {
41 pub path: String,
43 pub default: Option<Value>,
45 pub lazy: bool,
47}
48
49impl BindingEntry {
50 pub fn new(path: impl Into<String>) -> Self {
52 Self {
53 path: path.into(),
54 default: None,
55 lazy: false,
56 }
57 }
58
59 pub fn with_default(path: impl Into<String>, default: Value) -> Self {
61 Self {
62 path: path.into(),
63 default: Some(default),
64 lazy: false,
65 }
66 }
67
68 pub fn new_lazy(path: impl Into<String>) -> Self {
70 Self {
71 path: path.into(),
72 default: None,
73 lazy: true,
74 }
75 }
76
77 pub fn lazy_with_default(path: impl Into<String>, default: Value) -> Self {
79 Self {
80 path: path.into(),
81 default: Some(default),
82 lazy: true,
83 }
84 }
85
86 pub fn is_lazy(&self) -> bool {
88 self.lazy
89 }
90
91 pub fn task_id(&self) -> &str {
93 self.path.split('.').next().unwrap_or(&self.path)
94 }
95
96 #[inline]
114 pub fn normalize_path(path: &str) -> &str {
115 path.strip_prefix('$').unwrap_or(path)
116 }
117}
118
119pub fn parse_binding_entry(s: &str) -> Result<BindingEntry, CoreError> {
126 let s = s.trim();
127
128 if s.is_empty() {
129 return Err(CoreError::InvalidPath {
130 path: String::new(),
131 });
132 }
133
134 match find_operator_outside_quotes(s, "??") {
135 Some(idx) => {
136 let path = s[..idx].trim();
137
138 if path.is_empty() {
139 return Err(CoreError::InvalidPath {
140 path: s.to_string(),
141 });
142 }
143
144 let default_str = s[idx + 2..].trim();
145 let default =
146 serde_json::from_str(default_str).map_err(|e| CoreError::InvalidDefault {
147 raw: default_str.to_string(),
148 reason: e.to_string(),
149 })?;
150
151 Ok(BindingEntry {
152 path: path.to_string(),
153 default: Some(default),
154 lazy: false,
155 })
156 }
157 None => Ok(BindingEntry {
158 path: s.to_string(),
159 default: None,
160 lazy: false,
161 }),
162 }
163}
164
165fn find_operator_outside_quotes(s: &str, op: &str) -> Option<usize> {
170 let mut in_quotes = false;
171 let mut escape_next = false;
172 let mut byte_pos = 0;
173
174 for ch in s.chars() {
175 if escape_next {
176 escape_next = false;
177 } else if ch == '\\' {
178 escape_next = true;
179 } else if ch == '"' {
180 in_quotes = !in_quotes;
181 } else if !in_quotes && s[byte_pos..].starts_with(op) {
182 return Some(byte_pos);
183 }
184
185 byte_pos += ch.len_utf8();
186 }
187
188 None
189}
190
191impl<'de> Deserialize<'de> for BindingEntry {
197 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
198 where
199 D: Deserializer<'de>,
200 {
201 deserializer.deserialize_any(BindingEntryVisitor)
202 }
203}
204
205struct BindingEntryVisitor;
206
207impl<'de> Visitor<'de> for BindingEntryVisitor {
208 type Value = BindingEntry;
209
210 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
211 formatter
212 .write_str("a string 'task.path [?? default]' or an object {path, lazy?, default?}")
213 }
214
215 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
218 where
219 E: de::Error,
220 {
221 let mut entry = parse_binding_entry(value).map_err(|e| de::Error::custom(e.to_string()))?;
222 entry.path = BindingEntry::normalize_path(&entry.path).to_string();
223 Ok(entry)
224 }
225
226 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
228 where
229 M: MapAccess<'de>,
230 {
231 let mut path: Option<String> = None;
232 let mut lazy: Option<bool> = None;
233 let mut default: Option<Value> = None;
234
235 while let Some(key) = map.next_key::<String>()? {
236 match key.as_str() {
237 "path" => {
238 if path.is_some() {
239 return Err(de::Error::duplicate_field("path"));
240 }
241 path = Some(map.next_value()?);
242 }
243 "lazy" => {
244 if lazy.is_some() {
245 return Err(de::Error::duplicate_field("lazy"));
246 }
247 lazy = Some(map.next_value()?);
248 }
249 "default" => {
250 if default.is_some() {
251 return Err(de::Error::duplicate_field("default"));
252 }
253 default = Some(map.next_value()?);
254 }
255 _ => {
256 let _ = map.next_value::<de::IgnoredAny>()?;
258 }
259 }
260 }
261
262 let path = path.ok_or_else(|| de::Error::missing_field("path"))?;
263 let path = BindingEntry::normalize_path(&path).to_string();
264
265 Ok(BindingEntry {
266 path,
267 default,
268 lazy: lazy.unwrap_or(false),
269 })
270 }
271}
272
273#[derive(Debug, Clone)]
300pub struct WithEntry {
301 pub source: BindingPath,
303 pub binding_type: BindingType,
305 pub default: Option<Value>,
307 pub lazy: bool,
309 pub transform: Option<TransformExpr>,
311}
312
313pub type WithSpec = FxHashMap<String, WithEntry>;
315
316#[derive(Debug, Clone, PartialEq)]
318pub struct WithEntryParseError {
319 pub input: String,
320 pub reason: String,
321}
322
323impl fmt::Display for WithEntryParseError {
324 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325 write!(
326 f,
327 "[NIKA-155] WithEntry parse error in '{}': {}",
328 self.input, self.reason
329 )
330 }
331}
332
333impl std::error::Error for WithEntryParseError {}
334
335impl WithEntry {
336 pub fn simple(source: BindingPath) -> Self {
338 Self {
339 source,
340 binding_type: BindingType::default(),
341 default: None,
342 lazy: false,
343 transform: None,
344 }
345 }
346
347 pub fn with_default(source: BindingPath, default: Value) -> Self {
349 Self {
350 source,
351 binding_type: BindingType::default(),
352 default: Some(default),
353 lazy: false,
354 transform: None,
355 }
356 }
357
358 pub fn task_id(&self) -> Option<&str> {
362 use super::types::BindingSource;
363 match &self.source.source {
364 BindingSource::Task(id) => Some(id),
365 _ => None,
366 }
367 }
368
369 pub fn is_lazy(&self) -> bool {
371 self.lazy
372 }
373}
374
375pub fn parse_with_entry(input: &str) -> Result<WithEntry, WithEntryParseError> {
395 let input_trimmed = input.trim();
396
397 if input_trimmed.is_empty() {
398 return Err(WithEntryParseError {
399 input: input.to_string(),
400 reason: "empty input".to_string(),
401 });
402 }
403
404 let (path_and_transforms, default_value) = split_default(input_trimmed)?;
406
407 if path_and_transforms.is_empty() {
408 return Err(WithEntryParseError {
409 input: input.to_string(),
410 reason: "empty path before '??'".to_string(),
411 });
412 }
413
414 let (path_str, transform_str) = split_transforms(path_and_transforms);
416
417 let path_str = path_str.trim();
418 if path_str.is_empty() {
419 return Err(WithEntryParseError {
420 input: input.to_string(),
421 reason: "empty path".to_string(),
422 });
423 }
424
425 let source = BindingPath::parse(path_str).map_err(|e| WithEntryParseError {
427 input: input.to_string(),
428 reason: e.reason,
429 })?;
430
431 let transform = if let Some(t_str) = transform_str {
433 let t_str = t_str.trim();
434 if t_str.is_empty() {
435 return Err(WithEntryParseError {
436 input: input.to_string(),
437 reason: "empty transform after '|'".to_string(),
438 });
439 }
440 Some(
441 TransformExpr::parse(t_str).map_err(|e| WithEntryParseError {
442 input: input.to_string(),
443 reason: e.reason,
444 })?,
445 )
446 } else {
447 None
448 };
449
450 let default = match default_value {
452 Some(d_str) => {
453 let d_str = d_str.trim();
454 if d_str.is_empty() {
455 return Err(WithEntryParseError {
456 input: input.to_string(),
457 reason: "empty default value after '??'".to_string(),
458 });
459 }
460 let val: Value = serde_json::from_str(d_str).map_err(|e| WithEntryParseError {
461 input: input.to_string(),
462 reason: format!("invalid default JSON: {e}"),
463 })?;
464 Some(val)
465 }
466 None => None,
467 };
468
469 Ok(WithEntry {
470 source,
471 binding_type: BindingType::default(),
472 default,
473 lazy: false,
474 transform,
475 })
476}
477
478fn split_default(s: &str) -> Result<(&str, Option<&str>), WithEntryParseError> {
483 let mut in_quotes = false;
487 let mut escape_next = false;
488 let mut paren_depth: u32 = 0;
489 let mut last_default_pos: Option<usize> = None;
490 let bytes = s.as_bytes();
491
492 let mut i = 0;
493 while i < bytes.len() {
494 if escape_next {
495 escape_next = false;
496 i += 1;
497 continue;
498 }
499
500 match bytes[i] {
501 b'\\' => {
502 escape_next = true;
503 }
504 b'"' => {
505 in_quotes = !in_quotes;
506 }
507 b'(' if !in_quotes => {
508 paren_depth = paren_depth.saturating_add(1);
509 }
510 b')' if !in_quotes => {
511 paren_depth = paren_depth.saturating_sub(1);
512 }
513 b'?' if !in_quotes && paren_depth == 0 => {
514 if i + 1 < bytes.len() && bytes[i + 1] == b'?' {
516 last_default_pos = Some(i);
517 i += 2; continue;
519 }
520 }
521 _ => {}
522 }
523 i += 1;
524 }
525
526 match last_default_pos {
527 Some(pos) => {
528 let path_part = &s[..pos].trim_end();
529 let default_part = &s[pos + 2..].trim_start();
530 Ok((path_part, Some(default_part)))
531 }
532 None => Ok((s, None)),
533 }
534}
535
536fn split_transforms(s: &str) -> (&str, Option<&str>) {
540 let mut in_quotes = false;
541 let mut escape_next = false;
542 let mut paren_depth: u32 = 0;
543 let bytes = s.as_bytes();
544
545 for (i, &b) in bytes.iter().enumerate() {
546 if escape_next {
547 escape_next = false;
548 continue;
549 }
550
551 match b {
552 b'\\' => escape_next = true,
553 b'"' => in_quotes = !in_quotes,
554 b'(' if !in_quotes => paren_depth = paren_depth.saturating_add(1),
555 b')' if !in_quotes => paren_depth = paren_depth.saturating_sub(1),
556 b'|' if !in_quotes && paren_depth == 0 => {
557 return (&s[..i], Some(&s[i + 1..]));
558 }
559 _ => {}
560 }
561 }
562
563 (s, None)
564}
565
566impl<'de> Deserialize<'de> for WithEntry {
572 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
573 where
574 D: Deserializer<'de>,
575 {
576 deserializer.deserialize_any(WithEntryVisitor)
577 }
578}
579
580struct WithEntryVisitor;
581
582impl<'de> Visitor<'de> for WithEntryVisitor {
583 type Value = WithEntry;
584
585 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
586 formatter.write_str(
587 "a string '$path | transform ?? default' or an object \
588 { from, type?, transform?, default?, lazy? }",
589 )
590 }
591
592 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
594 where
595 E: de::Error,
596 {
597 parse_with_entry(value).map_err(|e| de::Error::custom(e.to_string()))
598 }
599
600 fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
602 where
603 M: MapAccess<'de>,
604 {
605 #[derive(Deserialize)]
607 struct WithEntryObject {
608 from: String,
609 #[serde(rename = "type", default)]
610 binding_type: BindingType,
611 #[serde(default)]
612 transform: Option<String>,
613 #[serde(default)]
614 default: Option<Value>,
615 #[serde(default)]
616 lazy: bool,
617 }
618
619 let obj = WithEntryObject::deserialize(de::value::MapAccessDeserializer::new(map))?;
620
621 let source = BindingPath::parse(&obj.from)
623 .map_err(|e| de::Error::custom(format!("[NIKA-155] invalid 'from' path: {e}")))?;
624
625 let transform = match obj.transform {
627 Some(ref t_str) if !t_str.trim().is_empty() => Some(
628 TransformExpr::parse(t_str.trim())
629 .map_err(|e| de::Error::custom(format!("[NIKA-155] invalid transform: {e}")))?,
630 ),
631 _ => None,
632 };
633
634 Ok(WithEntry {
635 source,
636 binding_type: obj.binding_type,
637 default: obj.default,
638 lazy: obj.lazy,
639 transform,
640 })
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use super::super::transform::TransformOp;
647 use super::super::types::BindingSource;
648 use super::*;
649 use serde_json::json;
650 use serde_saphyr as serde_yaml;
651
652 #[test]
657 fn parse_simple_path() {
658 let entry = parse_binding_entry("weather.summary").unwrap();
659 assert_eq!(entry.path, "weather.summary");
660 assert_eq!(entry.default, None);
661 }
662
663 #[test]
664 fn parse_simple_task_only() {
665 let entry = parse_binding_entry("weather").unwrap();
666 assert_eq!(entry.path, "weather");
667 assert_eq!(entry.default, None);
668 }
669
670 #[test]
671 fn parse_nested_path() {
672 let entry = parse_binding_entry("weather.data.temperature.celsius").unwrap();
673 assert_eq!(entry.path, "weather.data.temperature.celsius");
674 assert_eq!(entry.default, None);
675 }
676
677 #[test]
678 fn parse_with_default_number() {
679 let entry = parse_binding_entry("x.y ?? 0").unwrap();
680 assert_eq!(entry.path, "x.y");
681 assert_eq!(entry.default, Some(json!(0)));
682 }
683
684 #[test]
685 fn parse_with_default_negative_number() {
686 let entry = parse_binding_entry("score ?? -1").unwrap();
687 assert_eq!(entry.path, "score");
688 assert_eq!(entry.default, Some(json!(-1)));
689 }
690
691 #[test]
692 fn parse_with_default_float() {
693 let entry = parse_binding_entry("rate ?? 0.5").unwrap();
694 assert_eq!(entry.path, "rate");
695 assert_eq!(entry.default, Some(json!(0.5)));
696 }
697
698 #[test]
699 fn parse_with_default_string() {
700 let entry = parse_binding_entry(r#"x.y ?? "Anon""#).unwrap();
701 assert_eq!(entry.path, "x.y");
702 assert_eq!(entry.default, Some(json!("Anon")));
703 }
704
705 #[test]
706 fn parse_with_default_empty_string() {
707 let entry = parse_binding_entry(r#"name ?? """#).unwrap();
708 assert_eq!(entry.path, "name");
709 assert_eq!(entry.default, Some(json!("")));
710 }
711
712 #[test]
713 fn parse_with_default_bool_true() {
714 let entry = parse_binding_entry("enabled ?? true").unwrap();
715 assert_eq!(entry.path, "enabled");
716 assert_eq!(entry.default, Some(json!(true)));
717 }
718
719 #[test]
720 fn parse_with_default_bool_false() {
721 let entry = parse_binding_entry("enabled ?? false").unwrap();
722 assert_eq!(entry.path, "enabled");
723 assert_eq!(entry.default, Some(json!(false)));
724 }
725
726 #[test]
727 fn parse_with_default_null() {
728 let entry = parse_binding_entry("value ?? null").unwrap();
729 assert_eq!(entry.path, "value");
730 assert_eq!(entry.default, Some(json!(null)));
731 }
732
733 #[test]
734 fn parse_with_default_object() {
735 let entry = parse_binding_entry(r#"x ?? {"a": 1, "b": 2}"#).unwrap();
736 assert_eq!(entry.path, "x");
737 assert_eq!(entry.default, Some(json!({"a": 1, "b": 2})));
738 }
739
740 #[test]
741 fn parse_with_default_array() {
742 let entry = parse_binding_entry(r#"tags ?? ["untagged"]"#).unwrap();
743 assert_eq!(entry.path, "tags");
744 assert_eq!(entry.default, Some(json!(["untagged"])));
745 }
746
747 #[test]
748 fn parse_with_default_nested_object() {
749 let entry = parse_binding_entry(r#"cfg ?? {"debug": false, "nested": {"a": 1}}"#).unwrap();
750 assert_eq!(entry.path, "cfg");
751 assert_eq!(
752 entry.default,
753 Some(json!({"debug": false, "nested": {"a": 1}}))
754 );
755 }
756
757 #[test]
758 fn parse_quotes_in_default() {
759 let entry = parse_binding_entry(r#"x ?? "What?? Really??""#).unwrap();
761 assert_eq!(entry.path, "x");
762 assert_eq!(entry.default, Some(json!("What?? Really??")));
763 }
764
765 #[test]
766 fn parse_escaped_quotes_in_default() {
767 let entry = parse_binding_entry(r#"x ?? "He said \"hello\"""#).unwrap();
768 assert_eq!(entry.path, "x");
769 assert_eq!(entry.default, Some(json!("He said \"hello\"")));
770 }
771
772 #[test]
773 fn parse_with_whitespace() {
774 let entry = parse_binding_entry(" weather.summary ").unwrap();
775 assert_eq!(entry.path, "weather.summary");
776 }
777
778 #[test]
779 fn parse_with_whitespace_around_operator() {
780 let entry = parse_binding_entry("x ?? 0").unwrap();
781 assert_eq!(entry.path, "x");
782 assert_eq!(entry.default, Some(json!(0)));
783 }
784
785 #[test]
790 fn parse_reject_unquoted_string() {
791 let result = parse_binding_entry("x ?? Anonymous");
793 assert!(result.is_err());
794 let err = result.unwrap_err();
795 assert!(err.to_string().contains("NIKA-056"));
796 }
797
798 #[test]
799 fn parse_reject_empty_path() {
800 let result = parse_binding_entry("");
801 assert!(result.is_err());
802 }
803
804 #[test]
805 fn parse_reject_only_operator() {
806 let result = parse_binding_entry("??");
807 assert!(result.is_err());
808 }
809
810 #[test]
811 fn parse_reject_empty_path_with_default() {
812 let result = parse_binding_entry("?? 0");
813 assert!(result.is_err());
814 }
815
816 #[test]
817 fn parse_reject_invalid_json_default() {
818 let result = parse_binding_entry(r#"x ?? {"a": 1"#);
820 assert!(result.is_err());
821 }
822
823 #[test]
828 fn task_id_simple() {
829 let entry = BindingEntry::new("weather");
830 assert_eq!(entry.task_id(), "weather");
831 }
832
833 #[test]
834 fn task_id_with_path() {
835 let entry = BindingEntry::new("weather.summary");
836 assert_eq!(entry.task_id(), "weather");
837 }
838
839 #[test]
840 fn task_id_with_nested_path() {
841 let entry = BindingEntry::new("weather.data.temp.celsius");
842 assert_eq!(entry.task_id(), "weather");
843 }
844
845 #[test]
850 fn yaml_parse_simple() {
851 let yaml = "forecast: weather.summary";
852 let spec: BindingSpec = serde_yaml::from_str(yaml).unwrap();
853 let entry = spec.get("forecast").unwrap();
854 assert_eq!(entry.path, "weather.summary");
855 assert_eq!(entry.default, None);
856 }
857
858 #[test]
859 fn yaml_parse_with_default() {
860 let yaml = r#"temp: weather.temp ?? 20"#;
861 let spec: BindingSpec = serde_yaml::from_str(yaml).unwrap();
862 let entry = spec.get("temp").unwrap();
863 assert_eq!(entry.path, "weather.temp");
864 assert_eq!(entry.default, Some(json!(20)));
865 }
866
867 #[test]
868 fn yaml_parse_multiple_entries() {
869 let yaml = r#"
870forecast: weather.summary
871temp: weather.temp ?? 20
872name: user.name ?? "Anonymous"
873"#;
874 let spec: BindingSpec = serde_yaml::from_str(yaml).unwrap();
875
876 let forecast = spec.get("forecast").unwrap();
877 assert_eq!(forecast.path, "weather.summary");
878 assert_eq!(forecast.default, None);
879
880 let temp = spec.get("temp").unwrap();
881 assert_eq!(temp.path, "weather.temp");
882 assert_eq!(temp.default, Some(json!(20)));
883
884 let name = spec.get("name").unwrap();
885 assert_eq!(name.path, "user.name");
886 assert_eq!(name.default, Some(json!("Anonymous")));
887 }
888
889 #[test]
890 fn yaml_parse_complex_defaults() {
891 let yaml = r#"
894cfg: 'settings ?? {"debug": false}'
895tags: 'meta.tags ?? ["default"]'
896"#;
897 let spec: BindingSpec = serde_yaml::from_str(yaml).unwrap();
898
899 let cfg = spec.get("cfg").unwrap();
900 assert_eq!(cfg.default, Some(json!({"debug": false})));
901
902 let tags = spec.get("tags").unwrap();
903 assert_eq!(tags.default, Some(json!(["default"])));
904 }
905
906 #[test]
911 fn find_op_simple() {
912 assert_eq!(find_operator_outside_quotes("a ?? b", "??"), Some(2));
913 }
914
915 #[test]
916 fn find_op_no_match() {
917 assert_eq!(find_operator_outside_quotes("a.b.c", "??"), None);
918 }
919
920 #[test]
921 fn find_op_inside_quotes_ignored() {
922 let s = r#"x ?? "What?? Really??""#;
924 assert_eq!(find_operator_outside_quotes(s, "??"), Some(2));
925 }
926
927 #[test]
928 fn find_op_only_inside_quotes() {
929 let s = r#""a ?? b""#;
930 assert_eq!(find_operator_outside_quotes(s, "??"), None);
931 }
932
933 #[test]
934 fn find_op_multiple_operators() {
935 let s = "a ?? b ?? c";
937 assert_eq!(find_operator_outside_quotes(s, "??"), Some(2));
938 }
939
940 #[test]
941 fn find_op_with_escaped_quote() {
942 let s = r#"x ?? "He said \"??\"""#;
943 assert_eq!(find_operator_outside_quotes(s, "??"), Some(2));
944 }
945
946 #[test]
951 fn test_normalize_path_strips_dollar_prefix() {
952 assert_eq!(BindingEntry::normalize_path("$task1"), "task1");
953 assert_eq!(BindingEntry::normalize_path("task1"), "task1");
954 assert_eq!(BindingEntry::normalize_path("$my_task"), "my_task");
955 assert_eq!(BindingEntry::normalize_path("task.field"), "task.field");
956 assert_eq!(BindingEntry::normalize_path("$task.field"), "task.field");
957 }
958
959 #[test]
964 fn test_binding_entry_deserialize_normalizes_dollar_prefix_shorthand() {
965 let entry: BindingEntry = serde_yaml::from_str("\"$task1\"").unwrap();
967 assert_eq!(entry.path, "task1");
968
969 let entry: BindingEntry = serde_yaml::from_str("\"task1\"").unwrap();
971 assert_eq!(entry.path, "task1");
972 }
973
974 #[test]
975 fn test_binding_entry_deserialize_normalizes_dollar_prefix_full_form() {
976 let entry: BindingEntry = serde_yaml::from_str(
978 r#"
979 path: "$my_task"
980 default: "fallback"
981 "#,
982 )
983 .unwrap();
984 assert_eq!(entry.path, "my_task");
985 assert_eq!(
986 entry.default.as_ref().map(|v| v.as_str()),
987 Some(Some("fallback"))
988 );
989
990 let entry: BindingEntry = serde_yaml::from_str(
992 r#"
993 path: "my_task"
994 lazy: true
995 "#,
996 )
997 .unwrap();
998 assert_eq!(entry.path, "my_task");
999 assert!(entry.lazy);
1000 }
1001
1002 #[test]
1007 fn test_normalize_path_edge_cases() {
1008 assert_eq!(BindingEntry::normalize_path("$$task"), "$task");
1010 assert_eq!(BindingEntry::normalize_path("$$$task"), "$$task");
1011
1012 assert_eq!(BindingEntry::normalize_path("ta$sk"), "ta$sk");
1014 assert_eq!(BindingEntry::normalize_path("task$"), "task$");
1015 assert_eq!(BindingEntry::normalize_path("ta$sk$"), "ta$sk$");
1016
1017 assert_eq!(
1019 BindingEntry::normalize_path("$task.field.subfield"),
1020 "task.field.subfield"
1021 );
1022 assert_eq!(
1023 BindingEntry::normalize_path("$task.nested.deep.path"),
1024 "task.nested.deep.path"
1025 );
1026
1027 assert_eq!(BindingEntry::normalize_path(""), "");
1029 assert_eq!(BindingEntry::normalize_path("$"), "");
1030 assert_eq!(BindingEntry::normalize_path("$$"), "$");
1031
1032 assert_eq!(BindingEntry::normalize_path("$."), ".");
1034 assert_eq!(BindingEntry::normalize_path("$.."), "..");
1035 assert_eq!(BindingEntry::normalize_path(".task"), ".task");
1036 assert_eq!(BindingEntry::normalize_path("$.task"), ".task");
1037
1038 assert_eq!(BindingEntry::normalize_path("$résultat"), "résultat");
1040 assert_eq!(BindingEntry::normalize_path("$задача"), "задача");
1041
1042 assert_eq!(BindingEntry::normalize_path("$ task"), " task");
1044 assert_eq!(BindingEntry::normalize_path("$task "), "task ");
1045 }
1046
1047 #[test]
1048 fn test_binding_entry_deserialize_nested_field_access() {
1049 let entry: BindingEntry = serde_yaml::from_str("\"$research.summary.title\"").unwrap();
1051 assert_eq!(entry.path, "research.summary.title");
1052
1053 let entry: BindingEntry = serde_yaml::from_str(
1055 r#"
1056 path: "$agent_result.response.data.items"
1057 lazy: true
1058 "#,
1059 )
1060 .unwrap();
1061 assert_eq!(entry.path, "agent_result.response.data.items");
1062 assert!(entry.lazy);
1063 }
1064
1065 #[test]
1066 fn test_binding_entry_deserialize_multiple_dollar_signs() {
1067 let entry: BindingEntry = serde_yaml::from_str("\"$$task\"").unwrap();
1069 assert_eq!(entry.path, "$task");
1070
1071 let entry: BindingEntry = serde_yaml::from_str("\"$$$triple\"").unwrap();
1072 assert_eq!(entry.path, "$$triple");
1073
1074 let entry: BindingEntry = serde_yaml::from_str(
1076 r#"
1077 path: "$$escaped_var"
1078 "#,
1079 )
1080 .unwrap();
1081 assert_eq!(entry.path, "$escaped_var");
1082 }
1083
1084 #[test]
1085 fn test_binding_entry_deserialize_dollar_in_middle() {
1086 let entry: BindingEntry = serde_yaml::from_str("\"task$name\"").unwrap();
1088 assert_eq!(entry.path, "task$name");
1089
1090 let entry: BindingEntry = serde_yaml::from_str("\"$task$name\"").unwrap();
1091 assert_eq!(entry.path, "task$name");
1092
1093 let entry: BindingEntry = serde_yaml::from_str(
1095 r#"
1096 path: "result$2"
1097 "#,
1098 )
1099 .unwrap();
1100 assert_eq!(entry.path, "result$2");
1101 }
1102
1103 #[test]
1104 fn test_binding_entry_deserialize_special_characters() {
1105 let entry: BindingEntry = serde_yaml::from_str("\"$task_123\"").unwrap();
1107 assert_eq!(entry.path, "task_123");
1108
1109 let entry: BindingEntry = serde_yaml::from_str("\"$_private_task\"").unwrap();
1110 assert_eq!(entry.path, "_private_task");
1111
1112 let entry: BindingEntry = serde_yaml::from_str("\"$task-name\"").unwrap();
1114 assert_eq!(entry.path, "task-name");
1115
1116 let entry: BindingEntry = serde_yaml::from_str("\"$task_1-result.field_2\"").unwrap();
1118 assert_eq!(entry.path, "task_1-result.field_2");
1119 }
1120
1121 #[test]
1122 fn test_binding_entry_deserialize_with_all_options() {
1123 let entry: BindingEntry = serde_yaml::from_str(
1125 r#"
1126 path: "$complex_task.nested.value"
1127 default: "default_value"
1128 lazy: true
1129 "#,
1130 )
1131 .unwrap();
1132 assert_eq!(entry.path, "complex_task.nested.value");
1133 assert_eq!(
1134 entry.default.as_ref().map(|v| v.as_str()),
1135 Some(Some("default_value"))
1136 );
1137 assert!(entry.lazy);
1138 }
1139
1140 #[test]
1141 fn test_binding_entry_equivalence_with_and_without_dollar() {
1142 let with_dollar: BindingEntry = serde_yaml::from_str("\"$my_task\"").unwrap();
1144 let without_dollar: BindingEntry = serde_yaml::from_str("\"my_task\"").unwrap();
1145
1146 assert_eq!(with_dollar.path, without_dollar.path);
1147 assert_eq!(with_dollar.default, without_dollar.default);
1148 assert_eq!(with_dollar.lazy, without_dollar.lazy);
1149 }
1150
1151 #[test]
1152 fn test_binding_entry_deserialize_real_workflow_patterns() {
1153 let entry: BindingEntry = serde_yaml::from_str("\"$get_context\"").unwrap();
1155 assert_eq!(entry.path, "get_context");
1156
1157 let entry: BindingEntry = serde_yaml::from_str("\"$generate.content\"").unwrap();
1159 assert_eq!(entry.path, "generate.content");
1160
1161 let entry: BindingEntry =
1163 serde_yaml::from_str("\"$research_agent.findings.summary\"").unwrap();
1164 assert_eq!(entry.path, "research_agent.findings.summary");
1165
1166 let entry: BindingEntry = serde_yaml::from_str(
1168 r#"
1169 path: "$optional_step.result"
1170 default: null
1171 lazy: true
1172 "#,
1173 )
1174 .unwrap();
1175 assert_eq!(entry.path, "optional_step.result");
1176 assert_eq!(entry.default, Some(serde_json::Value::Null));
1177 assert!(entry.lazy);
1178 }
1179
1180 #[test]
1185 fn with_parse_simple() {
1186 let entry = parse_with_entry("$step1").unwrap();
1187 assert_eq!(entry.source, BindingPath::parse("$step1").unwrap());
1188 assert_eq!(entry.default, None);
1189 assert_eq!(entry.transform, None);
1190 assert!(!entry.lazy);
1191 assert_eq!(entry.binding_type, BindingType::Any);
1192 }
1193
1194 #[test]
1195 fn with_parse_field_access() {
1196 let entry = parse_with_entry("$step1.output").unwrap();
1197 assert_eq!(entry.source, BindingPath::parse("$step1.output").unwrap());
1198 assert_eq!(entry.default, None);
1199 assert_eq!(entry.transform, None);
1200 }
1201
1202 #[test]
1203 fn with_parse_deep_path() {
1204 let entry = parse_with_entry("$step1.data.items[0].name").unwrap();
1205 assert_eq!(
1206 entry.source,
1207 BindingPath::parse("$step1.data.items[0].name").unwrap()
1208 );
1209 }
1210
1211 #[test]
1212 fn with_parse_default_string() {
1213 let entry = parse_with_entry(r#"$step1 ?? "fallback""#).unwrap();
1214 assert_eq!(entry.source, BindingPath::parse("$step1").unwrap());
1215 assert_eq!(entry.default, Some(json!("fallback")));
1216 assert_eq!(entry.transform, None);
1217 }
1218
1219 #[test]
1220 fn with_parse_default_number() {
1221 let entry = parse_with_entry("$step1 ?? 42").unwrap();
1222 assert_eq!(entry.default, Some(json!(42)));
1223 }
1224
1225 #[test]
1226 fn with_parse_default_float() {
1227 let entry = parse_with_entry("$step1.score ?? 0.5").unwrap();
1228 assert_eq!(entry.default, Some(json!(0.5)));
1229 }
1230
1231 #[test]
1232 fn with_parse_default_bool() {
1233 let entry = parse_with_entry("$step1.enabled ?? true").unwrap();
1234 assert_eq!(entry.default, Some(json!(true)));
1235 }
1236
1237 #[test]
1238 fn with_parse_default_null() {
1239 let entry = parse_with_entry("$step1.val ?? null").unwrap();
1240 assert_eq!(entry.default, Some(json!(null)));
1241 }
1242
1243 #[test]
1244 fn with_parse_default_array() {
1245 let entry = parse_with_entry("$step1 ?? []").unwrap();
1246 assert_eq!(entry.default, Some(json!([])));
1247 }
1248
1249 #[test]
1250 fn with_parse_default_object() {
1251 let entry = parse_with_entry(r#"$step1 ?? {"key": "val"}"#).unwrap();
1252 assert_eq!(entry.default, Some(json!({"key": "val"})));
1253 }
1254
1255 #[test]
1256 fn with_parse_transform_single() {
1257 let entry = parse_with_entry("$step1 | upper").unwrap();
1258 assert_eq!(entry.source, BindingPath::parse("$step1").unwrap());
1259 assert!(entry.transform.is_some());
1260 let t = entry.transform.unwrap();
1261 assert_eq!(t.ops.len(), 1);
1262 assert_eq!(t.ops[0], TransformOp::Upper);
1263 }
1264
1265 #[test]
1266 fn with_parse_transform_chain() {
1267 let entry = parse_with_entry("$step1.items | sort | unique").unwrap();
1268 let t = entry.transform.unwrap();
1269 assert_eq!(t.ops.len(), 2);
1270 assert_eq!(t.ops[0], TransformOp::Sort);
1271 assert_eq!(t.ops[1], TransformOp::Unique);
1272 }
1273
1274 #[test]
1275 fn with_parse_transform_with_args() {
1276 let entry = parse_with_entry("$step1.items | first(3)").unwrap();
1277 let t = entry.transform.unwrap();
1278 assert_eq!(t.ops.len(), 1);
1279 assert_eq!(t.ops[0], TransformOp::FirstN(3));
1280 }
1281
1282 #[test]
1283 fn with_parse_transform_and_default() {
1284 let entry = parse_with_entry("$step1.items | length ?? 0").unwrap();
1285 assert!(entry.transform.is_some());
1286 let t = entry.transform.unwrap();
1287 assert_eq!(t.ops.len(), 1);
1288 assert_eq!(t.ops[0], TransformOp::Length);
1289 assert_eq!(entry.default, Some(json!(0)));
1290 }
1291
1292 #[test]
1293 fn with_parse_full_chain_and_default() {
1294 let entry = parse_with_entry(r#"$step1.items | sort | first(3) ?? []"#).unwrap();
1295 let t = entry.transform.unwrap();
1296 assert_eq!(t.ops.len(), 2);
1297 assert_eq!(t.ops[0], TransformOp::Sort);
1298 assert_eq!(t.ops[1], TransformOp::FirstN(3));
1299 assert_eq!(entry.default, Some(json!([])));
1300 }
1301
1302 #[test]
1303 fn with_parse_context_ref() {
1304 let entry = parse_with_entry("$context.files.brand").unwrap();
1305 match &entry.source.source {
1306 BindingSource::Context(path) => assert_eq!(path.as_ref(), "files.brand"),
1307 _ => panic!("expected Context source"),
1308 }
1309 }
1310
1311 #[test]
1312 fn with_parse_input_ref() {
1313 let entry = parse_with_entry("$inputs.locale").unwrap();
1314 match &entry.source.source {
1315 BindingSource::Input(path) => assert_eq!(path.as_ref(), "locale"),
1316 _ => panic!("expected Input source"),
1317 }
1318 }
1319
1320 #[test]
1321 fn with_parse_env_ref() {
1322 let entry = parse_with_entry("$env.API_URL").unwrap();
1323 match &entry.source.source {
1324 BindingSource::Env(path) => assert_eq!(path.as_ref(), "API_URL"),
1325 _ => panic!("expected Env source"),
1326 }
1327 }
1328
1329 #[test]
1330 fn with_parse_whitespace_tolerance() {
1331 let entry = parse_with_entry(" $step1 | upper ").unwrap();
1332 assert_eq!(entry.source, BindingPath::parse("$step1").unwrap());
1333 let t = entry.transform.unwrap();
1334 assert_eq!(t.ops[0], TransformOp::Upper);
1335 }
1336
1337 #[test]
1338 fn with_parse_whitespace_around_default() {
1339 let entry = parse_with_entry("$step1 ?? 42").unwrap();
1340 assert_eq!(entry.default, Some(json!(42)));
1341 }
1342
1343 #[test]
1348 fn with_parse_empty_string_error() {
1349 let result = parse_with_entry("");
1350 assert!(result.is_err());
1351 assert!(result.unwrap_err().reason.contains("empty"));
1352 }
1353
1354 #[test]
1355 fn with_parse_no_dollar_error() {
1356 let result = parse_with_entry("step1");
1357 assert!(result.is_err());
1358 let err = result.unwrap_err();
1359 assert!(err.reason.contains("must start with '$'"));
1360 }
1361
1362 #[test]
1363 fn with_parse_pipe_only_error() {
1364 let result = parse_with_entry("| upper");
1365 assert!(result.is_err());
1366 }
1367
1368 #[test]
1369 fn with_parse_default_only_error() {
1370 let result = parse_with_entry("?? 42");
1371 assert!(result.is_err());
1372 }
1373
1374 #[test]
1375 fn with_parse_trailing_pipe_error() {
1376 let result = parse_with_entry("$step1 |");
1377 assert!(result.is_err());
1378 assert!(result.unwrap_err().reason.contains("empty transform"));
1379 }
1380
1381 #[test]
1382 fn with_parse_invalid_json_default_error() {
1383 let result = parse_with_entry(r#"$step1 ?? {broken"#);
1384 assert!(result.is_err());
1385 assert!(result.unwrap_err().reason.contains("invalid default JSON"));
1386 }
1387
1388 #[test]
1389 fn with_parse_unquoted_string_default_error() {
1390 let result = parse_with_entry("$step1 ?? Anonymous");
1391 assert!(result.is_err());
1392 }
1393
1394 #[test]
1395 fn with_parse_empty_default_error() {
1396 let result = parse_with_entry("$step1 ??");
1398 assert!(result.is_err());
1399 assert!(result.unwrap_err().reason.contains("empty default"));
1400 }
1401
1402 #[test]
1403 fn with_parse_unknown_transform_error() {
1404 let result = parse_with_entry("$step1 | nonexistent_transform");
1405 assert!(result.is_err());
1406 }
1407
1408 #[test]
1413 fn with_parse_default_inside_parens_ignored() {
1414 let entry = parse_with_entry(r#"$step1 | default("a ?? b")"#).unwrap();
1416 assert!(entry.transform.is_some());
1417 assert_eq!(entry.default, None); }
1419
1420 #[test]
1421 fn with_parse_default_after_transform_with_inner_qq() {
1422 let entry = parse_with_entry(r#"$step1 | default("a ?? b") ?? "fallback""#).unwrap();
1424 assert!(entry.transform.is_some());
1425 assert_eq!(entry.default, Some(json!("fallback")));
1426 }
1427
1428 #[test]
1433 fn with_entry_task_id() {
1434 let entry = parse_with_entry("$step1.data.name").unwrap();
1435 assert_eq!(entry.task_id(), Some("step1"));
1436 }
1437
1438 #[test]
1439 fn with_entry_task_id_context() {
1440 let entry = parse_with_entry("$context.files.brand").unwrap();
1441 assert_eq!(entry.task_id(), None);
1442 }
1443
1444 #[test]
1445 fn with_entry_simple_constructor() {
1446 let path = BindingPath::parse("$step1.data").unwrap();
1447 let entry = WithEntry::simple(path.clone());
1448 assert_eq!(entry.source, path);
1449 assert_eq!(entry.default, None);
1450 assert!(!entry.lazy);
1451 assert_eq!(entry.transform, None);
1452 }
1453
1454 #[test]
1455 fn with_entry_with_default_constructor() {
1456 let path = BindingPath::parse("$step1").unwrap();
1457 let entry = WithEntry::with_default(path.clone(), json!(42));
1458 assert_eq!(entry.source, path);
1459 assert_eq!(entry.default, Some(json!(42)));
1460 }
1461
1462 #[test]
1467 fn with_deser_string_simple() {
1468 let entry: WithEntry = serde_yaml::from_str("\"$step1\"").unwrap();
1469 assert_eq!(entry.source, BindingPath::parse("$step1").unwrap());
1470 }
1471
1472 #[test]
1473 fn with_deser_string_with_transform() {
1474 let entry: WithEntry = serde_yaml::from_str("\"$step1.name | upper\"").unwrap();
1475 assert_eq!(entry.source, BindingPath::parse("$step1.name").unwrap());
1476 let t = entry.transform.unwrap();
1477 assert_eq!(t.ops[0], TransformOp::Upper);
1478 }
1479
1480 #[test]
1481 fn with_deser_string_with_default() {
1482 let entry: WithEntry = serde_yaml::from_str(r#""$step1 ?? 42""#).unwrap();
1483 assert_eq!(entry.default, Some(json!(42)));
1484 }
1485
1486 #[test]
1491 fn with_deser_object_minimal() {
1492 let entry: WithEntry = serde_yaml::from_str(
1493 r#"
1494 from: "$step1"
1495 "#,
1496 )
1497 .unwrap();
1498 assert_eq!(entry.source, BindingPath::parse("$step1").unwrap());
1499 assert_eq!(entry.binding_type, BindingType::Any);
1500 assert_eq!(entry.default, None);
1501 assert!(!entry.lazy);
1502 assert_eq!(entry.transform, None);
1503 }
1504
1505 #[test]
1506 fn with_deser_object_typed() {
1507 let entry: WithEntry = serde_yaml::from_str(
1508 r#"
1509 from: "$step1.name"
1510 type: string
1511 "#,
1512 )
1513 .unwrap();
1514 assert_eq!(entry.binding_type, BindingType::String);
1515 }
1516
1517 #[test]
1518 fn with_deser_object_with_transform() {
1519 let entry: WithEntry = serde_yaml::from_str(
1520 r#"
1521 from: "$step1.text"
1522 transform: "upper | trim"
1523 "#,
1524 )
1525 .unwrap();
1526 let t = entry.transform.unwrap();
1527 assert_eq!(t.ops.len(), 2);
1528 assert_eq!(t.ops[0], TransformOp::Upper);
1529 assert_eq!(t.ops[1], TransformOp::Trim);
1530 }
1531
1532 #[test]
1533 fn with_deser_object_full() {
1534 let entry: WithEntry = serde_yaml::from_str(
1535 r#"
1536 from: "$step1.abstract"
1537 type: string
1538 transform: "lower | trim"
1539 default: "No abstract"
1540 lazy: true
1541 "#,
1542 )
1543 .unwrap();
1544 assert_eq!(entry.source, BindingPath::parse("$step1.abstract").unwrap());
1545 assert_eq!(entry.binding_type, BindingType::String);
1546 assert!(entry.transform.is_some());
1547 assert_eq!(entry.default, Some(json!("No abstract")));
1548 assert!(entry.lazy);
1549 }
1550
1551 #[test]
1552 fn with_deser_object_lazy() {
1553 let entry: WithEntry = serde_yaml::from_str(
1554 r#"
1555 from: "$step1.result"
1556 lazy: true
1557 "#,
1558 )
1559 .unwrap();
1560 assert!(entry.lazy);
1561 }
1562
1563 #[test]
1568 fn with_deser_spec_empty() {
1569 let spec: WithSpec = serde_yaml::from_str("{}").unwrap();
1570 assert!(spec.is_empty());
1571 }
1572
1573 #[test]
1574 fn with_deser_spec_single() {
1575 let spec: WithSpec = serde_yaml::from_str(r#"result: "$step1""#).unwrap();
1576 assert_eq!(spec.len(), 1);
1577 let entry = spec.get("result").unwrap();
1578 assert_eq!(entry.source, BindingPath::parse("$step1").unwrap());
1579 }
1580
1581 #[test]
1582 fn with_deser_spec_mixed() {
1583 let yaml = r#"
1584result: "$step1"
1585title: "$step1.title | upper"
1586summary:
1587 from: "$step1.abstract"
1588 type: string
1589 transform: "lower | trim"
1590 default: "N/A"
1591 lazy: true
1592"#;
1593 let spec: WithSpec = serde_yaml::from_str(yaml).unwrap();
1594 assert_eq!(spec.len(), 3);
1595
1596 let result = spec.get("result").unwrap();
1598 assert_eq!(result.source, BindingPath::parse("$step1").unwrap());
1599 assert_eq!(result.transform, None);
1600
1601 let title = spec.get("title").unwrap();
1603 assert_eq!(title.source, BindingPath::parse("$step1.title").unwrap());
1604 let t = title.transform.as_ref().unwrap();
1605 assert_eq!(t.ops[0], TransformOp::Upper);
1606
1607 let summary = spec.get("summary").unwrap();
1609 assert_eq!(
1610 summary.source,
1611 BindingPath::parse("$step1.abstract").unwrap()
1612 );
1613 assert_eq!(summary.binding_type, BindingType::String);
1614 assert!(summary.lazy);
1615 assert_eq!(summary.default, Some(json!("N/A")));
1616 }
1617
1618 #[test]
1623 fn with_deser_object_missing_from_error() {
1624 let result: Result<WithEntry, _> = serde_yaml::from_str(
1625 r#"
1626 type: string
1627 "#,
1628 );
1629 assert!(result.is_err());
1630 }
1631
1632 #[test]
1633 fn with_deser_object_invalid_path_error() {
1634 let result: Result<WithEntry, _> = serde_yaml::from_str(
1635 r#"
1636 from: "step1"
1637 "#,
1638 );
1639 assert!(result.is_err());
1641 }
1642
1643 #[test]
1644 fn with_deser_object_invalid_transform_error() {
1645 let result: Result<WithEntry, _> = serde_yaml::from_str(
1646 r#"
1647 from: "$step1"
1648 transform: "nonexistent_op"
1649 "#,
1650 );
1651 assert!(result.is_err());
1652 }
1653
1654 #[test]
1659 fn split_default_no_default() {
1660 let (path, def) = split_default("$step1 | upper").unwrap();
1661 assert_eq!(path, "$step1 | upper");
1662 assert_eq!(def, None);
1663 }
1664
1665 #[test]
1666 fn split_default_simple() {
1667 let (path, def) = split_default("$step1 ?? 42").unwrap();
1668 assert_eq!(path, "$step1");
1669 assert_eq!(def, Some("42"));
1670 }
1671
1672 #[test]
1673 fn split_default_inside_parens_ignored() {
1674 let (path, def) = split_default(r#"$step1 | default("a ?? b")"#).unwrap();
1675 assert_eq!(path, r#"$step1 | default("a ?? b")"#);
1676 assert_eq!(def, None);
1677 }
1678
1679 #[test]
1680 fn split_default_after_parens() {
1681 let (path, def) = split_default(r#"$step1 | default("inner") ?? "outer""#).unwrap();
1682 assert_eq!(path, r#"$step1 | default("inner")"#);
1683 assert_eq!(def, Some(r#""outer""#));
1684 }
1685
1686 #[test]
1691 fn split_transforms_no_pipe() {
1692 let (path, t) = split_transforms("$step1");
1693 assert_eq!(path, "$step1");
1694 assert_eq!(t, None);
1695 }
1696
1697 #[test]
1698 fn split_transforms_single() {
1699 let (path, t) = split_transforms("$step1 | upper");
1700 assert_eq!(path, "$step1 ");
1701 assert_eq!(t, Some(" upper"));
1702 }
1703
1704 #[test]
1705 fn split_transforms_chain() {
1706 let (path, t) = split_transforms("$step1 | sort | unique");
1708 assert_eq!(path, "$step1 ");
1709 assert_eq!(t, Some(" sort | unique"));
1710 }
1711
1712 #[test]
1717 fn with_entry_parse_error_display() {
1718 let err = WithEntryParseError {
1719 input: "$bad".to_string(),
1720 reason: "test error".to_string(),
1721 };
1722 let msg = err.to_string();
1723 assert!(msg.contains("NIKA-155"));
1724 assert!(msg.contains("$bad"));
1725 assert!(msg.contains("test error"));
1726 }
1727
1728 #[test]
1733 fn with_simple_path() {
1734 let entry = parse_with_entry("$step1").unwrap();
1735 assert_eq!(entry.task_id(), Some("step1"));
1736 }
1737
1738 #[test]
1739 fn with_deep_path() {
1740 let entry = parse_with_entry("$step1.data.name").unwrap();
1741 assert_eq!(entry.task_id(), Some("step1"));
1742 assert_eq!(entry.source.segments.len(), 2);
1743 }
1744
1745 #[test]
1746 fn with_default_string() {
1747 let entry = parse_with_entry(r#"$step1 ?? "N/A""#).unwrap();
1748 assert_eq!(entry.default, Some(json!("N/A")));
1749 }
1750}