Skip to main content

apollo_federation/subgraph/
mod.rs

1use std::fmt::Display;
2use std::fmt::Formatter;
3use std::ops::Range;
4
5use apollo_compiler::Node;
6use apollo_compiler::Schema;
7use apollo_compiler::collections::IndexMap;
8use apollo_compiler::collections::IndexSet;
9use apollo_compiler::name;
10use apollo_compiler::parser::LineColumn;
11use apollo_compiler::schema::ComponentName;
12use apollo_compiler::schema::ExtendedType;
13use apollo_compiler::schema::ObjectType;
14use apollo_compiler::validation::DiagnosticList;
15use apollo_compiler::validation::Valid;
16use indexmap::map::Entry;
17
18use crate::ValidFederationSubgraph;
19use crate::compat::coerce_and_validate_schema_values;
20use crate::error::FederationError;
21use crate::error::MultipleFederationErrors;
22use crate::error::SingleFederationError;
23use crate::link::Link;
24use crate::link::link_spec_definition::LINK_DIRECTIVE_NAME_IN_SPEC;
25use crate::link::spec::Identity;
26use crate::subgraph::spec::ANY_SCALAR_NAME;
27use crate::subgraph::spec::AppliedFederationLink;
28use crate::subgraph::spec::CONTEXTFIELDVALUE_SCALAR_NAME;
29use crate::subgraph::spec::ENTITIES_QUERY;
30use crate::subgraph::spec::ENTITY_UNION_NAME;
31use crate::subgraph::spec::FEDERATION_V2_DIRECTIVE_NAMES;
32use crate::subgraph::spec::FederationSpecDefinitions;
33use crate::subgraph::spec::KEY_DIRECTIVE_NAME;
34use crate::subgraph::spec::LinkSpecDefinitions;
35use crate::subgraph::spec::SERVICE_SDL_QUERY;
36use crate::subgraph::spec::SERVICE_TYPE;
37
38pub mod spec;
39pub mod typestate; // TODO: Move here to overwrite Subgraph after API is reasonable
40
41pub struct Subgraph {
42    pub name: String,
43    pub url: String,
44    pub schema: Schema,
45}
46
47impl Subgraph {
48    pub fn new(name: &str, url: &str, schema_str: &str) -> Result<Self, FederationError> {
49        let schema = Schema::parse(schema_str, name)?;
50        // TODO: federation-specific validation
51        Ok(Self {
52            name: name.to_string(),
53            url: url.to_string(),
54            schema,
55        })
56    }
57
58    pub fn parse_and_expand(
59        name: &str,
60        url: &str,
61        schema_str: &str,
62    ) -> Result<ValidSubgraph, FederationError> {
63        let mut schema = Schema::builder()
64            .adopt_orphan_extensions()
65            .parse(schema_str, name)
66            .build()?;
67
68        let mut imported_federation_definitions: Option<FederationSpecDefinitions> = None;
69        let mut imported_link_definitions: Option<LinkSpecDefinitions> = None;
70        let default_link_name = LINK_DIRECTIVE_NAME_IN_SPEC;
71        let link_directives = schema
72            .schema_definition
73            .directives
74            .get_all(&default_link_name);
75
76        for directive in link_directives {
77            let link_directive =
78                Link::from_directive_application_when_link_spec_unknown(directive, &schema)?;
79            if link_directive.url.identity == Identity::federation_identity() {
80                if imported_federation_definitions.is_some() {
81                    let msg = "Invalid use of @link in schema: invalid graphql schema - multiple @link imports for the federation specification are not supported";
82                    return Err(SingleFederationError::InvalidLinkDirectiveUsage {
83                        message: msg.to_owned(),
84                    }
85                    .into());
86                }
87
88                imported_federation_definitions =
89                    Some(FederationSpecDefinitions::from_link(link_directive)?);
90            } else if link_directive.url.identity == Identity::link_identity() {
91                // user manually imported @link specification
92                if imported_link_definitions.is_some() {
93                    let msg = "Invalid use of @link in schema: invalid graphql schema - multiple @link imports for the link specification are not supported";
94                    return Err(SingleFederationError::InvalidLinkDirectiveUsage {
95                        message: msg.to_owned(),
96                    }
97                    .into());
98                }
99
100                imported_link_definitions = Some(LinkSpecDefinitions::new(link_directive));
101            }
102        }
103
104        // generate additional schema definitions
105        Self::populate_missing_type_definitions(
106            &mut schema,
107            imported_federation_definitions,
108            imported_link_definitions,
109        )?;
110        coerce_and_validate_schema_values(&mut schema)?;
111        let schema = schema.validate()?;
112        Ok(ValidSubgraph {
113            name: name.to_owned(),
114            url: url.to_owned(),
115            schema,
116        })
117    }
118
119    fn populate_missing_type_definitions(
120        schema: &mut Schema,
121        imported_federation_definitions: Option<FederationSpecDefinitions>,
122        imported_link_definitions: Option<LinkSpecDefinitions>,
123    ) -> Result<(), FederationError> {
124        // populate @link spec definitions
125        let link_spec_definitions = match imported_link_definitions {
126            Some(definitions) => definitions,
127            None => {
128                // need to apply default @link directive for link spec on schema
129                let defaults = LinkSpecDefinitions::default();
130                schema
131                    .schema_definition
132                    .make_mut()
133                    .directives
134                    .push(defaults.applied_link_directive());
135                defaults
136            }
137        };
138        Self::populate_missing_link_definitions(schema, link_spec_definitions)?;
139
140        // populate @link federation spec definitions
141        let fed_definitions = match imported_federation_definitions {
142            Some(definitions) => definitions,
143            None => {
144                // federation v1 schema or user does not import federation spec
145                // need to apply default @link directive for federation spec on schema
146                let defaults = FederationSpecDefinitions::default()?;
147                schema
148                    .schema_definition
149                    .make_mut()
150                    .directives
151                    .push(defaults.applied_link_directive());
152                defaults
153            }
154        };
155        Self::populate_missing_federation_directive_definitions(schema, &fed_definitions)?;
156        Self::populate_missing_federation_types(schema, &fed_definitions)
157    }
158
159    fn populate_missing_link_definitions(
160        schema: &mut Schema,
161        link_spec_definitions: LinkSpecDefinitions,
162    ) -> Result<(), FederationError> {
163        let purpose_enum_name = &link_spec_definitions.purpose_enum_name;
164        schema
165            .types
166            .entry(purpose_enum_name.clone())
167            .or_insert_with(|| {
168                link_spec_definitions
169                    .link_purpose_enum_definition(purpose_enum_name.clone())
170                    .into()
171            });
172        let import_scalar_name = &link_spec_definitions.import_scalar_name;
173        schema
174            .types
175            .entry(import_scalar_name.clone())
176            .or_insert_with(|| {
177                link_spec_definitions
178                    .import_scalar_definition(import_scalar_name.clone())
179                    .into()
180            });
181        if let Entry::Vacant(entry) = schema
182            .directive_definitions
183            .entry(LINK_DIRECTIVE_NAME_IN_SPEC)
184        {
185            entry.insert(link_spec_definitions.link_directive_definition()?.into());
186        }
187        Ok(())
188    }
189
190    fn populate_missing_federation_directive_definitions(
191        schema: &mut Schema,
192        fed_definitions: &FederationSpecDefinitions,
193    ) -> Result<(), FederationError> {
194        // scalar FieldSet
195        let fieldset_scalar_name = &fed_definitions.fieldset_scalar_name;
196        schema
197            .types
198            .entry(fieldset_scalar_name.clone())
199            .or_insert_with(|| {
200                fed_definitions
201                    .fieldset_scalar_definition(fieldset_scalar_name.clone())
202                    .into()
203            });
204
205        // scalar ContextFieldValue
206        let namespaced_contextfieldvalue_scalar_name =
207            fed_definitions.namespaced_type_name(&CONTEXTFIELDVALUE_SCALAR_NAME, false);
208        if let Entry::Vacant(entry) = schema
209            .types
210            .entry(namespaced_contextfieldvalue_scalar_name.clone())
211        {
212            let type_definition = fed_definitions.contextfieldvalue_scalar_definition(&Some(
213                namespaced_contextfieldvalue_scalar_name,
214            ));
215            entry.insert(type_definition.into());
216        }
217
218        for directive_name in &FEDERATION_V2_DIRECTIVE_NAMES {
219            let namespaced_directive_name =
220                fed_definitions.namespaced_type_name(directive_name, true);
221            if let Entry::Vacant(entry) = schema
222                .directive_definitions
223                .entry(namespaced_directive_name.clone())
224            {
225                let directive_definition = fed_definitions.directive_definition(
226                    directive_name,
227                    &Some(namespaced_directive_name.to_owned()),
228                )?;
229                entry.insert(directive_definition.into());
230            }
231        }
232        Ok(())
233    }
234
235    fn populate_missing_federation_types(
236        schema: &mut Schema,
237        fed_definitions: &FederationSpecDefinitions,
238    ) -> Result<(), FederationError> {
239        schema
240            .types
241            .entry(SERVICE_TYPE)
242            .or_insert_with(|| fed_definitions.service_object_type_definition());
243
244        let entities = Self::locate_entities(schema, fed_definitions);
245        let entities_present = !entities.is_empty();
246        if entities_present {
247            schema
248                .types
249                .entry(ENTITY_UNION_NAME)
250                .or_insert_with(|| fed_definitions.entity_union_definition(entities));
251            schema
252                .types
253                .entry(ANY_SCALAR_NAME)
254                .or_insert_with(|| fed_definitions.any_scalar_definition());
255        }
256
257        let query_type_name = schema
258            .schema_definition
259            .make_mut()
260            .query
261            .get_or_insert(ComponentName::from(name!("Query")));
262        if let ExtendedType::Object(query_type) = schema
263            .types
264            .entry(query_type_name.name.clone())
265            .or_insert(ExtendedType::Object(Node::new(ObjectType {
266                description: None,
267                name: query_type_name.name.clone(),
268                directives: Default::default(),
269                fields: IndexMap::default(),
270                implements_interfaces: IndexSet::default(),
271            })))
272        {
273            let query_type = query_type.make_mut();
274            query_type
275                .fields
276                .entry(SERVICE_SDL_QUERY)
277                .or_insert_with(|| fed_definitions.service_sdl_query_field());
278            if entities_present {
279                // _entities(representations: [_Any!]!): [_Entity]!
280                query_type
281                    .fields
282                    .entry(ENTITIES_QUERY)
283                    .or_insert_with(|| fed_definitions.entities_query_field());
284            }
285        }
286        Ok(())
287    }
288
289    fn locate_entities(
290        schema: &mut Schema,
291        fed_definitions: &FederationSpecDefinitions,
292    ) -> IndexSet<ComponentName> {
293        let mut entities = Vec::new();
294        let immutable_type_map = schema.types.to_owned();
295        for (named_type, extended_type) in immutable_type_map.iter() {
296            let is_entity = extended_type
297                .directives()
298                .iter()
299                .find(|d| {
300                    d.name
301                        == fed_definitions
302                            .namespaced_type_name(&KEY_DIRECTIVE_NAME, true)
303                            .as_str()
304                })
305                .map(|_| true)
306                .unwrap_or(false);
307            if is_entity {
308                entities.push(named_type);
309            }
310        }
311        let entity_set: IndexSet<ComponentName> =
312            entities.iter().map(|e| ComponentName::from(*e)).collect();
313        entity_set
314    }
315}
316
317impl std::fmt::Debug for Subgraph {
318    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
319        write!(f, r#"name: {}, urL: {}"#, self.name, self.url)
320    }
321}
322
323pub struct ValidSubgraph {
324    pub name: String,
325    pub url: String,
326    pub schema: Valid<Schema>,
327}
328
329impl std::fmt::Debug for ValidSubgraph {
330    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
331        write!(f, r#"name: {}, url: {}"#, self.name, self.url)
332    }
333}
334
335impl From<ValidFederationSubgraph> for ValidSubgraph {
336    fn from(value: ValidFederationSubgraph) -> Self {
337        Self {
338            name: value.name,
339            url: value.url,
340            schema: value.schema.schema().clone(),
341        }
342    }
343}
344
345#[derive(Clone, Debug)]
346pub(crate) struct SingleSubgraphError {
347    pub(crate) error: SingleFederationError,
348    pub(crate) locations: Vec<Range<LineColumn>>,
349}
350
351/// Currently, this is making up for the fact that we don't have an equivalent of `addSubgraphToErrors`.
352/// In JS, that manipulates the underlying `GraphQLError` message to prepend the subgraph name. In Rust,
353/// it's idiomatic to have strongly typed errors which defer conversion to strings via `thiserror`, so
354/// for now we wrap the underlying error until we figure out a longer-term replacement that accounts
355/// for missing error codes and the like.
356#[derive(Clone, Debug)]
357pub struct SubgraphError {
358    pub(crate) subgraph: String,
359    pub(crate) errors: Vec<SingleSubgraphError>,
360}
361
362impl SubgraphError {
363    // Legacy constructor without locations info.
364    pub(crate) fn new_without_locations(
365        subgraph: impl Into<String>,
366        error: impl Into<FederationError>,
367    ) -> Self {
368        let subgraph = subgraph.into();
369        let error: FederationError = error.into();
370        SubgraphError {
371            subgraph,
372            errors: error
373                .errors()
374                .into_iter()
375                .map(|e| SingleSubgraphError {
376                    error: e.clone(),
377                    locations: Vec::new(),
378                })
379                .collect(),
380        }
381    }
382
383    /// Construct from a FederationError.
384    ///
385    /// Note: FederationError may hold multiple errors. In that case, all individual errors in the
386    ///       FederationError will share the same locations.
387    #[allow(dead_code)]
388    pub(crate) fn from_federation_error(
389        subgraph: impl Into<String>,
390        error: impl Into<FederationError>,
391        locations: Vec<Range<LineColumn>>,
392    ) -> Self {
393        let error: FederationError = error.into();
394        let errors = error
395            .errors()
396            .into_iter()
397            .map(|e| SingleSubgraphError {
398                error: e.clone(),
399                locations: locations.clone(),
400            })
401            .collect();
402        SubgraphError {
403            subgraph: subgraph.into(),
404            errors,
405        }
406    }
407
408    /// Constructing from GraphQL errors.
409    pub(crate) fn from_diagnostic_list(
410        subgraph: impl Into<String>,
411        errors: DiagnosticList,
412    ) -> Self {
413        let subgraph = subgraph.into();
414        SubgraphError {
415            subgraph,
416            errors: errors
417                .iter()
418                .map(|d| SingleSubgraphError {
419                    error: SingleFederationError::InvalidGraphQL {
420                        message: d.to_string(),
421                    },
422                    locations: d.line_column_range().iter().cloned().collect(),
423                })
424                .collect(),
425        }
426    }
427
428    /// Convert SubgraphError to FederationError.
429    /// * WARNING: This is a lossy conversion, losing location information.
430    pub(crate) fn into_federation_error(self) -> FederationError {
431        MultipleFederationErrors::from_iter(self.errors.into_iter().map(|e| e.error)).into()
432    }
433
434    // Format subgraph errors in the same way as `Rover` does.
435    // And return them as a vector of (error_code, error_message) tuples
436    // - Gather associated errors from the validation error.
437    // - Split each error into its code and message.
438    // - Add the subgraph name prefix to CompositionError message.
439    //
440    // This is mainly for internal testing. Consider using `to_composition_errors` method instead.
441    pub fn format_errors(&self) -> Vec<(String, String)> {
442        self.errors
443            .iter()
444            .map(|e| {
445                let error = &e.error;
446                (
447                    error.code_string(),
448                    format!("[{subgraph}] {error}", subgraph = self.subgraph),
449                )
450            })
451            .collect()
452    }
453}
454
455impl Display for SubgraphError {
456    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
457        for (code, message) in self.format_errors() {
458            writeln!(f, "{code} {message}")?;
459        }
460        Ok(())
461    }
462}
463
464pub mod test_utils {
465
466    use super::SubgraphError;
467    use super::typestate::Expanded;
468    use super::typestate::Subgraph;
469    use super::typestate::Validated;
470
471    pub enum BuildOption {
472        AsIs,
473        AsFed2,
474    }
475
476    pub fn build_inner(
477        schema_str: &str,
478        build_option: BuildOption,
479    ) -> Result<Subgraph<Validated>, SubgraphError> {
480        let name = "S";
481        let subgraph =
482            Subgraph::parse(name, &format!("http://{name}"), schema_str).expect("valid schema");
483        let subgraph = if matches!(build_option, BuildOption::AsFed2) {
484            subgraph.into_fed2_test_subgraph(true)?
485        } else {
486            subgraph
487        };
488        Ok(subgraph
489            .expand_links()?
490            .normalize_root_types()?
491            .assume_validated())
492    }
493
494    pub fn build_inner_expanded(
495        schema_str: &str,
496        build_option: BuildOption,
497    ) -> Result<Subgraph<Expanded>, SubgraphError> {
498        let name = "S";
499        let subgraph =
500            Subgraph::parse(name, &format!("http://{name}"), schema_str).expect("valid schema");
501        let subgraph = if matches!(build_option, BuildOption::AsFed2) {
502            subgraph.into_fed2_test_subgraph(true)?
503        } else {
504            subgraph
505        };
506        subgraph.expand_links_without_validation()
507    }
508
509    pub fn build_and_validate(schema_str: &str) -> Subgraph<Validated> {
510        build_inner(schema_str, BuildOption::AsIs).expect("expanded subgraph to be valid")
511    }
512
513    pub fn build_and_expand(schema_str: &str) -> Subgraph<Expanded> {
514        build_inner_expanded(schema_str, BuildOption::AsIs).expect("expanded subgraph to be valid")
515    }
516
517    pub fn build_for_errors_with_option(
518        schema: &str,
519        build_option: BuildOption,
520    ) -> Vec<(String, String)> {
521        build_inner(schema, build_option)
522            .expect_err("subgraph error was expected")
523            .format_errors()
524    }
525
526    /// Build subgraph expecting errors, assuming fed 2.
527    pub fn build_for_errors(schema: &str) -> Vec<(String, String)> {
528        build_for_errors_with_option(schema, BuildOption::AsFed2)
529    }
530
531    pub fn remove_indentation(s: &str) -> String {
532        // count the last lines that are space-only
533        let first_empty_lines = s.lines().take_while(|line| line.trim().is_empty()).count();
534        let last_empty_lines = s
535            .lines()
536            .rev()
537            .take_while(|line| line.trim().is_empty())
538            .count();
539
540        // lines without the space-only first/last lines
541        let lines = s
542            .lines()
543            .skip(first_empty_lines)
544            .take(s.lines().count() - first_empty_lines - last_empty_lines);
545
546        // compute the indentation
547        let indentation = lines
548            .clone()
549            .map(|line| line.chars().take_while(|c| *c == ' ').count())
550            .min()
551            .unwrap_or(0);
552
553        // remove the indentation
554        lines
555            .map(|line| {
556                line.trim_end()
557                    .chars()
558                    .skip(indentation)
559                    .collect::<String>()
560            })
561            .collect::<Vec<_>>()
562            .join("\n")
563    }
564
565    /// True if a and b contain the same error messages
566    pub fn check_errors(a: &[(String, String)], b: &[(&str, &str)]) -> Result<(), String> {
567        if a.len() != b.len() {
568            return Err(format!(
569                "Mismatched error counts: {} != {}\n\nexpected:\n{}\n\nactual:\n{}",
570                b.len(),
571                a.len(),
572                b.iter()
573                    .map(|(code, msg)| { format!("- {code}: {msg}") })
574                    .collect::<Vec<_>>()
575                    .join("\n"),
576                a.iter()
577                    .map(|(code, msg)| { format!("+ {code}: {msg}") })
578                    .collect::<Vec<_>>()
579                    .join("\n"),
580            ));
581        }
582
583        // remove indentations from messages to ignore indentation differences
584        let b_iter = b
585            .iter()
586            .map(|(code, message)| (*code, remove_indentation(message)));
587        let diff: Vec<_> = a
588            .iter()
589            .map(|(code, message)| (code.as_str(), remove_indentation(message)))
590            .zip(b_iter)
591            .filter(|(a_i, b_i)| a_i.0 != b_i.0 || a_i.1 != b_i.1)
592            .collect();
593        if diff.is_empty() {
594            Ok(())
595        } else {
596            Err(format!(
597                "Mismatched errors:\n{}\n",
598                diff.iter()
599                    .map(|(a_i, b_i)| { format!("- {}: {}\n+ {}: {}", b_i.0, b_i.1, a_i.0, a_i.1) })
600                    .collect::<Vec<_>>()
601                    .join("\n")
602            ))
603        }
604    }
605
606    #[macro_export]
607    macro_rules! assert_errors {
608        ($a:expr, $b:expr) => {
609            match apollo_federation::subgraph::test_utils::check_errors(&$a, &$b) {
610                Ok(()) => {
611                    // Success
612                }
613                Err(e) => {
614                    panic!("{e}")
615                }
616            }
617        };
618    }
619}
620
621// INTERNAL: For use by Language Server Protocol (LSP) team
622// WARNING: Any changes to this function signature will result in breakages in the dependency chain
623// Generates a diff string containing directives and types not included in initial schema string
624pub fn schema_diff_expanded_from_initial(schema_str: String) -> Result<String, FederationError> {
625    // Parse schema string as Schema without validation.
626    let initial_schema = Schema::parse(schema_str, "")?;
627
628    // Initialize and expand subgraph without validation
629    let initial_subgraph =
630        typestate::Subgraph::new("S", "http://S", initial_schema.clone(), Default::default());
631    let expanded_subgraph = initial_subgraph
632        .map_err(|e| e.into_federation_error())?
633        .expand_links_without_validation()
634        .map_err(|e| e.into_federation_error())?;
635
636    // Build string of missing directives and types from initial to expanded
637    let mut diff = String::new();
638
639    // Push newly added directives onto diff
640    for (dir_name, dir_def) in &expanded_subgraph.schema().schema().directive_definitions {
641        if !initial_schema.directive_definitions.contains_key(dir_name) {
642            diff.push_str(&dir_def.to_string());
643            diff.push('\n');
644        }
645    }
646
647    // Push newly added types onto diff
648    for (named_ty, extended_ty) in &expanded_subgraph.schema().schema().types {
649        if !initial_schema.types.contains_key(named_ty) {
650            diff.push_str(&extended_ty.to_string());
651        }
652    }
653
654    Ok(diff)
655}
656
657#[cfg(test)]
658mod tests {
659    use crate::subgraph::schema_diff_expanded_from_initial;
660
661    #[test]
662    fn returns_correct_schema_diff_for_fed_2_0() {
663        let schema_string = r#"
664                extend schema @link(url: "https://specs.apollo.dev/federation/v2.0")
665
666                type Query {
667                    s: String
668                }"#
669        .to_string();
670
671        let diff = schema_diff_expanded_from_initial(schema_string);
672
673        insta::assert_snapshot!(diff.unwrap_or_default(), @r#"directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
674directive @federation__key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
675directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION
676directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION
677directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION
678directive @federation__tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
679directive @federation__extends on OBJECT | INTERFACE
680directive @federation__shareable on OBJECT | FIELD_DEFINITION
681directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
682directive @federation__override(from: String!) on FIELD_DEFINITION
683enum link__Purpose {
684  """
685  `SECURITY` features provide metadata necessary to securely resolve fields.
686  """
687  SECURITY
688  """
689  `EXECUTION` features provide metadata necessary for operation execution.
690  """
691  EXECUTION
692}
693scalar link__Import
694scalar federation__FieldSet
695scalar _Any
696type _Service {
697  sdl: String
698}"#);
699    }
700
701    #[test]
702    fn returns_correct_schema_diff_for_fed_2_4() {
703        let schema_string = r#"
704                extend schema @link(url: "https://specs.apollo.dev/federation/v2.4")
705
706                type Query {
707                    s: String
708                }"#
709        .to_string();
710
711        let diff = schema_diff_expanded_from_initial(schema_string);
712
713        insta::assert_snapshot!(diff.unwrap_or_default(), @r#"directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
714directive @federation__key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
715directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION
716directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION
717directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION
718directive @federation__tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA
719directive @federation__extends on OBJECT | INTERFACE
720directive @federation__shareable repeatable on OBJECT | FIELD_DEFINITION
721directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
722directive @federation__override(from: String!) on FIELD_DEFINITION
723directive @federation__composeDirective(name: String) repeatable on SCHEMA
724directive @federation__interfaceObject on OBJECT
725enum link__Purpose {
726  """
727  `SECURITY` features provide metadata necessary to securely resolve fields.
728  """
729  SECURITY
730  """
731  `EXECUTION` features provide metadata necessary for operation execution.
732  """
733  EXECUTION
734}
735scalar link__Import
736scalar federation__FieldSet
737scalar _Any
738type _Service {
739  sdl: String
740}"#);
741    }
742
743    #[test]
744    fn returns_correct_schema_diff_for_fed_2_9() {
745        let schema_string = r#"
746                extend schema @link(url: "https://specs.apollo.dev/federation/v2.9")
747
748                type Query {
749                    s: String
750                }"#
751        .to_string();
752
753        let diff = schema_diff_expanded_from_initial(schema_string);
754
755        insta::assert_snapshot!(diff.unwrap_or_default(), @r#"directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
756directive @federation__key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
757directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION
758directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION
759directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION
760directive @federation__tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA
761directive @federation__extends on OBJECT | INTERFACE
762directive @federation__shareable repeatable on OBJECT | FIELD_DEFINITION
763directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
764directive @federation__override(from: String!, label: String) on FIELD_DEFINITION
765directive @federation__composeDirective(name: String) repeatable on SCHEMA
766directive @federation__interfaceObject on OBJECT
767directive @federation__authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
768directive @federation__requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
769directive @federation__policy(policies: [[federation__Policy!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
770directive @federation__context(name: String!) repeatable on INTERFACE | OBJECT | UNION
771directive @federation__fromContext(field: federation__ContextFieldValue) on ARGUMENT_DEFINITION
772directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR
773directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION
774enum link__Purpose {
775  """
776  `SECURITY` features provide metadata necessary to securely resolve fields.
777  """
778  SECURITY
779  """
780  `EXECUTION` features provide metadata necessary for operation execution.
781  """
782  EXECUTION
783}
784scalar link__Import
785scalar federation__FieldSet
786scalar federation__Scope
787scalar federation__Policy
788scalar federation__ContextFieldValue
789scalar _Any
790type _Service {
791  sdl: String
792}"#);
793    }
794}