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}