1use crate::error::{Error, Result};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, PartialEq)]
16pub enum Interpolation {
17 Literal(String),
19 Resolver {
21 name: String,
23 args: Vec<InterpolationArg>,
25 kwargs: HashMap<String, InterpolationArg>,
27 },
28 SelfRef {
30 path: String,
32 relative: bool,
34 },
35 Concat(Vec<Interpolation>),
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub enum InterpolationArg {
42 Literal(String),
44 Nested(Box<Interpolation>),
46}
47
48impl InterpolationArg {
49 pub fn is_literal(&self) -> bool {
51 matches!(self, InterpolationArg::Literal(_))
52 }
53
54 pub fn as_literal(&self) -> Option<&str> {
56 match self {
57 InterpolationArg::Literal(s) => Some(s),
58 _ => None,
59 }
60 }
61}
62
63pub struct InterpolationParser<'a> {
65 input: &'a str,
66 pos: usize,
67}
68
69impl<'a> InterpolationParser<'a> {
70 pub fn new(input: &'a str) -> Self {
72 Self { input, pos: 0 }
73 }
74
75 pub fn parse(&mut self) -> Result<Interpolation> {
77 let mut parts = Vec::new();
78
79 while !self.is_eof() {
80 if self.check_escape() {
81 self.advance(); self.advance(); self.advance(); parts.push(Interpolation::Literal("${".to_string()));
86 } else if self.check_interpolation_start() {
87 parts.push(self.parse_interpolation()?);
88 } else {
89 let literal = self.collect_literal();
91 if !literal.is_empty() {
92 parts.push(Interpolation::Literal(literal));
93 }
94 }
95 }
96
97 match parts.len() {
99 0 => Ok(Interpolation::Literal(String::new())),
100 1 => Ok(parts.remove(0)),
101 _ => {
102 let merged = merge_adjacent_literals(parts);
104 if merged.len() == 1 {
105 Ok(merged.into_iter().next().unwrap())
106 } else {
107 Ok(Interpolation::Concat(merged))
108 }
109 }
110 }
111 }
112
113 fn is_eof(&self) -> bool {
115 self.pos >= self.input.len()
116 }
117
118 fn current(&self) -> Option<char> {
120 self.input[self.pos..].chars().next()
121 }
122
123 fn peek(&self) -> Option<char> {
125 let mut chars = self.input[self.pos..].chars();
126 chars.next();
127 chars.next()
128 }
129
130 fn peek_n(&self, n: usize) -> Option<char> {
132 self.input[self.pos..].chars().nth(n)
133 }
134
135 fn advance(&mut self) {
137 if let Some(c) = self.current() {
138 self.pos += c.len_utf8();
139 }
140 }
141
142 fn check_escape(&self) -> bool {
144 self.current() == Some('\\') && self.peek() == Some('$') && self.peek_n(2) == Some('{')
145 }
146
147 fn check_interpolation_start(&self) -> bool {
149 self.current() == Some('$') && self.peek() == Some('{')
150 }
151
152 fn collect_literal(&mut self) -> String {
154 let mut result = String::new();
155
156 while !self.is_eof() {
157 if self.check_escape() {
158 break;
159 }
160 if self.check_interpolation_start() {
161 break;
162 }
163 if let Some(c) = self.current() {
164 result.push(c);
165 self.advance();
166 }
167 }
168
169 result
170 }
171
172 fn parse_interpolation(&mut self) -> Result<Interpolation> {
174 self.advance(); self.advance(); self.skip_whitespace();
180
181 if self.is_eof() {
182 return Err(Error::parse("Unexpected end of input in interpolation"));
183 }
184
185 if self.current() == Some('.') {
187 return self.parse_self_ref(true);
188 }
189
190 let identifier = self.collect_identifier();
192
193 if identifier.is_empty() {
194 return Err(Error::parse("Empty interpolation expression"));
195 }
196
197 self.skip_whitespace();
198
199 match self.current() {
201 Some(':') => {
202 self.advance(); self.parse_resolver_call(identifier)
205 }
206 Some('}') => {
207 self.advance(); Ok(Interpolation::Resolver {
211 name: "ref".to_string(),
212 args: vec![InterpolationArg::Literal(identifier)],
213 kwargs: HashMap::new(),
214 })
215 }
216 Some(',') => {
217 self.advance(); self.parse_resolver_call_with_first_arg("ref".to_string(), identifier)
221 }
222 Some(c) => Err(Error::parse(format!(
223 "Unexpected character '{}' in interpolation",
224 c
225 ))),
226 None => Err(Error::parse("Unexpected end of input in interpolation")),
227 }
228 }
229
230 fn parse_self_ref(&mut self, relative: bool) -> Result<Interpolation> {
232 let mut path = String::new();
233
234 while !self.is_eof() {
236 match self.current() {
237 Some('}') => {
238 self.advance();
239 break;
240 }
241 Some(c) if c.is_alphanumeric() || c == '_' || c == '.' || c == '[' || c == ']' => {
242 path.push(c);
243 self.advance();
244 }
245 Some(c) => {
246 return Err(Error::parse(format!("Invalid character '{}' in path", c)));
247 }
248 None => {
249 return Err(Error::parse("Unexpected end of input in path"));
250 }
251 }
252 }
253
254 Ok(Interpolation::SelfRef { path, relative })
255 }
256
257 fn parse_resolver_call(&mut self, name: String) -> Result<Interpolation> {
259 let mut args = Vec::new();
260 let mut kwargs = HashMap::new();
261
262 loop {
264 self.skip_whitespace();
265
266 if self.current() == Some('}') {
267 self.advance();
268 break;
269 }
270
271 if !args.is_empty() || !kwargs.is_empty() {
272 if self.current() != Some(',') {
274 return Err(Error::parse("Expected ',' between arguments"));
275 }
276 self.advance(); self.skip_whitespace();
278 }
279
280 if let Some((key, value_arg)) = self.try_parse_kwarg()? {
283 kwargs.insert(key, value_arg);
284 } else {
285 let arg = self.parse_argument()?;
287 args.push(arg);
288 }
289 }
290
291 Ok(Interpolation::Resolver { name, args, kwargs })
292 }
293
294 fn parse_resolver_call_with_first_arg(
297 &mut self,
298 name: String,
299 first_arg: String,
300 ) -> Result<Interpolation> {
301 let mut args = vec![InterpolationArg::Literal(first_arg)];
302 let mut kwargs = HashMap::new();
303
304 loop {
306 self.skip_whitespace();
307
308 if self.current() == Some('}') {
309 self.advance();
310 break;
311 }
312
313 self.skip_whitespace();
315
316 if let Some((key, value_arg)) = self.try_parse_kwarg()? {
318 kwargs.insert(key, value_arg);
319 } else {
320 let arg = self.parse_argument()?;
322 args.push(arg);
323 }
324
325 self.skip_whitespace();
326
327 match self.current() {
329 Some(',') => {
330 self.advance();
331 continue;
332 }
333 Some('}') => {
334 continue;
336 }
337 Some(c) => {
338 return Err(Error::parse(format!(
339 "Expected ',' or '}}' but found '{}'",
340 c
341 )));
342 }
343 None => {
344 return Err(Error::parse("Unexpected end of input in interpolation"));
345 }
346 }
347 }
348
349 Ok(Interpolation::Resolver { name, args, kwargs })
350 }
351
352 fn try_parse_kwarg(&mut self) -> Result<Option<(String, InterpolationArg)>> {
355 let start_pos = self.pos;
357
358 let mut key = String::new();
360 while !self.is_eof() {
361 match self.current() {
362 Some(c) if c.is_alphanumeric() || c == '_' => {
363 key.push(c);
364 self.advance();
365 }
366 Some('=') if !key.is_empty() => {
367 self.advance(); let value = self.parse_argument()?;
370 return Ok(Some((key, value)));
371 }
372 _ => {
373 self.pos = start_pos;
375 return Ok(None);
376 }
377 }
378 }
379
380 self.pos = start_pos;
382 Ok(None)
383 }
384
385 fn parse_argument(&mut self) -> Result<InterpolationArg> {
387 self.skip_whitespace();
388
389 if self.check_interpolation_start() {
390 let nested = self.parse_interpolation()?;
392 Ok(InterpolationArg::Nested(Box::new(nested)))
393 } else {
394 let mut value = String::new();
396 let mut depth = 0; while !self.is_eof() {
399 match self.current() {
400 Some('$') if self.peek() == Some('{') => {
401 let nested = self.parse_interpolation()?;
403 return Ok(InterpolationArg::Nested(Box::new(if value.is_empty() {
404 nested
405 } else {
406 Interpolation::Concat(vec![Interpolation::Literal(value), nested])
408 })));
409 }
410 Some('{') => {
411 depth += 1;
412 value.push('{');
413 self.advance();
414 }
415 Some('}') => {
416 if depth == 0 {
417 break;
418 }
419 depth -= 1;
420 value.push('}');
421 self.advance();
422 }
423 Some(',') if depth == 0 => {
424 break;
425 }
426 Some(c) => {
427 value.push(c);
428 self.advance();
429 }
430 None => break,
431 }
432 }
433
434 Ok(InterpolationArg::Literal(value.trim().to_string()))
435 }
436 }
437
438 fn collect_identifier(&mut self) -> String {
440 let mut result = String::new();
441
442 while !self.is_eof() {
443 match self.current() {
444 Some(c) if c.is_alphanumeric() || c == '_' || c == '.' || c == '[' || c == ']' => {
445 result.push(c);
446 self.advance();
447 }
448 _ => break,
449 }
450 }
451
452 result
453 }
454
455 fn skip_whitespace(&mut self) {
457 while let Some(c) = self.current() {
458 if c.is_whitespace() {
459 self.advance();
460 } else {
461 break;
462 }
463 }
464 }
465}
466
467fn merge_adjacent_literals(parts: Vec<Interpolation>) -> Vec<Interpolation> {
469 let mut result = Vec::new();
470 let mut current_literal = String::new();
471
472 for part in parts {
473 match part {
474 Interpolation::Literal(s) => {
475 current_literal.push_str(&s);
476 }
477 other => {
478 if !current_literal.is_empty() {
479 result.push(Interpolation::Literal(current_literal));
480 current_literal = String::new();
481 }
482 result.push(other);
483 }
484 }
485 }
486
487 if !current_literal.is_empty() {
488 result.push(Interpolation::Literal(current_literal));
489 }
490
491 result
492}
493
494pub fn parse(input: &str) -> Result<Interpolation> {
496 InterpolationParser::new(input).parse()
497}
498
499pub fn contains_interpolation(input: &str) -> bool {
501 let mut chars = input.chars().peekable();
502
503 while let Some(c) = chars.next() {
504 if c == '\\' {
505 chars.next();
507 } else if c == '$' && chars.peek() == Some(&'{') {
508 return true;
509 }
510 }
511
512 false
513}
514
515pub fn needs_processing(input: &str) -> bool {
517 let mut chars = input.chars().peekable();
518
519 while let Some(c) = chars.next() {
520 if c == '\\' && chars.peek() == Some(&'$') {
521 return true;
523 } else if c == '$' && chars.peek() == Some(&'{') {
524 return true;
526 }
527 }
528
529 false
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn test_parse_literal() {
538 let result = parse("hello world").unwrap();
539 assert_eq!(result, Interpolation::Literal("hello world".into()));
540 }
541
542 #[test]
543 fn test_parse_empty() {
544 let result = parse("").unwrap();
545 assert_eq!(result, Interpolation::Literal("".into()));
546 }
547
548 #[test]
549 fn test_parse_env_resolver() {
550 let result = parse("${env:MY_VAR}").unwrap();
551 assert_eq!(
552 result,
553 Interpolation::Resolver {
554 name: "env".into(),
555 args: vec![InterpolationArg::Literal("MY_VAR".into())],
556 kwargs: HashMap::new(),
557 }
558 );
559 }
560
561 #[test]
562 fn test_parse_env_with_default() {
563 let result = parse("${env:MY_VAR,default_value}").unwrap();
564 assert_eq!(
565 result,
566 Interpolation::Resolver {
567 name: "env".into(),
568 args: vec![
569 InterpolationArg::Literal("MY_VAR".into()),
570 InterpolationArg::Literal("default_value".into()),
571 ],
572 kwargs: HashMap::new(),
573 }
574 );
575 }
576
577 #[test]
578 fn test_parse_self_reference() {
579 let result = parse("${database.host}").unwrap();
580 assert_eq!(
582 result,
583 Interpolation::Resolver {
584 name: "ref".into(),
585 args: vec![InterpolationArg::Literal("database.host".into())],
586 kwargs: HashMap::new(),
587 }
588 );
589 }
590
591 #[test]
592 fn test_parse_self_reference_with_default() {
593 let result = parse("${database.host,default=fallback}").unwrap();
594 if let Interpolation::Resolver { name, args, kwargs } = result {
596 assert_eq!(name, "ref");
597 assert_eq!(args.len(), 1);
598 assert_eq!(args[0].as_literal(), Some("database.host"));
599 assert_eq!(kwargs.len(), 1);
600 assert!(kwargs.contains_key("default"));
601 assert_eq!(
602 kwargs.get("default").and_then(|v| v.as_literal()),
603 Some("fallback")
604 );
605 } else {
606 panic!("Expected Resolver");
607 }
608 }
609
610 #[test]
611 fn test_parse_relative_self_reference() {
612 let result = parse("${.sibling}").unwrap();
613 assert_eq!(
615 result,
616 Interpolation::SelfRef {
617 path: ".sibling".into(),
618 relative: true,
619 }
620 );
621 }
622
623 #[test]
624 fn test_parse_array_access() {
625 let result = parse("${servers[0].host}").unwrap();
626 assert_eq!(
628 result,
629 Interpolation::Resolver {
630 name: "ref".into(),
631 args: vec![InterpolationArg::Literal("servers[0].host".into())],
632 kwargs: HashMap::new(),
633 }
634 );
635 }
636
637 #[test]
638 fn test_parse_escaped() {
639 let result = parse(r"\${not_interpolated}").unwrap();
640 assert_eq!(result, Interpolation::Literal("${not_interpolated}".into()));
641 }
642
643 #[test]
644 fn test_parse_concatenation() {
645 let result = parse("prefix_${env:VAR}_suffix").unwrap();
646 assert!(matches!(result, Interpolation::Concat(_)));
647
648 if let Interpolation::Concat(parts) = result {
649 assert_eq!(parts.len(), 3);
650 assert_eq!(parts[0], Interpolation::Literal("prefix_".into()));
651 assert!(matches!(parts[1], Interpolation::Resolver { .. }));
652 assert_eq!(parts[2], Interpolation::Literal("_suffix".into()));
653 }
654 }
655
656 #[test]
657 fn test_parse_nested_interpolation() {
658 let result = parse("${env:VAR,${env:DEFAULT,fallback}}").unwrap();
659
660 if let Interpolation::Resolver { name, args, .. } = result {
661 assert_eq!(name, "env");
662 assert_eq!(args.len(), 2);
663 assert!(matches!(args[0], InterpolationArg::Literal(_)));
664 assert!(matches!(args[1], InterpolationArg::Nested(_)));
665 } else {
666 panic!("Expected Resolver, got {:?}", result);
667 }
668 }
669
670 #[test]
671 fn test_parse_kwargs() {
672 let result = parse("${file:./config.yaml,parse=text}").unwrap();
673
674 if let Interpolation::Resolver { kwargs, .. } = result {
675 assert!(kwargs.contains_key("parse"));
676 if let Some(InterpolationArg::Literal(value)) = kwargs.get("parse") {
677 assert_eq!(value, "text");
678 } else {
679 panic!("Expected literal value for parse kwarg");
680 }
681 } else {
682 panic!("Expected Resolver");
683 }
684 }
685
686 #[test]
687 fn test_contains_interpolation() {
688 assert!(contains_interpolation("${env:VAR}"));
689 assert!(contains_interpolation("prefix ${env:VAR} suffix"));
690 assert!(!contains_interpolation("no interpolation"));
691 assert!(!contains_interpolation(r"\${escaped}"));
692 assert!(!contains_interpolation("just $dollar"));
693 }
694
695 #[test]
696 fn test_parse_unclosed_interpolation() {
697 let result = parse("${env:VAR");
698 assert!(result.is_err());
699 }
700
701 #[test]
704 fn test_parse_empty_interpolation() {
705 let result = parse("${}");
706 assert!(result.is_err());
707 let err = result.unwrap_err();
708 assert!(err.to_string().contains("Empty"));
709 }
710
711 #[test]
712 fn test_parse_resolver_no_args() {
713 let result = parse("${env:}").unwrap();
715 if let Interpolation::Resolver { name, args, .. } = result {
716 assert_eq!(name, "env");
717 assert!(
720 args.is_empty()
721 || (args.len() == 1 && args[0] == InterpolationArg::Literal("".into()))
722 );
723 } else {
724 panic!("Expected Resolver");
725 }
726 }
727
728 #[test]
729 fn test_parse_whitespace_in_interpolation() {
730 let result = parse("${ env:VAR }").unwrap();
731 if let Interpolation::Resolver { name, .. } = result {
732 assert_eq!(name, "env");
733 } else {
734 panic!("Expected Resolver");
735 }
736 }
737
738 #[test]
739 fn test_parse_multiple_escapes() {
740 let result = parse(r"\${first}\${second}").unwrap();
741 assert_eq!(result, Interpolation::Literal("${first}${second}".into()));
742 }
743
744 #[test]
745 fn test_parse_interpolation_at_start() {
746 let result = parse("${env:VAR}suffix").unwrap();
747 if let Interpolation::Concat(parts) = result {
748 assert_eq!(parts.len(), 2);
749 assert!(matches!(parts[0], Interpolation::Resolver { .. }));
750 assert_eq!(parts[1], Interpolation::Literal("suffix".into()));
751 } else {
752 panic!("Expected Concat");
753 }
754 }
755
756 #[test]
757 fn test_parse_interpolation_at_end() {
758 let result = parse("prefix${env:VAR}").unwrap();
759 if let Interpolation::Concat(parts) = result {
760 assert_eq!(parts.len(), 2);
761 assert_eq!(parts[0], Interpolation::Literal("prefix".into()));
762 assert!(matches!(parts[1], Interpolation::Resolver { .. }));
763 } else {
764 panic!("Expected Concat");
765 }
766 }
767
768 #[test]
769 fn test_parse_adjacent_interpolations() {
770 let result = parse("${env:A}${env:B}").unwrap();
771 if let Interpolation::Concat(parts) = result {
772 assert_eq!(parts.len(), 2);
773 assert!(matches!(parts[0], Interpolation::Resolver { .. }));
774 assert!(matches!(parts[1], Interpolation::Resolver { .. }));
775 } else {
776 panic!("Expected Concat");
777 }
778 }
779
780 #[test]
781 fn test_parse_deeply_nested_path() {
782 let result = parse("${a.b.c.d.e.f.g.h}").unwrap();
783 if let Interpolation::Resolver { name, args, .. } = result {
785 assert_eq!(name, "ref");
786 assert_eq!(args.len(), 1);
787 assert_eq!(args[0].as_literal(), Some("a.b.c.d.e.f.g.h"));
788 } else {
789 panic!("Expected Resolver");
790 }
791 }
792
793 #[test]
794 fn test_parse_multiple_array_indices() {
795 let result = parse("${matrix[0][1][2]}").unwrap();
796 if let Interpolation::Resolver { name, args, .. } = result {
798 assert_eq!(name, "ref");
799 assert_eq!(args[0].as_literal(), Some("matrix[0][1][2]"));
800 } else {
801 panic!("Expected Resolver");
802 }
803 }
804
805 #[test]
806 fn test_parse_mixed_path_and_array() {
807 let result = parse("${data.items[0].nested[1].value}").unwrap();
808 if let Interpolation::Resolver { name, args, .. } = result {
810 assert_eq!(name, "ref");
811 assert_eq!(args[0].as_literal(), Some("data.items[0].nested[1].value"));
812 } else {
813 panic!("Expected Resolver");
814 }
815 }
816
817 #[test]
818 fn test_parse_underscore_in_identifiers() {
819 let result = parse("${my_var.some_path}").unwrap();
820 if let Interpolation::Resolver { name, args, .. } = result {
822 assert_eq!(name, "ref");
823 assert_eq!(args[0].as_literal(), Some("my_var.some_path"));
824 } else {
825 panic!("Expected Resolver");
826 }
827 }
828
829 #[test]
830 fn test_parse_resolver_with_multiple_args() {
831 let result = parse("${resolver:arg1,arg2,arg3}").unwrap();
832 if let Interpolation::Resolver { name, args, .. } = result {
833 assert_eq!(name, "resolver");
834 assert_eq!(args.len(), 3);
835 } else {
836 panic!("Expected Resolver");
837 }
838 }
839
840 #[test]
841 fn test_parse_mixed_escaped_and_interpolation() {
842 let result = parse(r"literal \${escaped} ${env:VAR} more").unwrap();
843 if let Interpolation::Concat(parts) = result {
844 assert!(parts.len() >= 3);
845 } else {
846 panic!("Expected Concat");
847 }
848 }
849
850 #[test]
851 fn test_needs_processing() {
852 assert!(needs_processing("${env:VAR}"));
853 assert!(needs_processing(r"\${escaped}"));
854 assert!(!needs_processing("no special chars"));
855 assert!(!needs_processing("just $dollar"));
856 }
857
858 #[test]
859 fn test_parse_invalid_char_in_path() {
860 let result = parse("${path!invalid}");
861 assert!(result.is_err());
862 }
863
864 #[test]
865 fn test_interpolation_arg_methods() {
866 let lit = InterpolationArg::Literal("test".into());
867 assert!(lit.is_literal());
868 assert_eq!(lit.as_literal(), Some("test"));
869
870 let nested = InterpolationArg::Nested(Box::new(Interpolation::Literal("x".into())));
871 assert!(!nested.is_literal());
872 assert_eq!(nested.as_literal(), None);
873 }
874
875 #[test]
876 fn test_parse_relative_parent_reference() {
877 let result = parse("${..parent.value}").unwrap();
878 if let Interpolation::SelfRef { path, relative } = result {
879 assert!(relative);
880 assert_eq!(path, "..parent.value");
881 } else {
882 panic!("Expected relative SelfRef");
883 }
884 }
885}