1use std::fmt::Display;
2use std::fmt::Formatter;
3use std::fmt::Write;
4use std::iter::once;
5use std::str::FromStr;
6
7use apollo_compiler::collections::IndexMap;
8use either::Either;
9use http::Uri;
10use http::uri::InvalidUri;
11use http::uri::InvalidUriParts;
12use http::uri::Parts;
13use http::uri::PathAndQuery;
14use serde_json_bytes::Value;
15use serde_json_bytes::json;
16use thiserror::Error;
17
18use super::ProblemLocation;
19use crate::connectors::ApplyToError;
20use crate::connectors::ConnectSpec;
21use crate::connectors::JSONSelection;
22use crate::connectors::Namespace;
23use crate::connectors::PathSelection;
24use crate::connectors::StringTemplate;
25use crate::connectors::json_selection::VarPaths;
26use crate::connectors::models::Header;
27use crate::connectors::spec::ConnectHTTPArguments;
28use crate::connectors::spec::SourceHTTPArguments;
29use crate::connectors::string_template;
30use crate::connectors::string_template::UriString;
31use crate::connectors::string_template::write_value;
32use crate::connectors::variable::VariableReference;
33use crate::error::FederationError;
34
35#[derive(Clone, Debug, Default)]
36pub struct HttpJsonTransport {
37 pub source_template: Option<StringTemplate>,
38 pub connect_template: StringTemplate,
39 pub method: HTTPMethod,
40 pub headers: Vec<Header>,
41 pub body: Option<JSONSelection>,
42 pub source_path: Option<JSONSelection>,
43 pub source_query_params: Option<JSONSelection>,
44 pub connect_path: Option<JSONSelection>,
45 pub connect_query_params: Option<JSONSelection>,
46}
47
48impl HttpJsonTransport {
49 pub fn from_directive(
50 http: ConnectHTTPArguments,
51 source: Option<&SourceHTTPArguments>,
52 spec: ConnectSpec,
53 ) -> Result<Self, FederationError> {
54 let (method, connect_url) = if let Some(url) = &http.get {
55 (HTTPMethod::Get, url)
56 } else if let Some(url) = &http.post {
57 (HTTPMethod::Post, url)
58 } else if let Some(url) = &http.patch {
59 (HTTPMethod::Patch, url)
60 } else if let Some(url) = &http.put {
61 (HTTPMethod::Put, url)
62 } else if let Some(url) = &http.delete {
63 (HTTPMethod::Delete, url)
64 } else {
65 return Err(FederationError::internal("missing http method"));
66 };
67
68 let mut headers = http.headers;
69 for header in source.map(|source| &source.headers).into_iter().flatten() {
70 if !headers
71 .iter()
72 .any(|connect_header| connect_header.name == header.name)
73 {
74 headers.push(header.clone());
75 }
76 }
77
78 Ok(Self {
79 source_template: source.map(|source| source.base_url.template.clone()),
80 connect_template: StringTemplate::parse_with_spec(connect_url, spec).map_err(
81 |e: string_template::Error| {
82 FederationError::internal(format!(
83 "could not parse URL template: {message}",
84 message = e.message
85 ))
86 },
87 )?,
88 method,
89 headers,
90 body: http.body,
91 source_path: source.and_then(|s| s.path.clone()),
92 source_query_params: source.and_then(|s| s.query_params.clone()),
93 connect_path: http.path,
94 connect_query_params: http.query_params,
95 })
96 }
97
98 pub(super) fn label(&self) -> String {
99 format!("http: {} {}", self.method, self.connect_template)
100 }
101
102 pub(crate) fn variable_references(&self) -> impl Iterator<Item = VariableReference<Namespace>> {
103 let url_selections = self.connect_template.expressions().map(|e| &e.expression);
104 let header_selections = self
105 .headers
106 .iter()
107 .flat_map(|header| header.source.expressions());
108
109 let source_selections = self
110 .source_template
111 .iter()
112 .flat_map(|template| template.expressions().map(|e| &e.expression));
113
114 url_selections
115 .chain(header_selections)
116 .chain(source_selections)
117 .chain(self.body.iter())
118 .chain(self.source_path.iter())
119 .chain(self.source_query_params.iter())
120 .chain(self.connect_path.iter())
121 .chain(self.connect_query_params.iter())
122 .flat_map(|b| {
123 b.external_var_paths()
124 .into_iter()
125 .flat_map(PathSelection::variable_reference)
126 })
127 }
128
129 pub fn make_uri(
130 &self,
131 inputs: &IndexMap<String, Value>,
132 ) -> Result<(Uri, Vec<(ProblemLocation, ApplyToError)>), MakeUriError> {
133 let mut uri_parts = Parts::default();
134 let mut warnings = Vec::new();
135
136 let (connect_uri, connect_template_warnings) =
137 self.connect_template.interpolate_uri(inputs)?;
138 warnings.extend(
139 connect_template_warnings
140 .into_iter()
141 .map(|warning| (ProblemLocation::ConnectUrl, warning)),
142 );
143 let resolved_source_uri = match &self.source_template {
144 Some(template) => {
145 let (uri, source_template_warnings) = template.interpolate_uri(inputs)?;
146 warnings.extend(
147 source_template_warnings
148 .into_iter()
149 .map(|warning| (ProblemLocation::SourceUrl, warning)),
150 );
151 Some(uri)
152 }
153 None => None,
154 };
155
156 if let Some(source_uri) = &resolved_source_uri {
157 uri_parts.scheme = source_uri.scheme().cloned();
158 uri_parts.authority = source_uri.authority().cloned();
159 } else {
160 uri_parts.scheme = connect_uri.scheme().cloned();
161 uri_parts.authority = connect_uri.authority().cloned();
162 }
163
164 let mut path = UriString::new();
165 if let Some(source_uri) = &resolved_source_uri {
166 path.write_without_encoding(source_uri.path())?;
167 }
168 if let Some(source_path) = self.source_path.as_ref() {
169 warnings.extend(
170 extend_path_from_expression(&mut path, source_path, inputs)?
171 .into_iter()
172 .map(|error| (ProblemLocation::SourcePath, error)),
173 );
174 }
175 let connect_path = connect_uri.path();
176 if !connect_path.is_empty() && connect_path != "/" {
177 if path.ends_with('/') {
178 path.write_without_encoding(connect_path.trim_start_matches('/'))?;
179 } else if connect_path.starts_with('/') {
180 path.write_without_encoding(connect_path)?;
181 } else {
182 path.write_without_encoding("/")?;
183 path.write_without_encoding(connect_path)?;
184 };
185 }
186 if let Some(connect_path) = self.connect_path.as_ref() {
187 warnings.extend(
188 extend_path_from_expression(&mut path, connect_path, inputs)?
189 .into_iter()
190 .map(|error| (ProblemLocation::ConnectPath, error)),
191 );
192 }
193
194 let mut query = UriString::new();
195 if let Some(source_uri_query) = resolved_source_uri
196 .as_ref()
197 .and_then(|source_uri| source_uri.query())
198 {
199 query.write_without_encoding(source_uri_query)?;
200 }
201 if let Some(source_query) = self.source_query_params.as_ref() {
202 warnings.extend(
203 extend_query_from_expression(&mut query, source_query, inputs)?
204 .into_iter()
205 .map(|error| (ProblemLocation::SourceQueryParams, error)),
206 );
207 }
208 let connect_query = connect_uri.query().unwrap_or_default();
209 if !connect_query.is_empty() {
210 if !query.is_empty() && !query.ends_with('&') {
211 query.write_without_encoding("&")?;
212 }
213 query.write_without_encoding(connect_query)?;
214 }
215 if let Some(connect_query) = self.connect_query_params.as_ref() {
216 warnings.extend(
217 extend_query_from_expression(&mut query, connect_query, inputs)?
218 .into_iter()
219 .map(|error| (ProblemLocation::ConnectQueryParams, error)),
220 );
221 }
222
223 let path = path.into_string();
224 let query = query.into_string();
225
226 uri_parts.path_and_query = Some(match (path.is_empty(), query.is_empty()) {
227 (true, true) => PathAndQuery::from_static(""),
228 (true, false) => PathAndQuery::try_from(format!("?{query}"))?,
229 (false, true) => PathAndQuery::try_from(path)?,
230 (false, false) => PathAndQuery::try_from(format!("{path}?{query}"))?,
231 });
232
233 let uri = Uri::from_parts(uri_parts).map_err(MakeUriError::BuildMergedUri)?;
234
235 Ok((uri, warnings))
236 }
237}
238
239fn extend_path_from_expression(
242 path: &mut UriString,
243 expression: &JSONSelection,
244 inputs: &IndexMap<String, Value>,
245) -> Result<Vec<ApplyToError>, MakeUriError> {
246 let (value, warnings) = expression.apply_with_vars(&json!({}), inputs);
247 let Some(value) = value else {
248 return Ok(warnings);
249 };
250 let Value::Array(values) = value else {
251 return Err(MakeUriError::PathComponents(
252 "Expression did not evaluate to an array".into(),
253 ));
254 };
255 for value in &values {
256 if !path.ends_with('/') {
257 path.write_trusted("/")?;
258 }
259 write_value(&mut *path, value)
260 .map_err(|err| MakeUriError::PathComponents(err.to_string()))?;
261 }
262 Ok(warnings)
263}
264
265fn extend_query_from_expression(
266 query: &mut UriString,
267 expression: &JSONSelection,
268 inputs: &IndexMap<String, Value>,
269) -> Result<Vec<ApplyToError>, MakeUriError> {
270 let (value, warnings) = expression.apply_with_vars(&json!({}), inputs);
271 let Some(value) = value else {
272 return Ok(warnings);
273 };
274 let Value::Object(map) = value else {
275 return Err(MakeUriError::QueryParams(
276 "Expression did not evaluate to an object".into(),
277 ));
278 };
279
280 let all_params = map
281 .iter()
282 .filter(|(_, value)| !value.is_null())
283 .flat_map(|(key, value)| {
284 if let Value::Array(values) = value {
285 Either::Left(values.iter().map(|value| (key.as_str(), value)))
287 } else {
288 Either::Right(once((key.as_str(), value)))
289 }
290 });
291
292 for (key, value) in all_params {
293 if !query.is_empty() && !query.ends_with('&') {
294 query.write_trusted("&")?;
295 }
296 query.write_str(key)?;
297 query.write_trusted("=")?;
298 write_value(&mut *query, value)
299 .map_err(|err| MakeUriError::QueryParams(err.to_string()))?;
300 }
301 Ok(warnings)
302}
303
304#[derive(Debug, Error)]
305pub enum MakeUriError {
306 #[error("Error building URI: {0}")]
307 ParsePathAndQuery(#[from] InvalidUri),
308 #[error("Error building URI: {0}")]
309 BuildMergedUri(InvalidUriParts),
310 #[error("Error rendering URI template: {0}")]
311 TemplateGenerationError(#[from] string_template::Error),
312 #[error("Internal error building URI")]
313 WriteError(#[from] std::fmt::Error),
314 #[error("Error building path components from expression: {0}")]
315 PathComponents(String),
316 #[error("Error building query parameters from queryParams: {0}")]
317 QueryParams(String),
318}
319
320#[derive(Debug, Clone, Copy, Default)]
322pub enum HTTPMethod {
323 #[default]
324 Get,
325 Post,
326 Patch,
327 Put,
328 Delete,
329}
330
331impl HTTPMethod {
332 #[inline]
333 pub const fn as_str(&self) -> &str {
334 match self {
335 HTTPMethod::Get => "GET",
336 HTTPMethod::Post => "POST",
337 HTTPMethod::Patch => "PATCH",
338 HTTPMethod::Put => "PUT",
339 HTTPMethod::Delete => "DELETE",
340 }
341 }
342}
343
344impl FromStr for HTTPMethod {
345 type Err = String;
346
347 fn from_str(s: &str) -> Result<Self, Self::Err> {
348 match s.to_uppercase().as_str() {
349 "GET" => Ok(HTTPMethod::Get),
350 "POST" => Ok(HTTPMethod::Post),
351 "PATCH" => Ok(HTTPMethod::Patch),
352 "PUT" => Ok(HTTPMethod::Put),
353 "DELETE" => Ok(HTTPMethod::Delete),
354 _ => Err(format!("Invalid HTTP method: {s}")),
355 }
356 }
357}
358
359impl Display for HTTPMethod {
360 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
361 write!(f, "{}", self.as_str())
362 }
363}
364
365#[cfg(test)]
366mod test_make_uri {
367 use std::str::FromStr;
368
369 use apollo_compiler::collections::IndexMap;
370 use pretty_assertions::assert_eq;
371 use serde_json_bytes::json;
372
373 use super::*;
374 use crate::connectors::JSONSelection;
375
376 #[test]
378 fn merge_all_sources() {
379 let transport = HttpJsonTransport {
380 source_template: StringTemplate::from_str(
381 "http://example.com/sourceUri?shared=sourceUri&sourceUri=sourceUri",
382 )
383 .ok(),
384 connect_template: StringTemplate::parse_with_spec(
385 "/{$args.connectUri}?shared={$args.connectUri}&{$args.connectUri}={$args.connectUri}",
386 ConnectSpec::latest(),
387 )
388 .unwrap(),
389 source_path: JSONSelection::parse("$args.sourcePath").ok(),
390 connect_path: JSONSelection::parse("$args.connectPath").ok(),
391 source_query_params: JSONSelection::parse("$args.sourceQuery").ok(),
392 connect_query_params: JSONSelection::parse("$args.connectQuery").ok(),
393 ..Default::default()
394 };
395 let inputs = IndexMap::from_iter([(
396 "$args".to_string(),
397 json!({
398 "connectUri": "connectUri",
399 "sourcePath": ["sourcePath1", "sourcePath2"],
400 "connectPath": ["connectPath1", "connectPath2"],
401 "sourceQuery": {"shared": "sourceQuery", "sourceQuery": "sourceQuery"},
402 "connectQuery": {"shared": "connectQuery", "connectQuery": "connectQuery"},
403 }),
404 )]);
405 let (url, _) = transport.make_uri(&inputs).unwrap();
406 assert_eq!(
407 url.to_string(),
408 "http://example.com/sourceUri/sourcePath1/sourcePath2/connectUri/connectPath1/connectPath2\
409 ?shared=sourceUri&sourceUri=sourceUri\
410 &shared=sourceQuery&sourceQuery=sourceQuery\
411 &shared=connectUri&connectUri=connectUri\
412 &shared=connectQuery&connectQuery=connectQuery"
413 );
414 }
415
416 macro_rules! this {
417 ($($value:tt)*) => {{
418 let mut map = IndexMap::with_capacity_and_hasher(1, Default::default());
419 map.insert("$this".to_string(), serde_json_bytes::json!({ $($value)* }));
420 map
421 }};
422 }
423
424 mod combining_paths {
425 use pretty_assertions::assert_eq;
426 use rstest::rstest;
427
428 use super::*;
429
430 #[rstest]
431 #[case::connect_only("https://localhost:8080/v1", "/hello")]
432 #[case::source_only("https://localhost:8080/v1/", "hello")]
433 #[case::neither("https://localhost:8080/v1", "hello")]
434 #[case::both("https://localhost:8080/v1/", "/hello")]
435 fn slashes_between_source_and_connect(
436 #[case] source_uri: &str,
437 #[case] connect_path: &str,
438 ) {
439 let transport = HttpJsonTransport {
440 source_template: StringTemplate::from_str(source_uri).ok(),
441 connect_template: connect_path.parse().unwrap(),
442 ..Default::default()
443 };
444 assert_eq!(
445 transport
446 .make_uri(&Default::default())
447 .unwrap()
448 .0
449 .to_string(),
450 "https://localhost:8080/v1/hello"
451 );
452 }
453
454 #[rstest]
455 #[case::when_base_has_trailing("http://localhost/")]
456 #[case::when_base_does_not_have_trailing("http://localhost")]
457 fn handle_slashes_when_adding_path_expression(#[case] base: &str) {
458 let transport = HttpJsonTransport {
459 source_template: StringTemplate::from_str(base).ok(),
460 source_path: JSONSelection::parse("$([1, 2])").ok(),
461 ..Default::default()
462 };
463 assert_eq!(
464 transport
465 .make_uri(&Default::default())
466 .unwrap()
467 .0
468 .to_string(),
469 "http://localhost/1/2"
470 );
471 }
472
473 #[test]
474 fn preserve_trailing_slash_from_connect() {
475 let transport = HttpJsonTransport {
476 source_template: StringTemplate::from_str("https://localhost:8080/v1").ok(),
477 connect_template: "/hello/".parse().unwrap(),
478 ..Default::default()
479 };
480 assert_eq!(
481 transport
482 .make_uri(&Default::default())
483 .unwrap()
484 .0
485 .to_string(),
486 "https://localhost:8080/v1/hello/"
487 );
488 }
489
490 #[test]
491 fn preserve_trailing_slash_from_source() {
492 let transport = HttpJsonTransport {
493 source_template: StringTemplate::from_str("https://localhost:8080/v1/").ok(),
494 connect_template: "/".parse().unwrap(),
495 ..Default::default()
496 };
497 assert_eq!(
498 transport
499 .make_uri(&Default::default())
500 .unwrap()
501 .0
502 .to_string(),
503 "https://localhost:8080/v1/"
504 );
505 }
506
507 #[test]
508 fn preserve_no_trailing_slash_from_source() {
509 let transport = HttpJsonTransport {
510 source_template: StringTemplate::from_str("https://localhost:8080/v1").ok(),
511 connect_template: "/".parse().unwrap(),
512 ..Default::default()
513 };
514 assert_eq!(
515 transport
516 .make_uri(&Default::default())
517 .unwrap()
518 .0
519 .to_string(),
520 "https://localhost:8080/v1"
521 );
522 }
523
524 #[test]
525 fn add_path_before_query_params() {
526 let transport = HttpJsonTransport {
527 source_template: StringTemplate::from_str("https://localhost:8080/v1?something")
528 .ok(),
529 connect_template: "/hello".parse().unwrap(),
530 ..Default::default()
531 };
532 assert_eq!(
533 transport
534 .make_uri(&this! { "id": 42 })
535 .unwrap()
536 .0
537 .to_string(),
538 "https://localhost:8080/v1/hello?something"
539 );
540 }
541
542 #[test]
543 fn trailing_slash_plus_query_params() {
544 let transport = HttpJsonTransport {
545 source_template: StringTemplate::from_str("https://localhost:8080/v1/?something")
546 .ok(),
547 connect_template: "/hello/".parse().unwrap(),
548 ..Default::default()
549 };
550 assert_eq!(
551 transport
552 .make_uri(&this! { "id": 42 })
553 .unwrap()
554 .0
555 .to_string(),
556 "https://localhost:8080/v1/hello/?something"
557 );
558 }
559
560 #[test]
561 fn with_trailing_slash_in_base_plus_query_params() {
562 let transport = HttpJsonTransport {
563 source_template: StringTemplate::from_str("https://localhost:8080/v1/?foo=bar")
564 .ok(),
565 connect_template: StringTemplate::parse_with_spec(
566 "/hello/{$this.id}?id={$this.id}",
567 ConnectSpec::latest(),
568 )
569 .unwrap(),
570 ..Default::default()
571 };
572 assert_eq!(
573 transport
574 .make_uri(&this! {"id": 42 })
575 .unwrap()
576 .0
577 .to_string(),
578 "https://localhost:8080/v1/hello/42?foo=bar&id=42"
579 );
580 }
581 }
582
583 mod merge_query {
584 use pretty_assertions::assert_eq;
585
586 use super::*;
587 #[test]
588 fn source_only() {
589 let transport = HttpJsonTransport {
590 source_template: StringTemplate::from_str("http://localhost/users?a=b").ok(),
591 connect_template: "/123".parse().unwrap(),
592 ..Default::default()
593 };
594 assert_eq!(
595 transport.make_uri(&Default::default()).unwrap().0,
596 "http://localhost/users/123?a=b"
597 );
598 }
599
600 #[test]
601 fn connect_only() {
602 let transport = HttpJsonTransport {
603 source_template: StringTemplate::from_str("http://localhost/users").ok(),
604 connect_template: "?a=b&c=d".parse().unwrap(),
605 ..Default::default()
606 };
607 assert_eq!(
608 transport.make_uri(&Default::default()).unwrap().0,
609 "http://localhost/users?a=b&c=d"
610 )
611 }
612
613 #[test]
614 fn combine_from_both_uris() {
615 let transport = HttpJsonTransport {
616 source_template: StringTemplate::from_str("http://localhost/users?a=b").ok(),
617 connect_template: "?c=d".parse().unwrap(),
618 ..Default::default()
619 };
620 assert_eq!(
621 transport.make_uri(&Default::default()).unwrap().0,
622 "http://localhost/users?a=b&c=d"
623 )
624 }
625
626 #[test]
627 fn source_and_connect_have_same_param() {
628 let transport = HttpJsonTransport {
629 source_template: StringTemplate::from_str("http://localhost/users?a=b").ok(),
630 connect_template: "?a=d".parse().unwrap(),
631 ..Default::default()
632 };
633 assert_eq!(
634 transport.make_uri(&Default::default()).unwrap().0,
635 "http://localhost/users?a=b&a=d"
636 )
637 }
638
639 #[test]
640 fn repeated_params_from_array() {
641 let transport = HttpJsonTransport {
642 connect_template: "http://localhost".parse().unwrap(),
643 connect_query_params: JSONSelection::parse("$args.connectQuery").ok(),
644 ..Default::default()
645 };
646 let inputs = IndexMap::from_iter([(
647 "$args".to_string(),
648 json!({
649 "connectQuery": {"multi": ["first", "second"]},
650 }),
651 )]);
652 assert_eq!(
653 transport.make_uri(&inputs).unwrap().0,
654 "http://localhost?multi=first&multi=second"
655 )
656 }
657 }
658
659 #[test]
660 fn fragments_are_dropped() {
661 let transport = HttpJsonTransport {
662 source_template: StringTemplate::from_str("http://localhost/source?a=b#SourceFragment")
663 .ok(),
664 connect_template: "/connect?c=d#connectFragment".parse().unwrap(),
665 ..Default::default()
666 };
667 assert_eq!(
668 transport.make_uri(&Default::default()).unwrap().0,
669 "http://localhost/source/connect?a=b&c=d"
670 )
671 }
672
673 #[test]
676 fn pieces_are_not_double_encoded() {
677 let transport = HttpJsonTransport {
678 source_template: StringTemplate::from_str(
679 "http://localhost/source%20path?param=source%20param",
680 )
681 .ok(),
682 connect_template: "/connect%20path?param=connect%20param".parse().unwrap(),
683 ..Default::default()
684 };
685 assert_eq!(
686 transport.make_uri(&Default::default()).unwrap().0,
687 "http://localhost/source%20path/connect%20path?param=source%20param¶m=connect%20param"
688 )
689 }
690
691 #[test]
694 fn empty_path_and_query() {
695 let transport = HttpJsonTransport {
696 source_template: None,
697 connect_template: "http://localhost/".parse().unwrap(),
698 ..Default::default()
699 };
700 assert_eq!(
701 transport.make_uri(&Default::default()).unwrap().0,
702 "http://localhost/"
703 )
704 }
705
706 #[test]
707 fn skip_null_query_params() {
708 let transport = HttpJsonTransport {
709 source_template: None,
710 connect_template: "http://localhost/".parse().unwrap(),
711 connect_query_params: JSONSelection::parse("something: $(null)").ok(),
712 ..Default::default()
713 };
714
715 assert_eq!(
716 transport.make_uri(&Default::default()).unwrap().0,
717 "http://localhost/"
718 )
719 }
720
721 #[test]
722 fn skip_null_path_params() {
723 let transport = HttpJsonTransport {
724 source_template: None,
725 connect_template: "http://localhost/".parse().unwrap(),
726 connect_path: JSONSelection::parse("$([1, null, 2])").ok(),
727 ..Default::default()
728 };
729
730 assert_eq!(
731 transport.make_uri(&Default::default()).unwrap().0,
732 "http://localhost/1/2"
733 )
734 }
735
736 #[test]
737 fn source_template_variables_retained() {
738 let transport = HttpJsonTransport {
739 source_template: StringTemplate::parse_with_spec(
740 "http://${$config.subdomain}.localhost",
741 ConnectSpec::latest(),
742 )
743 .ok(),
744 connect_template: "/connect?c=d".parse().unwrap(),
745 ..Default::default()
746 };
747
748 transport
750 .variable_references()
751 .find(|var_ref| var_ref.namespace.namespace == Namespace::Config)
752 .unwrap();
753 }
754
755 #[test]
756 fn source_template_interpolated_correctly() {
757 let transport = HttpJsonTransport {
758 source_template: StringTemplate::parse_with_spec(
759 "http://{$config.subdomain}.localhost:{$config.port}",
760 ConnectSpec::latest(),
761 )
762 .ok(),
763 connect_template: "/connect?c=d".parse().unwrap(),
764 ..Default::default()
765 };
766 let mut vars: IndexMap<String, Value> = Default::default();
767 vars.insert(
768 "$config".to_string(),
769 json!({ "subdomain": "api", "port": 5000 }),
770 );
771 assert_eq!(
772 transport.make_uri(&vars).unwrap().0,
773 "http://api.localhost:5000/connect?c=d"
774 );
775 }
776}