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