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::SelfRef {
210 path: identifier,
211 relative: false,
212 })
213 }
214 Some(',') => {
215 Err(Error::parse(format!(
218 "Unexpected ',' after identifier '{}'. Did you mean to use a resolver?",
219 identifier
220 )))
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 try_parse_kwarg(&mut self) -> Result<Option<(String, InterpolationArg)>> {
297 let start_pos = self.pos;
299
300 let mut key = String::new();
302 while !self.is_eof() {
303 match self.current() {
304 Some(c) if c.is_alphanumeric() || c == '_' => {
305 key.push(c);
306 self.advance();
307 }
308 Some('=') if !key.is_empty() => {
309 self.advance(); let value = self.parse_argument()?;
312 return Ok(Some((key, value)));
313 }
314 _ => {
315 self.pos = start_pos;
317 return Ok(None);
318 }
319 }
320 }
321
322 self.pos = start_pos;
324 Ok(None)
325 }
326
327 fn parse_argument(&mut self) -> Result<InterpolationArg> {
329 self.skip_whitespace();
330
331 if self.check_interpolation_start() {
332 let nested = self.parse_interpolation()?;
334 Ok(InterpolationArg::Nested(Box::new(nested)))
335 } else {
336 let mut value = String::new();
338 let mut depth = 0; while !self.is_eof() {
341 match self.current() {
342 Some('$') if self.peek() == Some('{') => {
343 let nested = self.parse_interpolation()?;
345 return Ok(InterpolationArg::Nested(Box::new(if value.is_empty() {
346 nested
347 } else {
348 Interpolation::Concat(vec![Interpolation::Literal(value), nested])
350 })));
351 }
352 Some('{') => {
353 depth += 1;
354 value.push('{');
355 self.advance();
356 }
357 Some('}') => {
358 if depth == 0 {
359 break;
360 }
361 depth -= 1;
362 value.push('}');
363 self.advance();
364 }
365 Some(',') if depth == 0 => {
366 break;
367 }
368 Some(c) => {
369 value.push(c);
370 self.advance();
371 }
372 None => break,
373 }
374 }
375
376 Ok(InterpolationArg::Literal(value.trim().to_string()))
377 }
378 }
379
380 fn collect_identifier(&mut self) -> String {
382 let mut result = String::new();
383
384 while !self.is_eof() {
385 match self.current() {
386 Some(c) if c.is_alphanumeric() || c == '_' || c == '.' || c == '[' || c == ']' => {
387 result.push(c);
388 self.advance();
389 }
390 _ => break,
391 }
392 }
393
394 result
395 }
396
397 fn skip_whitespace(&mut self) {
399 while let Some(c) = self.current() {
400 if c.is_whitespace() {
401 self.advance();
402 } else {
403 break;
404 }
405 }
406 }
407}
408
409fn merge_adjacent_literals(parts: Vec<Interpolation>) -> Vec<Interpolation> {
411 let mut result = Vec::new();
412 let mut current_literal = String::new();
413
414 for part in parts {
415 match part {
416 Interpolation::Literal(s) => {
417 current_literal.push_str(&s);
418 }
419 other => {
420 if !current_literal.is_empty() {
421 result.push(Interpolation::Literal(current_literal));
422 current_literal = String::new();
423 }
424 result.push(other);
425 }
426 }
427 }
428
429 if !current_literal.is_empty() {
430 result.push(Interpolation::Literal(current_literal));
431 }
432
433 result
434}
435
436pub fn parse(input: &str) -> Result<Interpolation> {
438 InterpolationParser::new(input).parse()
439}
440
441pub fn contains_interpolation(input: &str) -> bool {
443 let mut chars = input.chars().peekable();
444
445 while let Some(c) = chars.next() {
446 if c == '\\' {
447 chars.next();
449 } else if c == '$' && chars.peek() == Some(&'{') {
450 return true;
451 }
452 }
453
454 false
455}
456
457pub fn needs_processing(input: &str) -> bool {
459 let mut chars = input.chars().peekable();
460
461 while let Some(c) = chars.next() {
462 if c == '\\' && chars.peek() == Some(&'$') {
463 return true;
465 } else if c == '$' && chars.peek() == Some(&'{') {
466 return true;
468 }
469 }
470
471 false
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn test_parse_literal() {
480 let result = parse("hello world").unwrap();
481 assert_eq!(result, Interpolation::Literal("hello world".into()));
482 }
483
484 #[test]
485 fn test_parse_empty() {
486 let result = parse("").unwrap();
487 assert_eq!(result, Interpolation::Literal("".into()));
488 }
489
490 #[test]
491 fn test_parse_env_resolver() {
492 let result = parse("${env:MY_VAR}").unwrap();
493 assert_eq!(
494 result,
495 Interpolation::Resolver {
496 name: "env".into(),
497 args: vec![InterpolationArg::Literal("MY_VAR".into())],
498 kwargs: HashMap::new(),
499 }
500 );
501 }
502
503 #[test]
504 fn test_parse_env_with_default() {
505 let result = parse("${env:MY_VAR,default_value}").unwrap();
506 assert_eq!(
507 result,
508 Interpolation::Resolver {
509 name: "env".into(),
510 args: vec![
511 InterpolationArg::Literal("MY_VAR".into()),
512 InterpolationArg::Literal("default_value".into()),
513 ],
514 kwargs: HashMap::new(),
515 }
516 );
517 }
518
519 #[test]
520 fn test_parse_self_reference() {
521 let result = parse("${database.host}").unwrap();
522 assert_eq!(
523 result,
524 Interpolation::SelfRef {
525 path: "database.host".into(),
526 relative: false,
527 }
528 );
529 }
530
531 #[test]
532 fn test_parse_relative_self_reference() {
533 let result = parse("${.sibling}").unwrap();
534 assert_eq!(
536 result,
537 Interpolation::SelfRef {
538 path: ".sibling".into(),
539 relative: true,
540 }
541 );
542 }
543
544 #[test]
545 fn test_parse_array_access() {
546 let result = parse("${servers[0].host}").unwrap();
547 assert_eq!(
548 result,
549 Interpolation::SelfRef {
550 path: "servers[0].host".into(),
551 relative: false,
552 }
553 );
554 }
555
556 #[test]
557 fn test_parse_escaped() {
558 let result = parse(r"\${not_interpolated}").unwrap();
559 assert_eq!(result, Interpolation::Literal("${not_interpolated}".into()));
560 }
561
562 #[test]
563 fn test_parse_concatenation() {
564 let result = parse("prefix_${env:VAR}_suffix").unwrap();
565 assert!(matches!(result, Interpolation::Concat(_)));
566
567 if let Interpolation::Concat(parts) = result {
568 assert_eq!(parts.len(), 3);
569 assert_eq!(parts[0], Interpolation::Literal("prefix_".into()));
570 assert!(matches!(parts[1], Interpolation::Resolver { .. }));
571 assert_eq!(parts[2], Interpolation::Literal("_suffix".into()));
572 }
573 }
574
575 #[test]
576 fn test_parse_nested_interpolation() {
577 let result = parse("${env:VAR,${env:DEFAULT,fallback}}").unwrap();
578
579 if let Interpolation::Resolver { name, args, .. } = result {
580 assert_eq!(name, "env");
581 assert_eq!(args.len(), 2);
582 assert!(matches!(args[0], InterpolationArg::Literal(_)));
583 assert!(matches!(args[1], InterpolationArg::Nested(_)));
584 } else {
585 panic!("Expected Resolver, got {:?}", result);
586 }
587 }
588
589 #[test]
590 fn test_parse_kwargs() {
591 let result = parse("${file:./config.yaml,parse=yaml}").unwrap();
592
593 if let Interpolation::Resolver { kwargs, .. } = result {
594 assert!(kwargs.contains_key("parse"));
595 } else {
596 panic!("Expected Resolver");
597 }
598 }
599
600 #[test]
601 fn test_contains_interpolation() {
602 assert!(contains_interpolation("${env:VAR}"));
603 assert!(contains_interpolation("prefix ${env:VAR} suffix"));
604 assert!(!contains_interpolation("no interpolation"));
605 assert!(!contains_interpolation(r"\${escaped}"));
606 assert!(!contains_interpolation("just $dollar"));
607 }
608
609 #[test]
610 fn test_parse_unclosed_interpolation() {
611 let result = parse("${env:VAR");
612 assert!(result.is_err());
613 }
614
615 #[test]
618 fn test_parse_empty_interpolation() {
619 let result = parse("${}");
620 assert!(result.is_err());
621 let err = result.unwrap_err();
622 assert!(err.to_string().contains("Empty"));
623 }
624
625 #[test]
626 fn test_parse_resolver_no_args() {
627 let result = parse("${env:}").unwrap();
629 if let Interpolation::Resolver { name, args, .. } = result {
630 assert_eq!(name, "env");
631 assert!(
634 args.is_empty()
635 || (args.len() == 1 && args[0] == InterpolationArg::Literal("".into()))
636 );
637 } else {
638 panic!("Expected Resolver");
639 }
640 }
641
642 #[test]
643 fn test_parse_whitespace_in_interpolation() {
644 let result = parse("${ env:VAR }").unwrap();
645 if let Interpolation::Resolver { name, .. } = result {
646 assert_eq!(name, "env");
647 } else {
648 panic!("Expected Resolver");
649 }
650 }
651
652 #[test]
653 fn test_parse_multiple_escapes() {
654 let result = parse(r"\${first}\${second}").unwrap();
655 assert_eq!(result, Interpolation::Literal("${first}${second}".into()));
656 }
657
658 #[test]
659 fn test_parse_interpolation_at_start() {
660 let result = parse("${env:VAR}suffix").unwrap();
661 if let Interpolation::Concat(parts) = result {
662 assert_eq!(parts.len(), 2);
663 assert!(matches!(parts[0], Interpolation::Resolver { .. }));
664 assert_eq!(parts[1], Interpolation::Literal("suffix".into()));
665 } else {
666 panic!("Expected Concat");
667 }
668 }
669
670 #[test]
671 fn test_parse_interpolation_at_end() {
672 let result = parse("prefix${env:VAR}").unwrap();
673 if let Interpolation::Concat(parts) = result {
674 assert_eq!(parts.len(), 2);
675 assert_eq!(parts[0], Interpolation::Literal("prefix".into()));
676 assert!(matches!(parts[1], Interpolation::Resolver { .. }));
677 } else {
678 panic!("Expected Concat");
679 }
680 }
681
682 #[test]
683 fn test_parse_adjacent_interpolations() {
684 let result = parse("${env:A}${env:B}").unwrap();
685 if let Interpolation::Concat(parts) = result {
686 assert_eq!(parts.len(), 2);
687 assert!(matches!(parts[0], Interpolation::Resolver { .. }));
688 assert!(matches!(parts[1], Interpolation::Resolver { .. }));
689 } else {
690 panic!("Expected Concat");
691 }
692 }
693
694 #[test]
695 fn test_parse_deeply_nested_path() {
696 let result = parse("${a.b.c.d.e.f.g.h}").unwrap();
697 if let Interpolation::SelfRef { path, relative } = result {
698 assert_eq!(path, "a.b.c.d.e.f.g.h");
699 assert!(!relative);
700 } else {
701 panic!("Expected SelfRef");
702 }
703 }
704
705 #[test]
706 fn test_parse_multiple_array_indices() {
707 let result = parse("${matrix[0][1][2]}").unwrap();
708 if let Interpolation::SelfRef { path, .. } = result {
709 assert_eq!(path, "matrix[0][1][2]");
710 } else {
711 panic!("Expected SelfRef");
712 }
713 }
714
715 #[test]
716 fn test_parse_mixed_path_and_array() {
717 let result = parse("${data.items[0].nested[1].value}").unwrap();
718 if let Interpolation::SelfRef { path, .. } = result {
719 assert_eq!(path, "data.items[0].nested[1].value");
720 } else {
721 panic!("Expected SelfRef");
722 }
723 }
724
725 #[test]
726 fn test_parse_underscore_in_identifiers() {
727 let result = parse("${my_var.some_path}").unwrap();
728 if let Interpolation::SelfRef { path, .. } = result {
729 assert_eq!(path, "my_var.some_path");
730 } else {
731 panic!("Expected SelfRef");
732 }
733 }
734
735 #[test]
736 fn test_parse_resolver_with_multiple_args() {
737 let result = parse("${resolver:arg1,arg2,arg3}").unwrap();
738 if let Interpolation::Resolver { name, args, .. } = result {
739 assert_eq!(name, "resolver");
740 assert_eq!(args.len(), 3);
741 } else {
742 panic!("Expected Resolver");
743 }
744 }
745
746 #[test]
747 fn test_parse_mixed_escaped_and_interpolation() {
748 let result = parse(r"literal \${escaped} ${env:VAR} more").unwrap();
749 if let Interpolation::Concat(parts) = result {
750 assert!(parts.len() >= 3);
751 } else {
752 panic!("Expected Concat");
753 }
754 }
755
756 #[test]
757 fn test_needs_processing() {
758 assert!(needs_processing("${env:VAR}"));
759 assert!(needs_processing(r"\${escaped}"));
760 assert!(!needs_processing("no special chars"));
761 assert!(!needs_processing("just $dollar"));
762 }
763
764 #[test]
765 fn test_parse_invalid_char_in_path() {
766 let result = parse("${path!invalid}");
767 assert!(result.is_err());
768 }
769
770 #[test]
771 fn test_interpolation_arg_methods() {
772 let lit = InterpolationArg::Literal("test".into());
773 assert!(lit.is_literal());
774 assert_eq!(lit.as_literal(), Some("test"));
775
776 let nested = InterpolationArg::Nested(Box::new(Interpolation::Literal("x".into())));
777 assert!(!nested.is_literal());
778 assert_eq!(nested.as_literal(), None);
779 }
780
781 #[test]
782 fn test_parse_relative_parent_reference() {
783 let result = parse("${..parent.value}").unwrap();
784 if let Interpolation::SelfRef { path, relative } = result {
785 assert!(relative);
786 assert_eq!(path, "..parent.value");
787 } else {
788 panic!("Expected relative SelfRef");
789 }
790 }
791}