1use crate::error::ExpressionError;
8use crate::profile::ExprProfile;
9use crate::symbol_table::{SymbolTable, SymbolTableEntry};
10use crate::value::ExprValue;
11use serde::de::{self, Deserializer};
12use std::fmt;
13
14#[allow(clippy::large_enum_variant)]
18#[derive(Debug, Clone)]
19enum Segment {
20 Literal(String),
21 Expression {
22 start: usize,
23 end: usize,
24 parsed: crate::eval::ParsedExpression,
25 },
26}
27
28#[derive(Debug, Clone)]
29pub struct FormatString {
30 raw: String,
31 segments: Vec<Segment>,
32}
33
34pub const MAX_FORMAT_STRING_LEN: usize = 1024 * 1024;
43
44pub const MAX_FORMAT_STRING_SEGMENTS: usize = 1_000;
52
53impl FormatString {
54 pub fn new(input: &str) -> Result<Self, ExpressionError> {
67 Self::with_profile(input, &ExprProfile::latest())
68 }
69
70 pub fn with_profile(input: &str, profile: &ExprProfile) -> Result<Self, ExpressionError> {
77 if input.len() > MAX_FORMAT_STRING_LEN {
78 return Err(ExpressionError::new(format!(
79 "Format string length ({} bytes) exceeds maximum allowed size ({} bytes)",
80 input.len(),
81 MAX_FORMAT_STRING_LEN
82 )));
83 }
84 let segments = parse_segments(input, profile)?;
85 if segments.len() > MAX_FORMAT_STRING_SEGMENTS {
86 return Err(ExpressionError::new(format!(
87 "Format string contains too many interpolation segments ({}); maximum is {}",
88 segments.len(),
89 MAX_FORMAT_STRING_SEGMENTS
90 )));
91 }
92 Ok(Self {
93 raw: input.to_string(),
94 segments,
95 })
96 }
97
98 pub fn raw(&self) -> &str {
99 &self.raw
100 }
101
102 pub fn resolve_with(
119 &self,
120 symtab: &SymbolTable,
121 opts: &FormatStringOptions<'_>,
122 ) -> Result<ExprValue, ExpressionError> {
123 self.resolve_inner(symtab, opts.library, opts.path_format, opts.target_type)
124 }
125
126 pub fn resolve_string_with(
137 &self,
138 symtab: &SymbolTable,
139 opts: &FormatStringOptions<'_>,
140 ) -> Result<String, ExpressionError> {
141 let FormatStringOptions {
142 library,
143 path_format,
144 target_type: _,
145 } = *opts;
146 let mut result = String::new();
147 for seg in &self.segments {
148 match seg {
149 Segment::Literal(s) => result.push_str(s),
150 Segment::Expression { parsed, .. } => {
151 let val = self.eval_parsed(parsed, symtab, library, path_format, None)?;
152 if !matches!(val, ExprValue::Null) {
154 result.push_str(&val.to_display_string());
155 }
156 }
157 }
158 }
159 Ok(result)
160 }
161
162 fn resolve_inner(
163 &self,
164 symtab: &SymbolTable,
165 library: Option<&crate::function_library::FunctionLibrary>,
166 path_format: crate::path_mapping::PathFormat,
167 target_type: Option<&crate::types::ExprType>,
168 ) -> Result<ExprValue, ExpressionError> {
169 if self.segments.len() == 1 {
170 if let Segment::Expression { parsed, .. } = &self.segments[0] {
171 return self.eval_parsed(parsed, symtab, library, path_format, target_type);
172 }
173 }
174 self.resolve_string_with(
175 symtab,
176 &FormatStringOptions {
177 library,
178 path_format,
179 target_type: None,
180 },
181 )
182 .map(ExprValue::String)
183 }
184
185 fn eval_parsed(
186 &self,
187 parsed: &crate::eval::ParsedExpression,
188 symtab: &SymbolTable,
189 library: Option<&crate::function_library::FunctionLibrary>,
190 path_format: crate::path_mapping::PathFormat,
191 target_type: Option<&crate::types::ExprType>,
192 ) -> Result<ExprValue, ExpressionError> {
193 let mut builder = parsed.with_path_format(path_format);
194 if let Some(lib) = library {
195 builder = builder.with_library(lib);
196 }
197 if let Some(tt) = target_type {
198 builder = builder.with_target_type(tt);
199 }
200 builder.evaluate(&[symtab])
201 }
202
203 pub fn validate_expressions(
208 &self,
209 symtab: &SymbolTable,
210 lib: &crate::function_library::FunctionLibrary,
211 ) -> Result<(), FormatStringValidationError> {
212 for seg in &self.segments {
213 let (parsed, start, end) = match seg {
214 Segment::Literal(_) => continue,
215 Segment::Expression { parsed, start, end } => (parsed, *start, *end),
216 };
217 if let Err(e) = parsed.with_library(lib).evaluate(&[symtab]) {
218 return Err(FormatStringValidationError {
219 message: e.to_string(),
220 input: self.raw.clone(),
221 start,
222 end,
223 expression_error: Some(Box::new(e)),
224 });
225 }
226 }
227 Ok(())
228 }
229
230 pub fn validate_comprehension_vars(
233 &self,
234 let_names: &std::collections::HashSet<String>,
235 ) -> Result<(), ExpressionError> {
236 for seg in &self.segments {
237 if let Segment::Expression { parsed, .. } = seg {
238 check_comprehension_vars(&parsed.ast, let_names)?;
239 }
240 }
241 Ok(())
242 }
243
244 pub fn has_complex_expressions(&self) -> bool {
248 self.segments.iter().any(|s| match s {
249 Segment::Expression { parsed, .. } => parsed.as_name_lookup().is_none(),
250 Segment::Literal(_) => false,
251 })
252 }
253
254 pub fn expression_names(&self) -> Vec<&str> {
259 self.segments
260 .iter()
261 .filter_map(|s| match s {
262 Segment::Expression { parsed, .. } => parsed.as_name_lookup(),
263 Segment::Literal(_) => None,
264 })
265 .collect()
266 }
267
268 pub fn is_literal(&self) -> bool {
269 self.segments
270 .iter()
271 .all(|s| matches!(s, Segment::Literal(_)))
272 }
273
274 pub fn copy_used_symtab_values(&self, source: &SymbolTable, dest: &mut SymbolTable) {
283 for segment in &self.segments {
284 if let Segment::Expression { parsed, .. } = segment {
285 for symbol in &parsed.accessed_symbols {
286 copy_symbol_value(symbol, source, dest);
287 }
288 }
289 }
290 }
291
292 pub fn accessed_symbols(&self) -> std::collections::HashSet<String> {
294 let mut symbols = std::collections::HashSet::new();
295 for segment in &self.segments {
296 if let Segment::Expression { parsed, .. } = segment {
297 symbols.extend(parsed.accessed_symbols.iter().cloned());
298 }
299 }
300 symbols
301 }
302}
303
304pub fn copy_symbol_value(symbol: &str, source: &SymbolTable, dest: &mut SymbolTable) {
311 let parts: Vec<&str> = symbol.split('.').collect();
312 let mut current = source;
315 for i in 0..parts.len() {
316 match current.table.get(parts[i]) {
317 Some(SymbolTableEntry::Value(v)) => {
318 let key = parts[..=i].join(".");
320 let _ = dest.set(&key, v.clone());
321 return;
322 }
323 Some(SymbolTableEntry::Table(t)) => {
324 current = t;
325 }
327 None => return, }
329 }
330 let key = parts.join(".");
332 dest.set_table(&key, current.clone());
333}
334
335impl fmt::Display for FormatString {
336 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337 write!(f, "{}", self.raw)
338 }
339}
340impl PartialEq for FormatString {
341 fn eq(&self, other: &Self) -> bool {
342 self.raw == other.raw
343 }
344}
345impl Eq for FormatString {}
346impl<'de> serde::Deserialize<'de> for FormatString {
347 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
348 struct FsVisitor;
349 impl<'de> serde::de::Visitor<'de> for FsVisitor {
350 type Value = FormatString;
351 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
352 write!(f, "a string or number")
353 }
354 fn visit_str<E: de::Error>(self, v: &str) -> Result<FormatString, E> {
355 FormatString::new(v).map_err(de::Error::custom)
356 }
357 fn visit_string<E: de::Error>(self, v: String) -> Result<FormatString, E> {
358 FormatString::new(&v).map_err(de::Error::custom)
359 }
360 fn visit_i64<E: de::Error>(self, v: i64) -> Result<FormatString, E> {
361 FormatString::new(&v.to_string()).map_err(de::Error::custom)
362 }
363 fn visit_u64<E: de::Error>(self, v: u64) -> Result<FormatString, E> {
364 FormatString::new(&v.to_string()).map_err(de::Error::custom)
365 }
366 fn visit_f64<E: de::Error>(self, v: f64) -> Result<FormatString, E> {
367 FormatString::new(&v.to_string()).map_err(de::Error::custom)
368 }
369 fn visit_bool<E: de::Error>(self, v: bool) -> Result<FormatString, E> {
370 FormatString::new(&v.to_string()).map_err(de::Error::custom)
371 }
372 }
373 deserializer.deserialize_any(FsVisitor)
374 }
375}
376impl serde::Serialize for FormatString {
377 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
378 self.raw.serialize(serializer)
379 }
380}
381
382fn check_comprehension_vars(
384 node: &ruff_python_ast::Expr,
385 let_names: &std::collections::HashSet<String>,
386) -> Result<(), ExpressionError> {
387 use ruff_python_ast as ast;
388 match node {
389 ast::Expr::ListComp(lc) => {
390 for gen in &lc.generators {
391 if let ast::Expr::Name(n) = &gen.target {
392 let var = n.id.as_str();
393 if let Some(first) = var.chars().next() {
395 if !first.is_ascii_lowercase() && first != '_' {
396 return Err(ExpressionError::new(format!(
397 "List comprehension variable '{var}' must start with a lowercase letter or underscore"
398 )));
399 }
400 }
401 if let_names.contains(var) {
403 return Err(ExpressionError::new(format!(
404 "List comprehension variable '{var}' shadows a let binding"
405 )));
406 }
407 }
408 }
409 check_comprehension_vars(&lc.elt, let_names)?;
410 }
411 ast::Expr::BinOp(b) => {
412 check_comprehension_vars(&b.left, let_names)?;
413 check_comprehension_vars(&b.right, let_names)?;
414 }
415 ast::Expr::UnaryOp(u) => {
416 check_comprehension_vars(&u.operand, let_names)?;
417 }
418 ast::Expr::Compare(c) => {
419 check_comprehension_vars(&c.left, let_names)?;
420 for r in &c.comparators {
421 check_comprehension_vars(r, let_names)?;
422 }
423 }
424 ast::Expr::BoolOp(b) => {
425 for v in &b.values {
426 check_comprehension_vars(v, let_names)?;
427 }
428 }
429 ast::Expr::If(i) => {
430 check_comprehension_vars(&i.test, let_names)?;
431 check_comprehension_vars(&i.body, let_names)?;
432 check_comprehension_vars(&i.orelse, let_names)?;
433 }
434 ast::Expr::Call(c) => {
435 check_comprehension_vars(&c.func, let_names)?;
436 for a in &c.arguments.args {
437 check_comprehension_vars(a, let_names)?;
438 }
439 }
440 ast::Expr::List(l) => {
441 for e in &l.elts {
442 check_comprehension_vars(e, let_names)?;
443 }
444 }
445 ast::Expr::Tuple(t) => {
446 for e in &t.elts {
447 check_comprehension_vars(e, let_names)?;
448 }
449 }
450 ast::Expr::Subscript(s) => {
451 check_comprehension_vars(&s.value, let_names)?;
452 check_comprehension_vars(&s.slice, let_names)?;
453 }
454 ast::Expr::Attribute(a) => {
455 check_comprehension_vars(&a.value, let_names)?;
456 }
457 _ => {}
458 }
459 Ok(())
460}
461
462fn parse_segments(input: &str, profile: &ExprProfile) -> Result<Vec<Segment>, ExpressionError> {
463 let mut segments = Vec::new();
464 let len = input.len();
465 let mut pos = 0;
466 while pos < len {
467 match input[pos..].find("{{") {
468 None => {
469 if let Some(co) = input[pos..].find("}}") {
470 let cp = pos + co;
471 return Err(ExpressionError::new(format!(
472 "Failed to parse interpolation expression at [{pos}, {}]. Reason: Missing opening braces.", cp + 2
473 ))
474 .with_span(input, cp, cp + 2));
475 }
476 let rest = &input[pos..];
477 if !rest.is_empty() {
478 segments.push(Segment::Literal(rest.to_string()));
479 }
480 break;
481 }
482 Some(offset) => {
483 let op = pos + offset;
484 if let Some(co) = input[pos..].find("}}") {
485 if pos + co < op {
486 let cp = pos + co;
487 return Err(ExpressionError::new(format!(
488 "Failed to parse interpolation expression at [{pos}, {len}]. Reason: Braces mismatch."
489 ))
490 .with_span(input, cp, cp + 2));
491 }
492 }
493 if op > pos {
494 segments.push(Segment::Literal(input[pos..op].to_string()));
495 }
496 let es = op + 2;
497 match input[es..].find("}}") {
498 None => return Err(ExpressionError::new(format!(
499 "Failed to parse interpolation expression at [{op}, {len}]. Reason: Braces mismatch."
500 ))
501 .with_span(input, op, op + 2)),
502 Some(co) => {
503 let ee = es + co;
504 let be = ee + 2;
505 let et = input[es..ee].trim();
506 if et.is_empty() {
507 return Err(ExpressionError::new(format!(
508 "Failed to parse interpolation expression at [{op}, {be}]. Reason: Empty expression."
509 ))
510 .with_span(input, op, be));
511 }
512 let parsed = crate::eval::ParsedExpression::with_profile(et, profile)
513 .map_err(|e| {
514 ExpressionError::new(format!(
518 "Failed to parse interpolation expression at [{op}, {be}]. Reason: {}",
519 e.message()
520 ))
521 .with_span(input, op, be)
522 })?;
523 segments.push(Segment::Expression { start: op, end: be, parsed });
524 pos = be;
525 }
526 }
527 }
528 }
529 }
530 Ok(segments)
531}
532
533#[derive(Debug, Clone)]
538pub struct FormatStringValidationError {
539 pub message: String,
541 pub input: String,
543 pub start: usize,
545 pub end: usize,
547 pub expression_error: Option<Box<ExpressionError>>,
550}
551
552impl std::fmt::Display for FormatStringValidationError {
553 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
554 write!(
555 f,
556 "Failed to parse interpolation expression at [{}, {}]. {}",
557 self.start, self.end, self.message
558 )
559 }
560}
561
562impl std::error::Error for FormatStringValidationError {}
563
564#[derive(Clone)]
582pub struct FormatStringOptions<'a> {
583 library: Option<&'a crate::function_library::FunctionLibrary>,
584 path_format: crate::path_mapping::PathFormat,
585 target_type: Option<&'a crate::types::ExprType>,
586}
587
588impl<'a> Default for FormatStringOptions<'a> {
589 fn default() -> Self {
590 Self {
591 library: None,
592 path_format: crate::path_mapping::PathFormat::host(),
593 target_type: None,
594 }
595 }
596}
597
598impl<'a> FormatStringOptions<'a> {
599 #[must_use]
601 pub fn new() -> Self {
602 Self::default()
603 }
604
605 #[must_use]
615 pub fn with_library(
616 mut self,
617 library: impl Into<Option<&'a crate::function_library::FunctionLibrary>>,
618 ) -> Self {
619 self.library = library.into();
620 self
621 }
622
623 #[must_use]
625 pub fn with_path_format(mut self, fmt: crate::path_mapping::PathFormat) -> Self {
626 self.path_format = fmt;
627 self
628 }
629
630 #[must_use]
634 pub fn with_target_type(mut self, t: &'a crate::types::ExprType) -> Self {
635 self.target_type = Some(t);
636 self
637 }
638}
639
640#[must_use]
642pub fn escape_format_string(value: &str) -> String {
643 let mut result = String::new();
644 let mut chars = value.chars().peekable();
645 while let Some(c) = chars.next() {
646 if c == '{' && chars.peek() == Some(&'{') {
647 chars.next();
648 result.push_str("{{ \"{{\" }}");
649 } else if c == '}' && chars.peek() == Some(&'}') {
650 chars.next();
651 result.push_str("{{ \"}\" + \"}\" }}");
652 } else {
653 result.push(c);
654 }
655 }
656 result
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 #[test]
664 fn literal_only() {
665 let fs = FormatString::new("hello").unwrap();
666 assert!(fs.is_literal());
667 assert_eq!(
668 fs.resolve_string_with(&SymbolTable::new(), &FormatStringOptions::default())
669 .unwrap(),
670 "hello"
671 );
672 }
673 #[test]
674 fn simple_expr() {
675 let fs = FormatString::new("{{Param.X}}").unwrap();
676 let mut st = SymbolTable::new();
677 st.set_string("Param.X", "42").unwrap();
678 assert_eq!(
679 fs.resolve_string_with(&st, &FormatStringOptions::default())
680 .unwrap(),
681 "42"
682 );
683 }
684 #[test]
685 fn complex_parses() {
686 let fs = FormatString::new("{{Param.X + 1}}").unwrap();
687 assert!(fs.has_complex_expressions());
688 }
689 #[test]
690 fn missing_close() {
691 assert!(FormatString::new("{{x").is_err());
692 }
693 #[test]
694 fn missing_open() {
695 assert!(FormatString::new("x}}").is_err());
696 }
697 #[test]
698 fn empty_expr() {
699 assert!(FormatString::new("{{}}").is_err());
700 }
701 #[test]
702 fn resolve_expr_arithmetic() {
703 let fs = FormatString::new("{{ Param.X + 3 }}").unwrap();
704 let mut st = SymbolTable::new();
705 st.set("Param.X", ExprValue::Int(10)).unwrap();
706 assert_eq!(
707 fs.resolve_string_with(&st, &FormatStringOptions::default())
708 .unwrap(),
709 "13"
710 );
711 }
712 #[test]
713 fn validate_catches_bitwise() {
714 assert!(FormatString::new("{{ 5 & 3 }}").is_err());
716 }
717 #[test]
718 fn validate_catches_dict() {
719 assert!(FormatString::new("{{ {1: 2} }}").is_err());
721 }
722 #[test]
723 fn validate_catches_unknown_func() {
724 let fs = FormatString::new("{{ bad_func(1) }}").unwrap();
725 let host_lib =
726 crate::FunctionLibrary::for_profile(&crate::ExprProfile::current().with_host_context(
727 crate::HostContext::with_rules(Vec::<crate::path_mapping::PathMappingRule>::new()),
728 ));
729 assert!(fs
730 .validate_expressions(&SymbolTable::new(), &host_lib)
731 .is_err());
732 }
733 #[test]
734 fn validate_catches_empty_regex_pattern() {
735 let st = SymbolTable::new();
737 let result = crate::ParsedExpression::new("re_replace('hello', '', 'x')")
738 .and_then(|p| p.evaluate(&st));
739 assert!(
740 result.is_err(),
741 "Direct eval should error, got: {:?}",
742 result.map(|v| v.to_display_string())
743 );
744
745 let host_lib =
746 crate::FunctionLibrary::for_profile(&crate::ExprProfile::current().with_host_context(
747 crate::HostContext::with_rules(Vec::<crate::path_mapping::PathMappingRule>::new()),
748 ));
749 let fs = FormatString::new("{{ re_replace('hello', '', 'x') }}").unwrap();
750 let result = fs.validate_expressions(&SymbolTable::new(), &host_lib);
751 assert!(
752 result.is_err(),
753 "Format string validation should error, got: {:?}",
754 result
755 );
756 }
757 #[test]
758 fn validate_catches_regex_group_ref() {
759 let st = SymbolTable::new();
760 let result = crate::ParsedExpression::new(r"re_replace('hello', '(h)', r'\1')")
762 .and_then(|p| p.evaluate(&st));
763 assert!(
764 result.is_err(),
765 "Should reject \\1 group ref, got: {:?}",
766 result.map(|v| v.to_display_string())
767 );
768 let result = crate::ParsedExpression::new("re_replace('hello', '(h)', '$1')")
770 .and_then(|p| p.evaluate(&st));
771 assert!(
772 result.is_err(),
773 "Should reject $1 group ref, got: {:?}",
774 result.map(|v| v.to_display_string())
775 );
776 }
777 #[test]
778 fn validate_allows_known_func() {
779 let fs = FormatString::new("{{ len(Param.X) }}").unwrap();
780 let mut st = SymbolTable::new();
781 st.set(
782 "Param.X",
783 crate::ExprValue::unresolved(crate::ExprType::list(crate::ExprType::INT)),
784 )
785 .unwrap();
786 let host_lib =
787 crate::FunctionLibrary::for_profile(&crate::ExprProfile::current().with_host_context(
788 crate::HostContext::with_rules(Vec::<crate::path_mapping::PathMappingRule>::new()),
789 ));
790 assert!(fs.validate_expressions(&st, &host_lib).is_ok());
791 }
792 #[test]
793 fn validate_allows_arithmetic() {
794 let fs = FormatString::new("{{ Param.X + 3 }}").unwrap();
795 let mut st = SymbolTable::new();
796 st.set(
797 "Param.X",
798 crate::ExprValue::unresolved(crate::ExprType::INT),
799 )
800 .unwrap();
801 let host_lib =
802 crate::FunctionLibrary::for_profile(&crate::ExprProfile::current().with_host_context(
803 crate::HostContext::with_rules(Vec::<crate::path_mapping::PathMappingRule>::new()),
804 ));
805 assert!(fs.validate_expressions(&st, &host_lib).is_ok());
806 }
807
808 #[test]
809 fn escape_format_string_no_special_chars() {
810 assert_eq!(escape_format_string("hello world"), "hello world");
811 }
812 #[test]
813 fn escape_format_string_double_open_braces() {
814 assert_eq!(escape_format_string("{{"), "{{ \"{{\" }}");
815 }
816 #[test]
817 fn escape_format_string_double_close_braces() {
818 assert_eq!(escape_format_string("}}"), "{{ \"}\" + \"}\" }}");
819 }
820 #[test]
821 fn escape_format_string_mixed() {
822 assert_eq!(
823 escape_format_string("a{{b}}c"),
824 "a{{ \"{{\" }}b{{ \"}\" + \"}\" }}c"
825 );
826 }
827 #[test]
828 fn escape_format_string_empty() {
829 assert_eq!(escape_format_string(""), "");
830 }
831 #[test]
832 fn resolve_value_single_expr_int() {
833 let fs = FormatString::new("{{Param.X}}").unwrap();
834 let mut st = SymbolTable::new();
835 st.set("Param.X", ExprValue::Int(42)).unwrap();
836 let val = fs
837 .resolve_with(&st, &FormatStringOptions::default())
838 .unwrap();
839 assert!(matches!(val, ExprValue::Int(42)));
840 }
841 #[test]
842 fn resolve_value_single_expr_string() {
843 let fs = FormatString::new("{{Param.X}}").unwrap();
844 let mut st = SymbolTable::new();
845 st.set("Param.X", ExprValue::String("hello".into()))
846 .unwrap();
847 let val = fs
848 .resolve_with(&st, &FormatStringOptions::default())
849 .unwrap();
850 assert!(matches!(val, ExprValue::String(ref s) if s == "hello"));
851 }
852 #[test]
853 fn resolve_value_mixed() {
854 let fs = FormatString::new("hello {{Param.X}}").unwrap();
855 let mut st = SymbolTable::new();
856 st.set("Param.X", ExprValue::Int(42)).unwrap();
857 let val = fs
858 .resolve_with(&st, &FormatStringOptions::default())
859 .unwrap();
860 assert!(matches!(val, ExprValue::String(ref s) if s == "hello 42"));
861 }
862 #[test]
863 fn resolve_value_pure_literal() {
864 let fs = FormatString::new("hello").unwrap();
865 let val = fs
866 .resolve_with(&SymbolTable::new(), &FormatStringOptions::default())
867 .unwrap();
868 assert!(matches!(val, ExprValue::String(ref s) if s == "hello"));
869 }
870
871 #[test]
872 fn resolve_with_target_type_coerces_int_to_float() {
873 let fs = FormatString::new("{{Param.X}}").unwrap();
874 let mut st = SymbolTable::new();
875 st.set("Param.X", ExprValue::Int(42)).unwrap();
876 let target = crate::types::ExprType::FLOAT;
877 let val = fs
878 .resolve_with(
879 &st,
880 &FormatStringOptions::default().with_target_type(&target),
881 )
882 .unwrap();
883 assert!(matches!(val, ExprValue::Float(ref f) if f.value() == 42.0));
884 }
885
886 #[test]
887 fn resolve_with_target_type_none_preserves_int() {
888 let fs = FormatString::new("{{Param.X}}").unwrap();
889 let mut st = SymbolTable::new();
890 st.set("Param.X", ExprValue::Int(42)).unwrap();
891 let val = fs
892 .resolve_with(&st, &FormatStringOptions::default())
893 .unwrap();
894 assert!(matches!(val, ExprValue::Int(42)));
895 }
896
897 #[test]
898 fn resolve_with_target_type_path() {
899 let fs = FormatString::new("{{Param.X}}").unwrap();
900 let mut st = SymbolTable::new();
901 st.set("Param.X", ExprValue::String("/foo/bar".into()))
902 .unwrap();
903 let target = crate::types::ExprType::PATH;
904 let val = fs
905 .resolve_with(
906 &st,
907 &FormatStringOptions::default()
908 .with_path_format(crate::path_mapping::PathFormat::Posix)
909 .with_target_type(&target),
910 )
911 .unwrap();
912 assert!(matches!(val, ExprValue::Path { ref value, .. } if value == "/foo/bar"));
913 }
914
915 #[test]
916 fn copy_used_symtab_values_simple() {
917 let mut src = SymbolTable::new();
918 src.set("Param.Frame", ExprValue::Int(42)).unwrap();
919 src.set("Param.Name", ExprValue::String("test".into()))
920 .unwrap();
921 src.set("Param.Unused", ExprValue::Int(99)).unwrap();
922
923 let fs = FormatString::new("render --frame {{Param.Frame}}").unwrap();
924 let mut dest = SymbolTable::new();
925 fs.copy_used_symtab_values(&src, &mut dest);
926
927 assert!(dest.get_value("Param.Frame").is_some());
928 assert!(dest.get_value("Param.Name").is_none());
929 assert!(dest.get_value("Param.Unused").is_none());
930 }
931
932 #[test]
933 fn copy_used_symtab_values_method_call() {
934 let mut src = SymbolTable::new();
936 src.set("Param.Name", ExprValue::String("hello".into()))
937 .unwrap();
938
939 let fs = FormatString::new("{{Param.Name.upper()}}").unwrap();
940 let mut dest = SymbolTable::new();
941 fs.copy_used_symtab_values(&src, &mut dest);
942
943 assert_eq!(
944 dest.get_value("Param.Name").unwrap(),
945 &ExprValue::String("hello".into())
946 );
947 }
948
949 #[test]
950 fn copy_used_symtab_values_multiple_format_strings() {
951 let mut src = SymbolTable::new();
952 src.set("Param.Frame", ExprValue::Int(1)).unwrap();
953 src.set("Param.Name", ExprValue::String("job".into()))
954 .unwrap();
955 src.set("Task.Param.Index", ExprValue::Int(5)).unwrap();
956
957 let mut dest = SymbolTable::new();
958 FormatString::new("{{Param.Frame}}")
959 .unwrap()
960 .copy_used_symtab_values(&src, &mut dest);
961 FormatString::new("{{Task.Param.Index}}")
962 .unwrap()
963 .copy_used_symtab_values(&src, &mut dest);
964
965 assert!(dest.get_value("Param.Frame").is_some());
966 assert!(dest.get_value("Task.Param.Index").is_some());
967 assert!(dest.get_value("Param.Name").is_none());
968 }
969
970 #[test]
971 fn copy_used_symtab_values_literal_no_copy() {
972 let mut src = SymbolTable::new();
973 src.set("Param.X", ExprValue::Int(1)).unwrap();
974
975 let fs = FormatString::new("just a literal").unwrap();
976 let mut dest = SymbolTable::new();
977 fs.copy_used_symtab_values(&src, &mut dest);
978
979 assert!(dest.keys().next().is_none());
980 }
981
982 #[test]
983 fn copy_used_symtab_values_expression_with_multiple_refs() {
984 let mut src = SymbolTable::new();
985 src.set("Param.Start", ExprValue::Int(1)).unwrap();
986 src.set("Param.End", ExprValue::Int(10)).unwrap();
987 src.set("Param.Other", ExprValue::Int(99)).unwrap();
988
989 let fs = FormatString::new("{{Param.Start + Param.End}}").unwrap();
990 let mut dest = SymbolTable::new();
991 fs.copy_used_symtab_values(&src, &mut dest);
992
993 assert!(dest.get_value("Param.Start").is_some());
994 assert!(dest.get_value("Param.End").is_some());
995 assert!(dest.get_value("Param.Other").is_none());
996 }
997
998 #[test]
999 fn copy_used_symtab_values_property_access_stops_at_value() {
1000 let mut src = SymbolTable::new();
1003 src.set("Param.Name", ExprValue::String("hello".into()))
1004 .unwrap();
1005
1006 let fs = FormatString::new("{{Param.Name.upper()}}").unwrap();
1007 let mut dest = SymbolTable::new();
1008 fs.copy_used_symtab_values(&src, &mut dest);
1009
1010 assert_eq!(
1012 dest.get_value("Param.Name"),
1013 Some(&ExprValue::String("hello".into()))
1014 );
1015 assert!(dest.get("Param.Name.upper").is_none());
1017 }
1018
1019 #[test]
1020 fn copy_used_symtab_values_chained_property() {
1021 let mut src = SymbolTable::new();
1023 src.set("Param.Path", ExprValue::String("/foo/bar.exr".into()))
1024 .unwrap();
1025
1026 let fs = FormatString::new("{{Param.Path.stem.upper()}}").unwrap();
1027 let mut dest = SymbolTable::new();
1028 fs.copy_used_symtab_values(&src, &mut dest);
1029
1030 assert_eq!(
1031 dest.get_value("Param.Path"),
1032 Some(&ExprValue::String("/foo/bar.exr".into()))
1033 );
1034 assert!(dest.get("Param.Path.stem").is_none());
1035 }
1036
1037 #[test]
1038 fn copy_used_symtab_values_missing_symbol_no_error() {
1039 let src = SymbolTable::new(); let fs = FormatString::new("{{Param.Missing + Task.Param.Also.Missing}}").unwrap();
1043 let mut dest = SymbolTable::new();
1044 fs.copy_used_symtab_values(&src, &mut dest);
1045
1046 assert!(dest.keys().next().is_none());
1048 }
1049
1050 #[test]
1051 fn copy_used_symtab_values_partial_missing() {
1052 let mut src = SymbolTable::new();
1054 src.set("Param.Frame", ExprValue::Int(1)).unwrap();
1055
1056 let fs = FormatString::new("{{Param.Frame + Param.Missing}}").unwrap();
1057 let mut dest = SymbolTable::new();
1058 fs.copy_used_symtab_values(&src, &mut dest);
1059
1060 assert_eq!(dest.get_value("Param.Frame"), Some(&ExprValue::Int(1)));
1061 assert!(dest.get("Param.Missing").is_none());
1062 }
1063
1064 #[test]
1065 fn accessed_symbols_simple() {
1066 let fs = FormatString::new("render --frame {{Param.Frame}}").unwrap();
1067 let syms = fs.accessed_symbols();
1068 assert!(syms.contains("Param.Frame"));
1069 assert_eq!(syms.len(), 1);
1070 }
1071
1072 #[test]
1073 fn accessed_symbols_multiple_refs() {
1074 let fs = FormatString::new("{{Param.Start + Param.End}}").unwrap();
1075 let syms = fs.accessed_symbols();
1076 assert!(syms.contains("Param.Start"));
1077 assert!(syms.contains("Param.End"));
1078 assert_eq!(syms.len(), 2);
1079 }
1080
1081 #[test]
1082 fn accessed_symbols_literal_returns_empty() {
1083 let fs = FormatString::new("just a literal").unwrap();
1084 assert!(fs.accessed_symbols().is_empty());
1085 }
1086
1087 #[test]
1088 fn accessed_symbols_method_call() {
1089 let fs = FormatString::new("{{Param.Name.upper()}}").unwrap();
1090 let syms = fs.accessed_symbols();
1091 assert!(syms.contains("Param.Name"));
1093 }
1094
1095 #[test]
1096 fn accessed_symbols_multiple_segments() {
1097 let fs = FormatString::new("{{Param.A}}_{{Param.B}}").unwrap();
1098 let syms = fs.accessed_symbols();
1099 assert!(syms.contains("Param.A"));
1100 assert!(syms.contains("Param.B"));
1101 assert_eq!(syms.len(), 2);
1102 }
1103
1104 #[test]
1107 fn options_default_matches_host_format() {
1108 let opts = FormatStringOptions::new();
1109 assert_eq!(opts.path_format, crate::path_mapping::PathFormat::host());
1110 assert!(opts.library.is_none());
1111 assert!(opts.target_type.is_none());
1112 }
1113
1114 #[test]
1115 fn options_with_path_format() {
1116 let fs = FormatString::new("{{path('/tmp/out')}}").unwrap();
1117 let st = SymbolTable::new();
1118 let opts =
1119 FormatStringOptions::new().with_path_format(crate::path_mapping::PathFormat::Posix);
1120 let val = fs.resolve_with(&st, &opts).unwrap();
1121 match val {
1122 ExprValue::Path { format, .. } => {
1123 assert_eq!(format, crate::path_mapping::PathFormat::Posix);
1124 }
1125 _ => panic!("expected path value, got {:?}", val),
1126 }
1127 }
1128
1129 #[test]
1130 fn options_with_target_type_coerces() {
1131 let fs = FormatString::new("{{42}}").unwrap();
1132 let st = SymbolTable::new();
1133 let target = crate::types::ExprType::FLOAT;
1134 let opts = FormatStringOptions::new().with_target_type(&target);
1135 let val = fs.resolve_with(&st, &opts).unwrap();
1136 assert!(matches!(val, ExprValue::Float(_)), "got {:?}", val);
1137 }
1138
1139 #[test]
1140 fn options_default_equivalent_to_builder() {
1141 let fs = FormatString::new("{{Param.X + 1}}").unwrap();
1145 let mut st = SymbolTable::new();
1146 st.set("Param.X", ExprValue::Int(10)).unwrap();
1147
1148 let a = fs
1149 .resolve_with(&st, &FormatStringOptions::default())
1150 .unwrap();
1151 let b = fs.resolve_with(&st, &FormatStringOptions::new()).unwrap();
1152 match (a, b) {
1153 (ExprValue::Int(11), ExprValue::Int(11)) => {}
1154 (a, b) => panic!("expected Int(11) for both; got {:?} vs {:?}", a, b),
1155 }
1156 }
1157
1158 #[test]
1159 fn options_resolve_string_with_ignores_target_type() {
1160 let fs = FormatString::new("{{Param.X}}").unwrap();
1162 let mut st = SymbolTable::new();
1163 st.set("Param.X", ExprValue::Int(42)).unwrap();
1164 let t = crate::types::ExprType::FLOAT;
1165 let opts = FormatStringOptions::new().with_target_type(&t);
1166 let s = fs.resolve_string_with(&st, &opts).unwrap();
1167 assert_eq!(s, "42");
1168 }
1169
1170 #[test]
1171 fn options_with_library_is_plumbed() {
1172 let fs = FormatString::new("{{ upper('hi') }}").unwrap();
1175 let st = SymbolTable::new();
1176 let mut minimal = crate::function_library::FunctionLibrary::new();
1177 minimal
1178 .register_sig("len", "(string) -> int", crate::functions::misc::len_string)
1179 .unwrap();
1180 let opts = FormatStringOptions::new().with_library(&minimal);
1181 assert!(
1182 fs.resolve_with(&st, &opts).is_err(),
1183 "should reject unknown function"
1184 );
1185 }
1186}