1#![allow(rustdoc::private_intra_doc_links)]
8
9use std::fmt::Display;
10use std::fmt::Write;
11use std::ops::Range;
12use std::str::FromStr;
13
14use apollo_compiler::collections::IndexMap;
15use http::Uri;
16use http::uri::PathAndQuery;
17use itertools::Itertools;
18use serde_json_bytes::Value;
19
20pub(crate) use self::encoding::UriString;
21use super::ApplyToError;
22use super::ConnectSpec;
23use crate::connectors::JSONSelection;
24use crate::connectors::json_selection::helpers::json_to_string;
25
26pub(crate) const SPECIAL_WHITE_SPACES: [char; 4] = ['\t', '\n', '\x0C', '\r'];
27
28#[derive(Clone, Debug, Default)]
30pub struct StringTemplate {
31 pub(crate) parts: Vec<Part>,
32}
33
34impl FromStr for StringTemplate {
35 type Err = Error;
36
37 fn from_str(s: &str) -> Result<Self, Self::Err> {
43 Self::parse_with_spec(s, ConnectSpec::V0_2)
44 }
62}
63
64impl StringTemplate {
65 pub fn parse_with_spec(input: &str, spec: ConnectSpec) -> Result<Self, Error> {
66 Self::common_parse_with_spec(input, 0, spec)
67 }
68
69 fn common_parse_with_spec(
72 input: &str,
73 mut offset: usize,
74 spec: ConnectSpec,
75 ) -> Result<Self, Error> {
76 let mut chars = input.chars().peekable();
77 let mut parts = Vec::new();
78 while let Some(next) = chars.peek() {
79 if SPECIAL_WHITE_SPACES.contains(next) {
80 chars.next();
81 offset += 1;
82 continue;
83 } else if *next == '{' {
84 let mut braces_count = 0; let expression = chars
86 .by_ref()
87 .skip(1)
88 .take_while(|c| {
89 if *c == '{' {
90 braces_count += 1;
91 } else if *c == '}' {
92 braces_count -= 1;
93 }
94 braces_count >= 0
95 })
96 .collect::<String>();
97 if braces_count >= 0 {
98 return Err(Error {
99 message: "Invalid expression, missing closing }".into(),
100 location: offset..input.len(),
101 });
102 }
103 offset += 1; let parsed = JSONSelection::parse_with_spec(&expression, spec).map_err(|err| {
107 let start_of_parse_error = offset + err.offset;
108 Error {
109 message: err.message,
110 location: start_of_parse_error..(offset + expression.len()),
111 }
112 })?;
113 parts.push(Part::Expression(Expression {
114 expression: parsed,
115 location: offset..(offset + expression.len()),
116 }));
117 offset += expression.len() + 1; } else {
119 let value = chars
120 .by_ref()
121 .peeking_take_while(|c| *c != '{' && !SPECIAL_WHITE_SPACES.contains(c))
122 .collect::<String>();
123 let len = value.len();
124 parts.push(Part::Constant(Constant {
125 value,
126 location: offset..offset + len,
127 }));
128 offset += len;
129 }
130 }
131 Ok(StringTemplate { parts })
132 }
133
134 pub(crate) fn expressions(&self) -> impl Iterator<Item = &Expression> {
137 self.parts.iter().filter_map(|part| {
138 if let Part::Expression(expression) = part {
139 Some(expression)
140 } else {
141 None
142 }
143 })
144 }
145}
146
147impl StringTemplate {
148 pub fn interpolate(
152 &self,
153 vars: &IndexMap<String, Value>,
154 ) -> Result<(String, Vec<ApplyToError>), Error> {
155 let mut result = String::new();
156 let mut warnings = Vec::new();
157 for part in &self.parts {
158 let part_warnings = part.interpolate(vars, &mut result)?;
159 warnings.extend(part_warnings);
160 }
161 Ok((result, warnings))
162 }
163
164 pub fn interpolate_uri(
166 &self,
167 vars: &IndexMap<String, Value>,
168 ) -> Result<(Uri, Vec<ApplyToError>), Error> {
169 let mut result = UriString::new();
170 let mut warnings = Vec::new();
171 for part in &self.parts {
172 match part {
173 Part::Constant(constant) => {
174 if constant.value.contains(['\n', '\r']) {
179 result.write_trusted(&constant.value.replace(['\n', '\r'], ""))
181 } else {
182 result.write_trusted(&constant.value)
183 }
184 .map_err(|_err| Error {
185 message: "Error writing string".to_string(),
186 location: constant.location.clone(),
187 })?;
188 }
189 Part::Expression(_) => {
190 let part_warnings = part.interpolate(vars, &mut result)?;
191 warnings.extend(part_warnings);
192 }
193 };
194 }
195 let uri = if result.contains("://") {
196 Uri::from_str(result.as_ref())
197 } else {
198 PathAndQuery::from_str(result.as_ref()).map(Uri::from)
200 }
201 .map_err(|err| Error {
202 message: format!("Invalid URI: {err}"),
203 location: 0..result.as_ref().len(),
204 })?;
205
206 Ok((uri, warnings))
207 }
208}
209
210impl Display for StringTemplate {
213 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214 for part in &self.parts {
215 match part {
216 Part::Constant(Constant { value, .. }) => write!(f, "{value}")?,
217 Part::Expression(Expression { expression, .. }) => write!(f, "{{{expression}}}")?,
218 }
219 }
220 Ok(())
221 }
222}
223
224#[derive(Debug, PartialEq, Eq)]
227pub struct Error {
228 pub message: String,
230 pub(crate) location: Range<usize>,
233}
234
235impl Display for Error {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 write!(f, "{}", self.message)
238 }
239}
240
241impl std::error::Error for Error {}
242
243#[derive(Clone, Debug)]
245pub(crate) enum Part {
246 Constant(Constant),
248 Expression(Expression),
250}
251
252impl Part {
253 pub(crate) fn location(&self) -> Range<usize> {
256 match self {
257 Self::Constant(c) => c.location.clone(),
258 Self::Expression(e) => e.location.clone(),
259 }
260 }
261
262 pub(crate) fn interpolate<Output: Write>(
268 &self,
269 vars: &IndexMap<String, Value>,
270 mut output: Output,
271 ) -> Result<Vec<ApplyToError>, Error> {
272 let mut warnings = Vec::new();
273 match self {
274 Part::Constant(Constant { value, .. }) => {
275 output.write_str(value).map_err(|err| err.into())
276 }
277 Part::Expression(Expression { expression, .. }) => {
278 let (value, errs) = expression.apply_with_vars(&Value::Null, vars);
280 warnings.extend(errs);
281 write_value(&mut output, value.as_ref().unwrap_or(&Value::Null))
282 }
283 }
284 .map_err(|err| Error {
285 message: err.to_string(),
286 location: self.location(),
287 })?;
288
289 Ok(warnings)
290 }
291}
292
293pub(crate) fn write_value<Output: Write>(
297 mut output: Output,
298 value: &Value,
299) -> Result<(), Box<dyn core::error::Error>> {
300 match json_to_string(value) {
301 Ok(result) => write!(output, "{}", result.unwrap_or_default()),
302 Err(_) => return Err("Expression is not allowed to evaluate to arrays or objects.".into()),
303 }
304 .map_err(|err| err.into())
305}
306
307#[derive(Clone, Debug, Default)]
309pub(crate) struct Constant {
310 pub(crate) value: String,
311 pub(crate) location: Range<usize>,
312}
313
314#[derive(Clone, Debug)]
316pub(crate) struct Expression {
317 pub(crate) expression: JSONSelection,
318 pub(crate) location: Range<usize>,
319}
320
321impl std::ops::Add<&Constant> for Constant {
322 type Output = Self;
323
324 fn add(self, rhs: &Self) -> Self::Output {
325 Self {
326 value: self.value + &rhs.value,
327 location: self.location.start..rhs.location.end,
328 }
329 }
330}
331
332mod encoding {
341 use std::fmt::Write;
342
343 use percent_encoding::AsciiSet;
344 use percent_encoding::NON_ALPHANUMERIC;
345 use percent_encoding::utf8_percent_encode;
346
347 const USER_INPUT: &AsciiSet = &NON_ALPHANUMERIC
353 .remove(b'-')
354 .remove(b'.')
355 .remove(b'_')
356 .remove(b'~');
357
358 const STATIC_TRUSTED: &AsciiSet = &USER_INPUT
369 .remove(b':')
370 .remove(b'/')
371 .remove(b'?')
372 .remove(b'#')
373 .remove(b'[')
374 .remove(b']')
375 .remove(b'@')
376 .remove(b'!')
377 .remove(b'$')
378 .remove(b'&')
379 .remove(b'\'')
380 .remove(b'(')
381 .remove(b')')
382 .remove(b'*')
383 .remove(b'+')
384 .remove(b',')
385 .remove(b';')
386 .remove(b'=')
387 .remove(b'%');
388
389 pub(crate) struct UriString {
390 value: String,
391 }
392
393 impl UriString {
394 pub(crate) const fn new() -> Self {
395 Self {
396 value: String::new(),
397 }
398 }
399
400 pub(crate) fn write_trusted(&mut self, s: &str) -> std::fmt::Result {
402 write!(
403 &mut self.value,
404 "{}",
405 utf8_percent_encode(s, STATIC_TRUSTED)
406 )
407 }
408
409 pub(crate) fn write_without_encoding(&mut self, s: &str) -> std::fmt::Result {
411 self.value.write_str(s)
412 }
413
414 pub(crate) fn contains(&self, pattern: &str) -> bool {
415 self.value.contains(pattern)
416 }
417
418 pub(crate) fn ends_with(&self, pattern: char) -> bool {
419 self.value.ends_with(pattern)
420 }
421
422 pub(crate) fn into_string(self) -> String {
423 self.value
424 }
425
426 pub(crate) fn is_empty(&self) -> bool {
427 self.value.is_empty()
428 }
429 }
430
431 impl Write for UriString {
432 fn write_str(&mut self, s: &str) -> std::fmt::Result {
433 write!(&mut self.value, "{}", utf8_percent_encode(s, USER_INPUT))
434 }
435 }
436
437 impl AsRef<str> for UriString {
438 fn as_ref(&self) -> &str {
439 &self.value
440 }
441 }
442
443 #[cfg(test)]
444 mod tests {
445 use percent_encoding::utf8_percent_encode;
446
447 use super::*;
448
449 #[test]
451 fn user_input_encodes_everything_but_unreserved() {
452 for i in 0..=255u8 {
453 let character = i as char;
454 let string = character.to_string();
455 let encoded = utf8_percent_encode(&string, USER_INPUT);
456 for encoded_char in encoded.into_iter().flat_map(|slice| slice.chars()) {
457 if character.is_ascii_alphanumeric()
458 || character == '-'
459 || character == '.'
460 || character == '_'
461 || character == '~'
462 {
463 assert_eq!(
464 encoded_char, character,
465 "{character} should not have been encoded"
466 );
467 } else {
468 assert!(
469 encoded_char.is_ascii_alphanumeric() || encoded_char == '%', "{encoded_char} was not encoded"
471 );
472 }
473 }
474 }
475 }
476 }
477}
478
479#[cfg(test)]
480mod test_parse {
481 use insta::assert_debug_snapshot;
482
483 use super::*;
484
485 #[test]
486 fn simple_constant() {
487 let template = StringTemplate::from_str("text").expect("simple template should be valid");
488 assert_debug_snapshot!(template);
489 }
490
491 #[test]
492 fn simple_expression() {
493 assert_debug_snapshot!(
494 StringTemplate::parse_with_spec("{$config.one}", ConnectSpec::latest()).unwrap()
495 );
496 }
497 #[test]
498 fn mixed_constant_and_expression() {
499 assert_debug_snapshot!(
500 StringTemplate::parse_with_spec("text{$config.one}text", ConnectSpec::latest())
501 .unwrap()
502 );
503 }
504
505 #[test]
506 fn expressions_with_nested_braces() {
507 assert_debug_snapshot!(
508 StringTemplate::parse_with_spec(
509 "const{$config.one { two { three } }}another-const",
510 ConnectSpec::latest()
511 )
512 .unwrap()
513 );
514 }
515
516 #[test]
517 fn missing_closing_braces() {
518 assert_debug_snapshot!(
519 StringTemplate::parse_with_spec("{$config.one", ConnectSpec::latest()),
520 @r###"
521 Err(
522 Error {
523 message: "Invalid expression, missing closing }",
524 location: 0..12,
525 },
526 )
527 "###
528 )
529 }
530}
531
532#[cfg(test)]
533mod test_interpolate {
534 use insta::assert_debug_snapshot;
535 use pretty_assertions::assert_eq;
536 use serde_json_bytes::json;
537
538 use super::*;
539 #[test]
540 fn test_interpolate() {
541 let template =
542 StringTemplate::parse_with_spec("before {$config.one} after", ConnectSpec::latest())
543 .unwrap();
544 let mut vars = IndexMap::default();
545 vars.insert("$config".to_string(), json!({"one": "foo"}));
546 assert_eq!(template.interpolate(&vars).unwrap().0, "before foo after");
547 }
548
549 #[test]
550 fn test_interpolate_missing_value() {
551 let template =
552 StringTemplate::parse_with_spec("{$config.one}", ConnectSpec::latest()).unwrap();
553 let vars = IndexMap::default();
554 assert_eq!(template.interpolate(&vars).unwrap().0, "");
555 }
556
557 #[test]
558 fn test_interpolate_value_array() {
559 let template =
560 StringTemplate::parse_with_spec("{$config.one}", ConnectSpec::latest()).unwrap();
561 let mut vars = IndexMap::default();
562 vars.insert("$config".to_string(), json!({"one": ["one", "two"]}));
563 assert_debug_snapshot!(
564 template.interpolate(&vars),
565 @r###"
566 Err(
567 Error {
568 message: "Expression is not allowed to evaluate to arrays or objects.",
569 location: 1..12,
570 },
571 )
572 "###
573 );
574 }
575
576 #[test]
577 fn test_interpolate_value_bool() {
578 let template =
579 StringTemplate::parse_with_spec("{$config.one}", ConnectSpec::latest()).unwrap();
580 let mut vars = IndexMap::default();
581 vars.insert("$config".to_string(), json!({"one": true}));
582 assert_eq!(template.interpolate(&vars).unwrap().0, "true");
583 }
584
585 #[test]
586 fn test_interpolate_value_null() {
587 let template =
588 StringTemplate::parse_with_spec("{$config.one}", ConnectSpec::latest()).unwrap();
589 let mut vars = IndexMap::default();
590 vars.insert("$config".to_string(), json!({"one": null}));
591 assert_eq!(template.interpolate(&vars).unwrap().0, "");
592 }
593
594 #[test]
595 fn test_interpolate_value_number() {
596 let template =
597 StringTemplate::parse_with_spec("{$config.one}", ConnectSpec::latest()).unwrap();
598 let mut vars = IndexMap::default();
599 vars.insert("$config".to_string(), json!({"one": 1}));
600 assert_eq!(template.interpolate(&vars).unwrap().0, "1");
601 }
602
603 #[test]
604 fn test_interpolate_value_object() {
605 let template =
606 StringTemplate::parse_with_spec("{$config.one}", ConnectSpec::latest()).unwrap();
607 let mut vars = IndexMap::default();
608 vars.insert("$config".to_string(), json!({"one": {}}));
609 assert_debug_snapshot!(
610 template.interpolate(&vars),
611 @r###"
612 Err(
613 Error {
614 message: "Expression is not allowed to evaluate to arrays or objects.",
615 location: 1..12,
616 },
617 )
618 "###
619 );
620 }
621
622 #[test]
623 fn test_interpolate_value_string() {
624 let template =
625 StringTemplate::parse_with_spec("{$config.one}", ConnectSpec::latest()).unwrap();
626 let mut vars = IndexMap::default();
627 vars.insert("$config".to_string(), json!({"one": "string"}));
628 assert_eq!(template.interpolate(&vars).unwrap().0, "string");
629 }
630}
631
632#[cfg(test)]
633mod test_interpolate_uri {
634 use pretty_assertions::assert_eq;
635 use rstest::rstest;
636
637 use super::*;
638 use crate::connectors::StringTemplate;
639
640 macro_rules! this {
641 ($($value:tt)*) => {{
642 let mut map = indexmap::IndexMap::with_capacity_and_hasher(1, Default::default());
643 map.insert("$this".to_string(), serde_json_bytes::json!({ $($value)* }));
644 map
645 }};
646 }
647
648 #[rstest]
649 #[case::leading_slash("/path")]
650 #[case::trailing_slash("path/")]
651 #[case::sandwich_slash("/path/")]
652 #[case::no_slash("path")]
653 #[case::query_params("?something&something")]
654 #[case::fragment("#blah")]
655 fn relative_uris(#[case] val: &str) {
656 let template = StringTemplate::from_str(val).unwrap();
657 let (uri, _) = template
658 .interpolate_uri(&Default::default())
659 .expect("case was valid URI");
660 assert!(uri.path_and_query().is_some());
661 assert!(uri.authority().is_none());
662 }
663
664 #[rstest]
665 #[case::http("http://example.com/something")]
666 #[case::https("https://example.com/something")]
667 #[case::ipv4("http://127.0.0.1/something")]
668 #[case::ipv6("http://[::1]/something")]
669 #[case::with_port("http://localhost:8080/something")]
670 fn absolute_uris(#[case] val: &str) {
671 let template = StringTemplate::from_str(val).unwrap();
672 let (uri, _) = template
673 .interpolate_uri(&Default::default())
674 .expect("case was valid URI");
675 assert!(uri.path_and_query().is_some());
676 assert!(uri.authority().is_some());
677 assert!(uri.scheme().is_some());
678 assert_eq!(uri.to_string(), val);
679 }
680
681 #[test]
684 fn expression_encoding() {
685 let vars = &this! {
686 "path": "/some/path",
687 "question_mark": "a?b",
688 "ampersand": "a&b=b",
689 "hash": "a#b",
690 };
691
692 let template = StringTemplate::parse_with_spec("http://localhost/{$this.path}/{$this.question_mark}?a={$this.ampersand}&c={$this.hash}", ConnectSpec::latest())
693 .expect("Failed to parse URL template");
694 let (url, _) = template
695 .interpolate_uri(vars)
696 .expect("Failed to generate URL");
697
698 assert_eq!(
699 url.to_string(),
700 "http://localhost/%2Fsome%2Fpath/a%3Fb?a=a%26b%3Db&c=a%23b"
701 );
702 }
703
704 #[test]
707 fn json_value_serialization() {
708 let vars = &this! {
710 "int": 1,
711 "float": 1.2,
712 "bool": true,
713 "null": null,
714 "string": "string",
715 "extra": []
716 };
717
718 let template = StringTemplate::parse_with_spec(
719 "/{$this.int}/{$this.float}/{$this.bool}/{$this.null}/{$this.string}",
720 ConnectSpec::latest(),
721 )
722 .unwrap();
723
724 let (uri, _) = template.interpolate(vars).expect("Failed to interpolate");
725
726 assert_eq!(uri, "/1/1.2/true//string")
727 }
728
729 #[test]
730 fn special_symbols_in_literal() {
731 let literal = "/?brackets=[]&comma=,&parens=()&semi=;&colon=:&at=@&dollar=$&excl=!&plus=+&astr=*"='";
732 let template = StringTemplate::from_str(literal).expect("Failed to parse URL template");
733 let (url, _) = template
734 .interpolate_uri(&Default::default())
735 .expect("Failed to generate URL");
736
737 assert_eq!(url.to_string(), literal);
738 }
739
740 #[test]
743 fn auto_encode_illegal_literal_characters() {
744 let template = StringTemplate::from_str("https://example.com/😈 \\")
745 .expect("Failed to parse URL template");
746
747 let (url, _) = template
748 .interpolate_uri(&Default::default())
749 .expect("Failed to generate URL");
750 assert_eq!(url.to_string(), "https://example.com/%F0%9F%98%88%20%5C")
751 }
752
753 #[test]
757 fn allow_manual_percent_encoding() {
758 let template = StringTemplate::from_str("https://example.com/%20")
759 .expect("Failed to parse URL template");
760
761 let (url, _) = template
762 .interpolate_uri(&Default::default())
763 .expect("Failed to generate URL");
764 assert_eq!(url.to_string(), "https://example.com/%20")
765 }
766
767 #[test]
770 fn multi_line_templates() {
771 let template = StringTemplate::from_str(
772 "https://example.com\n/broken\npath\n/path\n?param=value\n¶m=\r\nvalue&\nparam\n=\nvalue",
773 )
774 .expect("Failed to parse URL template");
775 let (url, _) = template
776 .interpolate_uri(&Default::default())
777 .expect("Failed to generate URL");
778
779 assert_eq!(
780 url.to_string(),
781 "https://example.com/brokenpath/path?param=value¶m=value¶m=value"
782 )
783 }
784}
785
786#[cfg(test)]
787mod test_get_expressions {
788 use super::*;
789
790 #[test]
791 fn test_variable_references() {
792 let value = StringTemplate::parse_with_spec(
793 "a {$this.a.b.c} b {$args.a.b.c} c {$config.a.b.c}",
794 ConnectSpec::latest(),
795 )
796 .unwrap();
797 let references: Vec<_> = value
798 .expressions()
799 .map(|e| e.expression.to_string())
800 .collect();
801 assert_eq!(
802 references,
803 vec!["$this.a.b.c", "$args.a.b.c", "$config.a.b.c"]
804 );
805 }
806}