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: Value(
113                                        WithRange {
114                                            node: Path(
115                                                PathSelection {
116                                                    path: WithRange {
117                                                        node: Var(
118                                                            WithRange {
119                                                                node: $config,
120                                                                range: Some(
121                                                                    0..7,
122                                                                ),
123                                                            },
124                                                            WithRange {
125                                                                node: Key(
126                                                                    WithRange {
127                                                                        node: Field(
128                                                                            "one",
129                                                                        ),
130                                                                        range: Some(
131                                                                            8..11,
132                                                                        ),
133                                                                    },
134                                                                    WithRange {
135                                                                        node: Empty,
136                                                                        range: Some(
137                                                                            11..11,
138                                                                        ),
139                                                                    },
140                                                                ),
141                                                                range: Some(
142                                                                    7..11,
143                                                                ),
144                                                            },
145                                                        ),
146                                                        range: Some(
147                                                            0..11,
148                                                        ),
149                                                    },
150                                                },
151                                            ),
152                                            range: Some(
153                                                0..11,
154                                            ),
155                                        },
156                                    ),
157                                    spec: V0_3,
158                                },
159                                location: 1..12,
160                            },
161                        ),
162                    ],
163                },
164            ),
165        )
166        "#
167        );
168    }
169    #[test]
170    fn mixed_constant_and_expression() {
171        assert_debug_snapshot!(
172            HeaderValue::from_str("text{$config.one}text"),
173            @r#"
174        Ok(
175            HeaderValue(
176                StringTemplate {
177                    parts: [
178                        Constant(
179                            Constant {
180                                value: "text",
181                                location: 0..4,
182                            },
183                        ),
184                        Expression(
185                            Expression {
186                                expression: JSONSelection {
187                                    inner: Value(
188                                        WithRange {
189                                            node: Path(
190                                                PathSelection {
191                                                    path: WithRange {
192                                                        node: Var(
193                                                            WithRange {
194                                                                node: $config,
195                                                                range: Some(
196                                                                    0..7,
197                                                                ),
198                                                            },
199                                                            WithRange {
200                                                                node: Key(
201                                                                    WithRange {
202                                                                        node: Field(
203                                                                            "one",
204                                                                        ),
205                                                                        range: Some(
206                                                                            8..11,
207                                                                        ),
208                                                                    },
209                                                                    WithRange {
210                                                                        node: Empty,
211                                                                        range: Some(
212                                                                            11..11,
213                                                                        ),
214                                                                    },
215                                                                ),
216                                                                range: Some(
217                                                                    7..11,
218                                                                ),
219                                                            },
220                                                        ),
221                                                        range: Some(
222                                                            0..11,
223                                                        ),
224                                                    },
225                                                },
226                                            ),
227                                            range: Some(
228                                                0..11,
229                                            ),
230                                        },
231                                    ),
232                                    spec: V0_3,
233                                },
234                                location: 5..16,
235                            },
236                        ),
237                        Constant(
238                            Constant {
239                                value: "text",
240                                location: 17..21,
241                            },
242                        ),
243                    ],
244                },
245            ),
246        )
247        "#
248        );
249    }
250
251    #[test]
252    fn invalid_header_values() {
253        assert_debug_snapshot!(
254            HeaderValue::from_str("\x7f"),
255            @r###"
256        Err(
257            Error {
258                message: "invalid value `\u{7f}`",
259                location: 0..1,
260            },
261        )
262        "###
263        )
264    }
265
266    #[test]
267    fn expressions_with_nested_braces() {
268        assert_debug_snapshot!(
269            HeaderValue::from_str("const{$config.one { two { three } }}another-const"),
270            @r#"
271        Ok(
272            HeaderValue(
273                StringTemplate {
274                    parts: [
275                        Constant(
276                            Constant {
277                                value: "const",
278                                location: 0..5,
279                            },
280                        ),
281                        Expression(
282                            Expression {
283                                expression: JSONSelection {
284                                    inner: Value(
285                                        WithRange {
286                                            node: Path(
287                                                PathSelection {
288                                                    path: WithRange {
289                                                        node: Var(
290                                                            WithRange {
291                                                                node: $config,
292                                                                range: Some(
293                                                                    0..7,
294                                                                ),
295                                                            },
296                                                            WithRange {
297                                                                node: Key(
298                                                                    WithRange {
299                                                                        node: Field(
300                                                                            "one",
301                                                                        ),
302                                                                        range: Some(
303                                                                            8..11,
304                                                                        ),
305                                                                    },
306                                                                    WithRange {
307                                                                        node: Selection(
308                                                                            SubSelection {
309                                                                                selections: [
310                                                                                    NamedSelection {
311                                                                                        prefix: None,
312                                                                                        path: WithRange {
313                                                                                            node: Path(
314                                                                                                PathSelection {
315                                                                                                    path: WithRange {
316                                                                                                        node: Key(
317                                                                                                            WithRange {
318                                                                                                                node: Field(
319                                                                                                                    "two",
320                                                                                                                ),
321                                                                                                                range: Some(
322                                                                                                                    14..17,
323                                                                                                                ),
324                                                                                                            },
325                                                                                                            WithRange {
326                                                                                                                node: Selection(
327                                                                                                                    SubSelection {
328                                                                                                                        selections: [
329                                                                                                                            NamedSelection {
330                                                                                                                                prefix: None,
331                                                                                                                                path: WithRange {
332                                                                                                                                    node: Path(
333                                                                                                                                        PathSelection {
334                                                                                                                                            path: WithRange {
335                                                                                                                                                node: Key(
336                                                                                                                                                    WithRange {
337                                                                                                                                                        node: Field(
338                                                                                                                                                            "three",
339                                                                                                                                                        ),
340                                                                                                                                                        range: Some(
341                                                                                                                                                            20..25,
342                                                                                                                                                        ),
343                                                                                                                                                    },
344                                                                                                                                                    WithRange {
345                                                                                                                                                        node: Empty,
346                                                                                                                                                        range: Some(
347                                                                                                                                                            25..25,
348                                                                                                                                                        ),
349                                                                                                                                                    },
350                                                                                                                                                ),
351                                                                                                                                                range: Some(
352                                                                                                                                                    20..25,
353                                                                                                                                                ),
354                                                                                                                                            },
355                                                                                                                                        },
356                                                                                                                                    ),
357                                                                                                                                    range: Some(
358                                                                                                                                        20..25,
359                                                                                                                                    ),
360                                                                                                                                },
361                                                                                                                            },
362                                                                                                                        ],
363                                                                                                                        range: Some(
364                                                                                                                            18..27,
365                                                                                                                        ),
366                                                                                                                    },
367                                                                                                                ),
368                                                                                                                range: Some(
369                                                                                                                    18..27,
370                                                                                                                ),
371                                                                                                            },
372                                                                                                        ),
373                                                                                                        range: Some(
374                                                                                                            14..27,
375                                                                                                        ),
376                                                                                                    },
377                                                                                                },
378                                                                                            ),
379                                                                                            range: Some(
380                                                                                                14..27,
381                                                                                            ),
382                                                                                        },
383                                                                                    },
384                                                                                ],
385                                                                                range: Some(
386                                                                                    12..29,
387                                                                                ),
388                                                                            },
389                                                                        ),
390                                                                        range: Some(
391                                                                            12..29,
392                                                                        ),
393                                                                    },
394                                                                ),
395                                                                range: Some(
396                                                                    7..29,
397                                                                ),
398                                                            },
399                                                        ),
400                                                        range: Some(
401                                                            0..29,
402                                                        ),
403                                                    },
404                                                },
405                                            ),
406                                            range: Some(
407                                                0..29,
408                                            ),
409                                        },
410                                    ),
411                                    spec: V0_3,
412                                },
413                                location: 6..35,
414                            },
415                        ),
416                        Constant(
417                            Constant {
418                                value: "another-const",
419                                location: 36..49,
420                            },
421                        ),
422                    ],
423                },
424            ),
425        )
426        "#
427        );
428    }
429
430    #[test]
431    fn missing_closing_braces() {
432        assert_debug_snapshot!(
433            HeaderValue::from_str("{$config.one"),
434            @r###"
435        Err(
436            Error {
437                message: "Invalid expression, missing closing }",
438                location: 0..12,
439            },
440        )
441        "###
442        )
443    }
444}
445
446#[cfg(test)]
447mod test_interpolate {
448    use insta::assert_debug_snapshot;
449    use pretty_assertions::assert_eq;
450    use serde_json_bytes::json;
451
452    use super::*;
453    #[test]
454    fn test_interpolate() {
455        let value = HeaderValue::from_str("before {$config.one} after").unwrap();
456        let mut vars = IndexMap::default();
457        vars.insert("$config".to_string(), json!({"one": "foo"}));
458        assert_eq!(
459            value.interpolate(&vars).unwrap().0,
460            http::HeaderValue::from_static("before foo after")
461        );
462    }
463
464    #[test]
465    fn test_interpolate_missing_value() {
466        let value = HeaderValue::from_str("{$config.one}").unwrap();
467        let vars = IndexMap::default();
468        assert_eq!(
469            value.interpolate(&vars).unwrap().0,
470            http::HeaderValue::from_static("")
471        );
472    }
473
474    #[test]
475    fn test_interpolate_value_array() {
476        let header_value = HeaderValue::from_str("{$config.one}").unwrap();
477        let mut vars = IndexMap::default();
478        vars.insert("$config".to_string(), json!({"one": ["one", "two"]}));
479        assert_eq!(
480            header_value.interpolate(&vars),
481            Err("Expression is not allowed to evaluate to arrays or objects.".to_string())
482        );
483    }
484
485    #[test]
486    fn test_interpolate_value_bool() {
487        let header_value = HeaderValue::from_str("{$config.one}").unwrap();
488        let mut vars = IndexMap::default();
489        vars.insert("$config".to_string(), json!({"one": true}));
490        assert_eq!(
491            http::HeaderValue::from_static("true"),
492            header_value.interpolate(&vars).unwrap().0
493        );
494    }
495
496    #[test]
497    fn test_interpolate_value_null() {
498        let header_value = HeaderValue::from_str("{$config.one}").unwrap();
499        let mut vars = IndexMap::default();
500        vars.insert("$config".to_string(), json!({"one": null}));
501        assert_eq!(
502            http::HeaderValue::from_static(""),
503            header_value.interpolate(&vars).unwrap().0
504        );
505    }
506
507    #[test]
508    fn test_interpolate_value_number() {
509        let header_value = HeaderValue::from_str("{$config.one}").unwrap();
510        let mut vars = IndexMap::default();
511        vars.insert("$config".to_string(), json!({"one": 1}));
512        assert_eq!(
513            http::HeaderValue::from_static("1"),
514            header_value.interpolate(&vars).unwrap().0
515        );
516    }
517
518    #[test]
519    fn test_interpolate_value_object() {
520        let header_value = HeaderValue::from_str("{$config.one}").unwrap();
521        let mut vars = IndexMap::default();
522        vars.insert("$config".to_string(), json!({"one": {}}));
523        assert_debug_snapshot!(
524            header_value.interpolate(&vars),
525            @r###"
526        Err(
527            "Expression is not allowed to evaluate to arrays or objects.",
528        )
529        "###
530        );
531    }
532
533    #[test]
534    fn test_interpolate_value_string() {
535        let header_value = HeaderValue::from_str("{$config.one}").unwrap();
536        let mut vars = IndexMap::default();
537        vars.insert("$config".to_string(), json!({"one": "string"}));
538        assert_eq!(
539            http::HeaderValue::from_static("string"),
540            header_value.interpolate(&vars).unwrap().0
541        );
542    }
543}
544
545#[cfg(test)]
546mod test_get_expressions {
547    use super::*;
548
549    #[test]
550    fn test_variable_references() {
551        let value =
552            HeaderValue::from_str("a {$this.a.b.c} b {$args.a.b.c} c {$config.a.b.c}").unwrap();
553        let references: Vec<_> = value
554            .expressions()
555            .map(|e| e.expression.to_string())
556            .collect();
557        assert_eq!(
558            references,
559            vec!["$this.a.b.c", "$args.a.b.c", "$config.a.b.c"]
560        );
561    }
562}