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