apollo_federation/connectors/spec/
source.rs1use apollo_compiler::Name;
2use apollo_compiler::Node;
3use apollo_compiler::Schema;
4use apollo_compiler::ast::Value;
5use apollo_compiler::name;
6use apollo_compiler::parser::SourceMap;
7use apollo_compiler::schema::Component;
8use apollo_compiler::schema::Directive;
9use itertools::Itertools;
10
11use super::errors::ERRORS_ARGUMENT_NAME;
12use super::errors::ErrorsArguments;
13use crate::connectors::ConnectSpec;
14use crate::connectors::Header;
15use crate::connectors::JSONSelection;
16use crate::connectors::OriginatingDirective;
17use crate::connectors::SourceName;
18use crate::connectors::StringTemplate;
19use crate::connectors::spec::connect::DEFAULT_CONNECT_SPEC;
20use crate::connectors::spec::connect::IS_SUCCESS_ARGUMENT_NAME;
21use crate::connectors::spec::connect_spec_from_schema;
22use crate::connectors::spec::http::HTTP_ARGUMENT_NAME;
23use crate::connectors::spec::http::PATH_ARGUMENT_NAME;
24use crate::connectors::spec::http::QUERY_PARAMS_ARGUMENT_NAME;
25use crate::connectors::string_template;
26use crate::connectors::validation::Code;
27use crate::connectors::validation::Message;
28use crate::error::FederationError;
29
30pub(crate) const SOURCE_DIRECTIVE_NAME_IN_SPEC: Name = name!("source");
31pub(crate) const SOURCE_NAME_ARGUMENT_NAME: Name = name!("name");
32pub(crate) const SOURCE_HTTP_NAME_IN_SPEC: Name = name!("SourceHTTP");
33
34pub(crate) fn extract_source_directive_arguments(
35 schema: &Schema,
36 name: &Name,
37) -> Result<Vec<SourceDirectiveArguments>, FederationError> {
38 let connect_spec = connect_spec_from_schema(schema).unwrap_or(DEFAULT_CONNECT_SPEC);
39 schema
40 .schema_definition
41 .directives
42 .iter()
43 .filter(|directive| directive.name == *name)
44 .map(|directive| {
45 SourceDirectiveArguments::from_directive(directive, &schema.sources, connect_spec)
46 })
47 .collect()
48}
49
50#[cfg_attr(test, derive(Debug))]
52pub(crate) struct SourceDirectiveArguments {
53 pub(crate) name: SourceName,
55
56 pub(crate) http: SourceHTTPArguments,
58
59 pub(crate) errors: Option<ErrorsArguments>,
61
62 pub(crate) is_success: Option<JSONSelection>,
64}
65
66impl SourceDirectiveArguments {
67 fn from_directive(
68 value: &Component<Directive>,
69 sources: &SourceMap,
70 spec: ConnectSpec,
71 ) -> Result<Self, FederationError> {
72 let args = &value.arguments;
73 let directive_name = &value.name;
74
75 let name = SourceName::from_directive_permissive(value, sources).map_err(|message| {
77 crate::error::SingleFederationError::InvalidGraphQL {
78 message: message.message,
79 }
80 })?;
81 let mut http = None;
82 let mut errors = None;
83 let mut is_success = None;
84 for arg in args {
85 let arg_name = arg.name.as_str();
86
87 if arg_name == HTTP_ARGUMENT_NAME.as_str() {
88 let http_value = arg.value.as_object().ok_or_else(|| {
89 FederationError::internal(format!(
90 "`http` field in `@{directive_name}` directive is not an object"
91 ))
92 })?;
93 let http_value =
94 SourceHTTPArguments::from_directive(http_value, directive_name, sources, spec)?;
95
96 http = Some(http_value);
97 } else if arg_name == ERRORS_ARGUMENT_NAME.as_str() {
98 let http_value = arg.value.as_object().ok_or_else(|| {
99 FederationError::internal(format!(
100 "`errors` field in `@{directive_name}` directive is not an object"
101 ))
102 })?;
103 let errors_value = ErrorsArguments::try_from((http_value, directive_name, spec))?;
104
105 errors = Some(errors_value);
106 } else if arg_name == IS_SUCCESS_ARGUMENT_NAME.as_str() {
107 let selection_value = arg.value.as_str().ok_or_else(|| {
108 FederationError::internal(format!(
109 "`is_success` field in `@{directive_name}` directive is not a string"
110 ))
111 })?;
112 is_success = Some(
113 JSONSelection::parse_with_spec(selection_value, spec)
114 .map_err(|e| FederationError::internal(e.message))?,
115 );
116 }
117 }
118
119 Ok(Self {
120 name,
121 http: http.ok_or_else(|| {
122 FederationError::internal(format!(
123 "missing `http` field in `@{directive_name}` directive"
124 ))
125 })?,
126 errors,
127 is_success,
128 })
129 }
130}
131
132#[cfg_attr(test, derive(Debug))]
134pub struct SourceHTTPArguments {
135 pub(crate) base_url: BaseUrl,
137
138 pub(crate) headers: Vec<Header>,
141 pub(crate) path: Option<JSONSelection>,
142 pub(crate) query_params: Option<JSONSelection>,
143}
144
145impl SourceHTTPArguments {
146 pub fn from_directive(
147 values: &[(Name, Node<Value>)],
148 directive_name: &Name,
149 sources: &SourceMap,
150 spec: ConnectSpec,
151 ) -> Result<Self, FederationError> {
152 let base_url = BaseUrl::parse(values, directive_name, sources, spec)
153 .map_err(|err| FederationError::internal(err.message))?;
154 let headers: Vec<Header> =
155 Header::from_http_arg(values, OriginatingDirective::Source, spec)
156 .into_iter()
157 .try_collect()
158 .map_err(|err| FederationError::internal(err.to_string()))?;
159 let mut path = None;
160 let mut query = None;
161 for (name, value) in values {
162 let name = name.as_str();
163
164 if name == PATH_ARGUMENT_NAME.as_str() {
165 let value = value.as_str().ok_or_else(|| {
166 FederationError::internal(format!(
167 "`{PATH_ARGUMENT_NAME}` field in `@{directive_name}` directive's `http.path` field is not a string"
168 ))
169 })?;
170 path = Some(
171 JSONSelection::parse_with_spec(value, spec)
172 .map_err(|e| FederationError::internal(e.message))?,
173 );
174 } else if name == QUERY_PARAMS_ARGUMENT_NAME.as_str() {
175 let value = value.as_str().ok_or_else(|| FederationError::internal(format!(
176 "`{QUERY_PARAMS_ARGUMENT_NAME}` field in `@{directive_name}` directive's `http.queryParams` field is not a string"
177 )))?;
178 query = Some(
179 JSONSelection::parse_with_spec(value, spec)
180 .map_err(|e| FederationError::internal(e.message))?,
181 );
182 }
183 }
184
185 Ok(Self {
186 base_url,
187 headers,
188 path,
189 query_params: query,
190 })
191 }
192}
193
194#[derive(Debug, Clone)]
196pub(crate) struct BaseUrl {
197 pub(crate) template: StringTemplate,
198 pub(crate) node: Node<Value>,
199}
200
201impl BaseUrl {
202 pub(crate) const ARGUMENT: Name = name!("baseURL");
203
204 pub(crate) fn parse(
205 values: &[(Name, Node<Value>)],
206 directive_name: &Name,
207 sources: &SourceMap,
208 spec: ConnectSpec,
209 ) -> Result<Self, Message> {
210 const BASE_URL: Name = BaseUrl::ARGUMENT;
211
212 let value = values
213 .iter()
214 .find_map(|(key, value)| (key == &Self::ARGUMENT).then_some(value))
215 .ok_or_else(|| Message {
216 code: Code::GraphQLError,
217 message: format!("`@{directive_name}` must have a `baseURL` argument."),
218 locations: directive_name
219 .line_column_range(sources)
220 .into_iter()
221 .collect(),
222 })?;
223 let str_value = value.as_str().ok_or_else(|| Message {
224 code: Code::GraphQLError,
225 message: format!("`@{directive_name}({BASE_URL}:)` must be a string."),
226 locations: value.line_column_range(sources).into_iter().collect(),
227 })?;
228 let template: StringTemplate = StringTemplate::parse_with_spec(
229 str_value,
230 spec,
231 ).map_err(|inner: string_template::Error| {
232 Message {
233 code: Code::InvalidUrl,
234 message: format!(
235 "`@{directive_name}({BASE_URL})` value {str_value} is not a valid URL Template: {inner}."
236 ),
237 locations: value.line_column_range(sources).into_iter().collect(),
238 }
239 })?;
240
241 Ok(Self {
242 template,
243 node: value.clone(),
244 })
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use apollo_compiler::Schema;
251 use http::Uri;
252
253 use super::*;
254 use crate::ValidFederationSubgraphs;
255 use crate::connectors::Namespace;
256 use crate::schema::FederationSchema;
257 use crate::supergraph::extract_subgraphs_from_supergraph;
258
259 static SIMPLE_SUPERGRAPH: &str = include_str!("../tests/schemas/simple.graphql");
260 static TEMPLATED_SOURCE_SUPERGRAPH: &str =
261 include_str!("../tests/schemas/source-template.graphql");
262 static IS_SUCCESS_SOURCE_SUPERGRAPH: &str =
263 include_str!("../tests/schemas/is-success-source.graphql");
264
265 fn get_subgraphs(supergraph_sdl: &str) -> ValidFederationSubgraphs {
266 let schema = Schema::parse(supergraph_sdl, "supergraph.graphql").unwrap();
267 let supergraph_schema = FederationSchema::new(schema).unwrap();
268 extract_subgraphs_from_supergraph(&supergraph_schema, Some(true)).unwrap()
269 }
270
271 #[test]
272 fn it_parses_at_source() {
273 let subgraphs = get_subgraphs(SIMPLE_SUPERGRAPH);
274 let subgraph = subgraphs.get("connectors").unwrap();
275
276 let actual_definition = subgraph
277 .schema
278 .get_directive_definition(&SOURCE_DIRECTIVE_NAME_IN_SPEC)
279 .unwrap()
280 .get(subgraph.schema.schema())
281 .unwrap();
282
283 insta::assert_snapshot!(actual_definition.to_string(), @"directive @source(name: String!, http: connect__SourceHTTP, errors: connect__ConnectorErrors, isSuccess: connect__JSONSelection) repeatable on SCHEMA");
284
285 insta::assert_debug_snapshot!(
286 subgraph.schema
287 .referencers()
288 .get_directive(SOURCE_DIRECTIVE_NAME_IN_SPEC.as_str())
289 .unwrap(),
290 @r###"
291 DirectiveReferencers {
292 schema: Some(
293 SchemaDefinitionPosition,
294 ),
295 scalar_types: {},
296 object_types: {},
297 object_fields: {},
298 object_field_arguments: {},
299 interface_types: {},
300 interface_fields: {},
301 interface_field_arguments: {},
302 union_types: {},
303 enum_types: {},
304 enum_values: {},
305 input_object_types: {},
306 input_object_fields: {},
307 directive_arguments: {},
308 }
309 "###
310 );
311 }
312
313 #[test]
314 fn it_extracts_at_source() {
315 let sources = extract_source_directive_args(SIMPLE_SUPERGRAPH);
316
317 let source = sources.first().unwrap();
318 assert_eq!(source.name, SourceName::cast("json"));
319 assert_eq!(
320 source
321 .http
322 .base_url
323 .template
324 .interpolate_uri(&Default::default())
325 .unwrap()
326 .0,
327 Uri::from_static("https://jsonplaceholder.typicode.com/")
328 );
329 assert_eq!(source.http.path, None);
330 assert_eq!(source.http.query_params, None);
331
332 insta::assert_debug_snapshot!(
333 source.http.headers,
334 @r#"
335 [
336 Header {
337 name: "authtoken",
338 source: From(
339 "x-auth-token",
340 ),
341 },
342 Header {
343 name: "user-agent",
344 source: Value(
345 HeaderValue(
346 StringTemplate {
347 parts: [
348 Constant(
349 Constant {
350 value: "Firefox",
351 location: 0..7,
352 },
353 ),
354 ],
355 },
356 ),
357 ),
358 },
359 ]
360 "#
361 );
362 }
363
364 #[test]
365 fn it_parses_as_template_at_source() {
366 let directive_args = extract_source_directive_args(TEMPLATED_SOURCE_SUPERGRAPH);
367
368 let templated_base_url = directive_args
370 .iter()
371 .find(|arg| arg.name == SourceName::cast("json"))
372 .map(|arg| arg.http.base_url.clone())
373 .unwrap()
374 .template;
375 assert_eq!(
376 templated_base_url.to_string(),
377 "https://${$config.subdomain}.typicode.com/"
378 );
379
380 templated_base_url
382 .expressions()
383 .flat_map(|exp| exp.expression.variable_references())
384 .find(|var_ref| var_ref.namespace.namespace == Namespace::Config)
385 .unwrap();
386 }
387
388 #[test]
389 fn it_supports_is_success_in_source() {
390 let spec_from_success_source_subgraph = ConnectSpec::V0_1;
391 let sources = extract_source_directive_args(IS_SUCCESS_SOURCE_SUPERGRAPH);
392 let source = sources.first().unwrap();
393 assert_eq!(source.name, SourceName::cast("json"));
394 assert!(source.is_success.is_some());
395 let expected =
396 JSONSelection::parse_with_spec("$status->eq(202)", spec_from_success_source_subgraph)
397 .unwrap();
398 assert_eq!(source.is_success.as_ref().unwrap(), &expected);
399 }
400
401 fn extract_source_directive_args(graph: &str) -> Vec<SourceDirectiveArguments> {
402 let subgraphs = get_subgraphs(graph);
403 let subgraph = subgraphs.get("connectors").unwrap();
404 let schema = &subgraph.schema;
405
406 let sources = schema
408 .referencers()
409 .get_directive(&SOURCE_DIRECTIVE_NAME_IN_SPEC)
410 .unwrap();
411
412 let schema_directive_refs = sources.schema.as_ref().unwrap();
413 let sources: Result<Vec<_>, _> = schema_directive_refs
414 .get(schema.schema())
415 .directives
416 .iter()
417 .filter(|directive| directive.name == SOURCE_DIRECTIVE_NAME_IN_SPEC)
418 .map(|directive| {
419 let connect_spec =
420 connect_spec_from_schema(schema.schema()).unwrap_or(DEFAULT_CONNECT_SPEC);
421 SourceDirectiveArguments::from_directive(
422 directive,
423 &schema.schema().sources,
424 connect_spec,
425 )
426 })
427 .collect();
428 sources.unwrap()
429 }
430}