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 @r###"
290 DirectiveReferencers {
291 schema: Some(
292 SchemaDefinitionPosition,
293 ),
294 scalar_types: {},
295 object_types: {},
296 object_fields: {},
297 object_field_arguments: {},
298 interface_types: {},
299 interface_fields: {},
300 interface_field_arguments: {},
301 union_types: {},
302 enum_types: {},
303 enum_values: {},
304 input_object_types: {},
305 input_object_fields: {},
306 directive_arguments: {},
307 }
308 "###
309 );
310 }
311
312 #[test]
313 fn it_extracts_at_source() {
314 let sources = extract_source_directive_args(SIMPLE_SUPERGRAPH);
315
316 let source = sources.first().unwrap();
317 assert_eq!(source.name, SourceName::cast("json"));
318 assert_eq!(
319 source
320 .http
321 .base_url
322 .template
323 .interpolate_uri(&Default::default())
324 .unwrap()
325 .0,
326 Uri::from_static("https://jsonplaceholder.typicode.com/")
327 );
328 assert_eq!(source.http.path, None);
329 assert_eq!(source.http.query_params, None);
330
331 insta::assert_debug_snapshot!(
332 source.http.headers,
333 @r#"
334 [
335 Header {
336 name: "authtoken",
337 source: From(
338 "x-auth-token",
339 ),
340 },
341 Header {
342 name: "user-agent",
343 source: Value(
344 HeaderValue(
345 StringTemplate {
346 parts: [
347 Constant(
348 Constant {
349 value: "Firefox",
350 location: 0..7,
351 },
352 ),
353 ],
354 },
355 ),
356 ),
357 },
358 ]
359 "#
360 );
361 }
362
363 #[test]
364 fn it_parses_as_template_at_source() {
365 let directive_args = extract_source_directive_args(TEMPLATED_SOURCE_SUPERGRAPH);
366
367 let templated_base_url = directive_args
369 .iter()
370 .find(|arg| arg.name == SourceName::cast("json"))
371 .map(|arg| arg.http.base_url.clone())
372 .unwrap()
373 .template;
374 assert_eq!(
375 templated_base_url.to_string(),
376 "https://${$config.subdomain}.typicode.com/"
377 );
378
379 templated_base_url
381 .expressions()
382 .flat_map(|exp| exp.expression.variable_references())
383 .find(|var_ref| var_ref.namespace.namespace == Namespace::Config)
384 .unwrap();
385 }
386
387 #[test]
388 fn it_supports_is_success_in_source() {
389 let spec_from_success_source_subgraph = ConnectSpec::V0_1;
390 let sources = extract_source_directive_args(IS_SUCCESS_SOURCE_SUPERGRAPH);
391 let source = sources.first().unwrap();
392 assert_eq!(source.name, SourceName::cast("json"));
393 assert!(source.is_success.is_some());
394 let expected =
395 JSONSelection::parse_with_spec("$status->eq(202)", spec_from_success_source_subgraph)
396 .unwrap();
397 assert_eq!(source.is_success.as_ref().unwrap(), &expected);
398 }
399
400 fn extract_source_directive_args(graph: &str) -> Vec<SourceDirectiveArguments> {
401 let subgraphs = get_subgraphs(graph);
402 let subgraph = subgraphs.get("connectors").unwrap();
403 let schema = &subgraph.schema;
404
405 let sources = schema
407 .referencers()
408 .get_directive(&SOURCE_DIRECTIVE_NAME_IN_SPEC);
409
410 let schema_directive_refs = sources.schema.as_ref().unwrap();
411 let sources: Result<Vec<_>, _> = schema_directive_refs
412 .get(schema.schema())
413 .directives
414 .iter()
415 .filter(|directive| directive.name == SOURCE_DIRECTIVE_NAME_IN_SPEC)
416 .map(|directive| {
417 let connect_spec =
418 connect_spec_from_schema(schema.schema()).unwrap_or(DEFAULT_CONNECT_SPEC);
419 SourceDirectiveArguments::from_directive(
420 directive,
421 &schema.schema().sources,
422 connect_spec,
423 )
424 })
425 .collect();
426 sources.unwrap()
427 }
428}