Skip to main content

apollo_federation/connectors/
header.rs

1//! Headers defined in connectors `@source` and `@connect` directives.
2
3use std::ops::Deref;
4#[cfg(test)]
5use std::str::FromStr;
6
7use apollo_compiler::collections::IndexMap;
8use serde_json_bytes::Value;
9
10use super::ApplyToError;
11use crate::connectors::ConnectSpec;
12use crate::connectors::string_template;
13use crate::connectors::string_template::Part;
14use crate::connectors::string_template::StringTemplate;
15
16#[derive(Clone, Debug)]
17pub struct HeaderValue(StringTemplate);
18
19impl HeaderValue {
20    pub fn parse_with_spec(s: &str, spec: ConnectSpec) -> Result<Self, string_template::Error> {
21        let template = StringTemplate::parse_with_spec(s, spec)?;
22        // Validate that any constant parts are valid header values.
23        for part in &template.parts {
24            let Part::Constant(constant) = part else {
25                continue;
26            };
27            http::HeaderValue::from_str(&constant.value).map_err(|_| string_template::Error {
28                message: format!("invalid value `{}`", constant.value),
29                location: constant.location.clone(),
30            })?;
31        }
32        Ok(Self(template))
33    }
34
35    /// Evaluate expressions in the header value.
36    ///
37    /// # Errors
38    ///
39    /// Returns an error any expression can't be evaluated, or evaluates to an unsupported type.
40    pub fn interpolate(
41        &self,
42        vars: &IndexMap<String, Value>,
43    ) -> Result<(http::HeaderValue, Vec<ApplyToError>), String> {
44        let (interpolated, apply_to_errors) =
45            self.0.interpolate(vars).map_err(|e| e.to_string())?;
46        let result = http::HeaderValue::from_str(&interpolated).map_err(|e| e.to_string())?;
47        Ok((result, apply_to_errors))
48    }
49}
50
51impl Deref for HeaderValue {
52    type Target = StringTemplate;
53
54    fn deref(&self) -> &Self::Target {
55        &self.0
56    }
57}
58
59#[cfg(test)]
60impl FromStr for HeaderValue {
61    type Err = string_template::Error;
62
63    /// Parses a [`HeaderValue`] from a &str, using [`ConnectSpec::latest()`] as
64    /// the parsing version. This trait implementation is only available in
65    /// tests, and should be avoided outside tests because it runs the risk of
66    /// ignoring the developer's chosen [`ConnectSpec`].
67    fn from_str(s: &str) -> Result<Self, Self::Err> {
68        Self::parse_with_spec(s, ConnectSpec::latest())
69    }
70}
71
72#[cfg(test)]
73mod test_header_value_parse {
74    use insta::assert_debug_snapshot;
75
76    use super::*;
77
78    #[test]
79    fn simple_constant() {
80        assert_debug_snapshot!(
81            HeaderValue::from_str("text"),
82            @r###"
83        Ok(
84            HeaderValue(
85                StringTemplate {
86                    parts: [
87                        Constant(
88                            Constant {
89                                value: "text",
90                                location: 0..4,
91                            },
92                        ),
93                    ],
94                },
95            ),
96        )
97        "###
98        );
99    }
100    #[test]
101    fn simple_expression() {
102        assert_debug_snapshot!(
103            HeaderValue::from_str("{$config.one}"),
104            @r###"
105        Ok(
106            HeaderValue(
107                StringTemplate {
108                    parts: [
109                        Expression(
110                            Expression {
111                                expression: JSONSelection {
112                                    inner: Path(
113                                        PathSelection {
114                                            path: WithRange {
115                                                node: Var(
116                                                    WithRange {
117                                                        node: $config,
118                                                        range: Some(
119                                                            0..7,
120                                                        ),
121                                                    },
122                                                    WithRange {
123                                                        node: Key(
124                                                            WithRange {
125                                                                node: Field(
126                                                                    "one",
127                                                                ),
128                                                                range: Some(
129                                                                    8..11,
130                                                                ),
131                                                            },
132                                                            WithRange {
133                                                                node: Empty,
134                                                                range: Some(
135                                                                    11..11,
136                                                                ),
137                                                            },
138                                                        ),
139                                                        range: Some(
140                                                            7..11,
141                                                        ),
142                                                    },
143                                                ),
144                                                range: Some(
145                                                    0..11,
146                                                ),
147                                            },
148                                        },
149                                    ),
150                                    spec: V0_2,
151                                },
152                                location: 1..12,
153                            },
154                        ),
155                    ],
156                },
157            ),
158        )
159        "###
160        );
161    }
162    #[test]
163    fn mixed_constant_and_expression() {
164        assert_debug_snapshot!(
165            HeaderValue::from_str("text{$config.one}text"),
166            @r###"
167        Ok(
168            HeaderValue(
169                StringTemplate {
170                    parts: [
171                        Constant(
172                            Constant {
173                                value: "text",
174                                location: 0..4,
175                            },
176                        ),
177                        Expression(
178                            Expression {
179                                expression: JSONSelection {
180                                    inner: Path(
181                                        PathSelection {
182                                            path: WithRange {
183                                                node: Var(
184                                                    WithRange {
185                                                        node: $config,
186                                                        range: Some(
187                                                            0..7,
188                                                        ),
189                                                    },
190                                                    WithRange {
191                                                        node: Key(
192                                                            WithRange {
193                                                                node: Field(
194                                                                    "one",
195                                                                ),
196                                                                range: Some(
197                                                                    8..11,
198                                                                ),
199                                                            },
200                                                            WithRange {
201                                                                node: Empty,
202                                                                range: Some(
203                                                                    11..11,
204                                                                ),
205                                                            },
206                                                        ),
207                                                        range: Some(
208                                                            7..11,
209                                                        ),
210                                                    },
211                                                ),
212                                                range: Some(
213                                                    0..11,
214                                                ),
215                                            },
216                                        },
217                                    ),
218                                    spec: V0_2,
219                                },
220                                location: 5..16,
221                            },
222                        ),
223                        Constant(
224                            Constant {
225                                value: "text",
226                                location: 17..21,
227                            },
228                        ),
229                    ],
230                },
231            ),
232        )
233        "###
234        );
235    }
236
237    #[test]
238    fn invalid_header_values() {
239        assert_debug_snapshot!(
240            HeaderValue::from_str("\x7f"),
241            @r###"
242        Err(
243            Error {
244                message: "invalid value `\u{7f}`",
245                location: 0..1,
246            },
247        )
248        "###
249        )
250    }
251
252    #[test]
253    fn expressions_with_nested_braces() {
254        assert_debug_snapshot!(
255            HeaderValue::from_str("const{$config.one { two { three } }}another-const"),
256            @r###"
257        Ok(
258            HeaderValue(
259                StringTemplate {
260                    parts: [
261                        Constant(
262                            Constant {
263                                value: "const",
264                                location: 0..5,
265                            },
266                        ),
267                        Expression(
268                            Expression {
269                                expression: JSONSelection {
270                                    inner: Path(
271                                        PathSelection {
272                                            path: WithRange {
273                                                node: Var(
274                                                    WithRange {
275                                                        node: $config,
276                                                        range: Some(
277                                                            0..7,
278                                                        ),
279                                                    },
280                                                    WithRange {
281                                                        node: Key(
282                                                            WithRange {
283                                                                node: Field(
284                                                                    "one",
285                                                                ),
286                                                                range: Some(
287                                                                    8..11,
288                                                                ),
289                                                            },
290                                                            WithRange {
291                                                                node: Selection(
292                                                                    SubSelection {
293                                                                        selections: [
294                                                                            NamedSelection {
295                                                                                prefix: None,
296                                                                                path: PathSelection {
297                                                                                    path: WithRange {
298                                                                                        node: Key(
299                                                                                            WithRange {
300                                                                                                node: Field(
301                                                                                                    "two",
302                                                                                                ),
303                                                                                                range: Some(
304                                                                                                    14..17,
305                                                                                                ),
306                                                                                            },
307                                                                                            WithRange {
308                                                                                                node: Selection(
309                                                                                                    SubSelection {
310                                                                                                        selections: [
311                                                                                                            NamedSelection {
312                                                                                                                prefix: None,
313                                                                                                                path: PathSelection {
314                                                                                                                    path: WithRange {
315                                                                                                                        node: Key(
316                                                                                                                            WithRange {
317                                                                                                                                node: Field(
318                                                                                                                                    "three",
319                                                                                                                                ),
320                                                                                                                                range: Some(
321                                                                                                                                    20..25,
322                                                                                                                                ),
323                                                                                                                            },
324                                                                                                                            WithRange {
325                                                                                                                                node: Empty,
326                                                                                                                                range: Some(
327                                                                                                                                    25..25,
328                                                                                                                                ),
329                                                                                                                            },
330                                                                                                                        ),
331                                                                                                                        range: Some(
332                                                                                                                            20..25,
333                                                                                                                        ),
334                                                                                                                    },
335                                                                                                                },
336                                                                                                            },
337                                                                                                        ],
338                                                                                                        range: Some(
339                                                                                                            18..27,
340                                                                                                        ),
341                                                                                                    },
342                                                                                                ),
343                                                                                                range: Some(
344                                                                                                    18..27,
345                                                                                                ),
346                                                                                            },
347                                                                                        ),
348                                                                                        range: Some(
349                                                                                            14..27,
350                                                                                        ),
351                                                                                    },
352                                                                                },
353                                                                            },
354                                                                        ],
355                                                                        range: Some(
356                                                                            12..29,
357                                                                        ),
358                                                                    },
359                                                                ),
360                                                                range: Some(
361                                                                    12..29,
362                                                                ),
363                                                            },
364                                                        ),
365                                                        range: Some(
366                                                            7..29,
367                                                        ),
368                                                    },
369                                                ),
370                                                range: Some(
371                                                    0..29,
372                                                ),
373                                            },
374                                        },
375                                    ),
376                                    spec: V0_2,
377                                },
378                                location: 6..35,
379                            },
380                        ),
381                        Constant(
382                            Constant {
383                                value: "another-const",
384                                location: 36..49,
385                            },
386                        ),
387                    ],
388                },
389            ),
390        )
391        "###
392        );
393    }
394
395    #[test]
396    fn missing_closing_braces() {
397        assert_debug_snapshot!(
398            HeaderValue::from_str("{$config.one"),
399            @r###"
400        Err(
401            Error {
402                message: "Invalid expression, missing closing }",
403                location: 0..12,
404            },
405        )
406        "###
407        )
408    }
409}
410
411#[cfg(test)]
412mod test_interpolate {
413    use insta::assert_debug_snapshot;
414    use pretty_assertions::assert_eq;
415    use serde_json_bytes::json;
416
417    use super::*;
418    #[test]
419    fn test_interpolate() {
420        let value = HeaderValue::from_str("before {$config.one} after").unwrap();
421        let mut vars = IndexMap::default();
422        vars.insert("$config".to_string(), json!({"one": "foo"}));
423        assert_eq!(
424            value.interpolate(&vars).unwrap().0,
425            http::HeaderValue::from_static("before foo after")
426        );
427    }
428
429    #[test]
430    fn test_interpolate_missing_value() {
431        let value = HeaderValue::from_str("{$config.one}").unwrap();
432        let vars = IndexMap::default();
433        assert_eq!(
434            value.interpolate(&vars).unwrap().0,
435            http::HeaderValue::from_static("")
436        );
437    }
438
439    #[test]
440    fn test_interpolate_value_array() {
441        let header_value = HeaderValue::from_str("{$config.one}").unwrap();
442        let mut vars = IndexMap::default();
443        vars.insert("$config".to_string(), json!({"one": ["one", "two"]}));
444        assert_eq!(
445            header_value.interpolate(&vars),
446            Err("Expression is not allowed to evaluate to arrays or objects.".to_string())
447        );
448    }
449
450    #[test]
451    fn test_interpolate_value_bool() {
452        let header_value = HeaderValue::from_str("{$config.one}").unwrap();
453        let mut vars = IndexMap::default();
454        vars.insert("$config".to_string(), json!({"one": true}));
455        assert_eq!(
456            http::HeaderValue::from_static("true"),
457            header_value.interpolate(&vars).unwrap().0
458        );
459    }
460
461    #[test]
462    fn test_interpolate_value_null() {
463        let header_value = HeaderValue::from_str("{$config.one}").unwrap();
464        let mut vars = IndexMap::default();
465        vars.insert("$config".to_string(), json!({"one": null}));
466        assert_eq!(
467            http::HeaderValue::from_static(""),
468            header_value.interpolate(&vars).unwrap().0
469        );
470    }
471
472    #[test]
473    fn test_interpolate_value_number() {
474        let header_value = HeaderValue::from_str("{$config.one}").unwrap();
475        let mut vars = IndexMap::default();
476        vars.insert("$config".to_string(), json!({"one": 1}));
477        assert_eq!(
478            http::HeaderValue::from_static("1"),
479            header_value.interpolate(&vars).unwrap().0
480        );
481    }
482
483    #[test]
484    fn test_interpolate_value_object() {
485        let header_value = HeaderValue::from_str("{$config.one}").unwrap();
486        let mut vars = IndexMap::default();
487        vars.insert("$config".to_string(), json!({"one": {}}));
488        assert_debug_snapshot!(
489            header_value.interpolate(&vars),
490            @r###"
491        Err(
492            "Expression is not allowed to evaluate to arrays or objects.",
493        )
494        "###
495        );
496    }
497
498    #[test]
499    fn test_interpolate_value_string() {
500        let header_value = HeaderValue::from_str("{$config.one}").unwrap();
501        let mut vars = IndexMap::default();
502        vars.insert("$config".to_string(), json!({"one": "string"}));
503        assert_eq!(
504            http::HeaderValue::from_static("string"),
505            header_value.interpolate(&vars).unwrap().0
506        );
507    }
508}
509
510#[cfg(test)]
511mod test_get_expressions {
512    use super::*;
513
514    #[test]
515    fn test_variable_references() {
516        let value =
517            HeaderValue::from_str("a {$this.a.b.c} b {$args.a.b.c} c {$config.a.b.c}").unwrap();
518        let references: Vec<_> = value
519            .expressions()
520            .map(|e| e.expression.to_string())
521            .collect();
522        assert_eq!(
523            references,
524            vec!["$this.a.b.c", "$args.a.b.c", "$config.a.b.c"]
525        );
526    }
527}