Skip to main content

apollo_federation/connectors/
string_template.rs

1//! A [`StringTemplate`] is a string containing one or more [`Expression`]s.
2//! These are used in connector URIs and headers.
3//!
4//! Parsing (this module) is done by both the router at startup and composition. Validation
5//! (in [`crate::connectors::validation`]) is done only by composition.
6
7#![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/// A parsed string template, containing a series of [`Part`]s.
29#[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    /// Parses a [`StringTemplate`] from a &str, using [`ConnectSpec::V0_2`] as
38    /// the parsing version. This trait implementation should be avoided outside
39    /// tests because it runs the risk of ignoring the developer's chosen
40    /// [`ConnectSpec`] if used blindly via `.parse()`, since `FromStr` gives no
41    /// opportunity to specify additional context like the [`ConnectSpec`].
42    fn from_str(s: &str) -> Result<Self, Self::Err> {
43        Self::parse_with_spec(s, ConnectSpec::V0_2)
44        // If we want to detect risky uses of StringTemplate::from_str for
45        // templates with JSONSelection expressions, we can reenable this code.
46        // match Self::parse_with_spec(s, ConnectSpec::latest()) {
47        //     Ok(template) => {
48        //         if let Some(first) = template.expressions().next() {
49        //             Err(Error {
50        //                 message: "StringTemplate::from_str should be used only if the template does not contain any JSONSelection expressions".to_string(),
51        //                 location: first.location.clone(),
52        //             })
53        //         } else {
54        //             // If there were no expressions, the ConnectSpec does not
55        //             // matter.
56        //             Ok(template)
57        //         }
58        //     }
59        //     Err(err) => Err(err),
60        // }
61    }
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    /// Parse a [`StringTemplate`] from a particular `offset` according to a
70    /// given [`ConnectSpec`].
71    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; // Ignore braces within JSONSelection
85                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; // Account for opening brace
104                // TODO This should call JSONSelection::parse_with_spec with a
105                // ConnectSpec, but we don't have that information handy.
106                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; // Account for closing brace
118            } 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    /// Get all the dynamic [`Expression`] pieces of the template for validation. If interpolating
135    /// the entire template, use [`Self::interpolate`] instead.
136    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    /// Interpolate the expressions in the template into a basic string.
149    ///
150    /// For URIs, use [`Self::interpolate_uri`] instead.
151    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    /// Interpolate the expression as a URI, percent-encoding parts as needed.
165    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                    // We don't percent-encode constant strings, assuming the user knows what they want.
175                    // `Uri::from_str` will take care of encoding completely illegal characters
176
177                    // New lines are used for code organization, but are not wanted in the result
178                    if constant.value.contains(['\n', '\r']) {
179                        // We don't always run this replace because it has a performance cost (allocating a string)
180                        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            // Explicitly set this as a relative URI so it doesn't get confused for a domain name
199            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
210/// Expressions should be written the same as they were originally, even though we don't keep the
211/// original source around. So constants are written as-is and expressions are surrounded with `{ }`.
212impl 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/// A general-purpose error type which includes both a description of the problem and the offset span
225/// within the original expression where the problem occurred. Used for both parsing and interpolation.
226#[derive(Debug, PartialEq, Eq)]
227pub struct Error {
228    /// A human-readable description of the issue.
229    pub message: String,
230    /// The string offsets to the original [`StringTemplate`] (not just the part) where the issue
231    /// occurred. As per usual, the end of the range is exclusive.
232    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/// One piece of a [`StringTemplate`]
244#[derive(Clone, Debug)]
245pub(crate) enum Part {
246    /// A constant string literal—the piece of a [`StringTemplate`] _not_ in `{ }`
247    Constant(Constant),
248    /// A dynamic piece of a [`StringTemplate`], which came from inside `{ }` originally.
249    Expression(Expression),
250}
251
252impl Part {
253    /// Get the original location of the part from the string which was parsed to form the
254    /// [`StringTemplate`].
255    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    /// Evaluate the expression of the part (if any) and write the result to `output`.
263    ///
264    /// # Errors
265    ///
266    /// If the expression evaluates to an array or object.
267    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                // TODO: do something with the ApplyTo errors
279                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
293/// A shared definition of what it means to write a [`Value`] into a string.
294///
295/// Used for string interpolation in templates and building URIs.
296pub(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/// A constant string literal—the piece of a [`StringTemplate`] _not_ in `{ }`
308#[derive(Clone, Debug, Default)]
309pub(crate) struct Constant {
310    pub(crate) value: String,
311    pub(crate) location: Range<usize>,
312}
313
314/// A dynamic piece of a [`StringTemplate`], which came from inside `{ }` originally.
315#[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
332/// All the percent encoding rules we use for building URIs.
333///
334/// The [`AsciiSet`] type is an efficient type used by [`percent_encoding`],
335/// but the logic of it is a bit inverted from what we want.
336/// An [`AsciiSet`] lists all the characters which should be encoded, rather than those which
337/// should be allowed.
338/// Following security best practices, we instead define sets by what is
339/// explicitly allowed in a given context, so we use `remove()` to _add_ allowed characters to a context.
340mod 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    /// Characters that never need to be percent encoded are allowed by this set.
348    /// https://www.rfc-editor.org/rfc/rfc3986#section-2.3
349    /// In other words, this is the most restrictive set, encoding everything that
350    /// should _sometimes_ be encoded. We can then explicitly allow additional characters
351    /// depending on the context.
352    const USER_INPUT: &AsciiSet = &NON_ALPHANUMERIC
353        .remove(b'-')
354        .remove(b'.')
355        .remove(b'_')
356        .remove(b'~');
357
358    /// Reserved characters https://www.rfc-editor.org/rfc/rfc3986#section-2.2 are valid in URLs
359    /// though not all contexts. The responsibility for these is the developer's in static pieces
360    /// of templates.
361    ///
362    /// We _also_ don't encode `%` because we need to allow users to do manual percent-encoding of
363    /// all the reserved symbols as-needed (since it's never automatic). Rather than parsing every
364    /// `%` to see if it's a valid hex sequence, we leave that up to the developer as well since
365    /// it's a pretty advanced use-case.
366    ///
367    /// This is required because percent encoding *is not idempotent*
368    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        /// Write a bit of trusted input, like a constant piece of a template, only encoding illegal symbols.
401        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        /// Add a pre-encoded string to the URI. Used for merging without duplicating percent-encoding.
410        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        /// This test is basically checking our understanding of how `AsciiSet` works.
450        #[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 == '%', // percent encoding
470                            "{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    /// Values are all strings, they can't have semantic value for HTTP. That means no dynamic paths,
682    /// no nested query params, etc. When we expand values, we have to make sure they're safe.
683    #[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    /// The resulting values of each expression are always [`Value`]s, for which we have a
705    /// set way of encoding each as a string.
706    #[test]
707    fn json_value_serialization() {
708        // `extra` would be illegal (we don't serialize arrays), but any unused values should be ignored
709        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=*&quot='";
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    /// If a user writes a string template that includes _illegal_ characters which must be encoded,
741    /// we still encode them to avoid runtime errors.
742    #[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    /// Because we don't encode a bunch of characters that are situationally disallowed
754    /// (for flexibility of the connector author), we also need to allow that they can manually
755    /// percent encode characters themselves as-needed.
756    #[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    /// Multi-line GraphQL strings are super useful for long templates. We need to make sure they're
768    /// properly handled when generating URIs, though. New lines should be ignored.
769    #[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&param=\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&param=value&param=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}