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}