graphql-composition 0.12.2

An implementation of GraphQL federated schema composition
Documentation
//! This is a separate module because we want to use only the public API of [Subgraphs] and avoid
//! mixing GraphQL parser logic and types with our internals.

mod context;
mod directive_definitions;
mod directives;
mod enums;
mod fields;
mod nested_key_fields;
mod schema_definitions;

use self::{
    directive_definitions::*, directives::*, nested_key_fields::ingest_nested_key_fields, schema_definitions::*,
};
use crate::{
    Subgraphs,
    subgraphs::{self, DefinitionId, DefinitionKind, DirectiveSiteId, SubgraphId},
};
use cynic_parser::{ConstValue, type_system as ast};

/// _Service is a special type exposed by subgraphs. It should not be composed.
const SERVICE_TYPE_NAME: &str = "_Service";

/// _Entity is a special union type exposed by subgraphs. It should not be composed.
const ENTITY_UNION_NAME: &str = "_Entity";

struct Context<'a> {
    document: &'a ast::TypeSystemDocument,
    subgraph_id: SubgraphId,
    subgraphs: &'a mut Subgraphs,
    root_type_matcher: RootTypeMatcher<'a>,
}

pub(crate) fn ingest_subgraph(
    document: &ast::TypeSystemDocument,
    name: &str,
    url: Option<&str>,
    subgraphs: &mut Subgraphs,
) {
    let subgraph_id = subgraphs.push_subgraph(name, url);

    let mut ctx = Context {
        document,
        subgraph_id,
        subgraphs,
        root_type_matcher: Default::default(),
    };

    ingest_directive_definitions(&mut ctx);
    ingest_schema_definitions(&mut ctx);

    ingest_top_level_definitions(&mut ctx);
    ingest_definition_bodies(&mut ctx);
    ingest_nested_key_fields(&mut ctx);
}

fn ingest_top_level_definitions(ctx: &mut Context<'_>) {
    let subgraph_id = ctx.subgraph_id;

    for definition in ctx.document.definitions() {
        match definition {
            ast::Definition::Type(type_definition) | ast::Definition::TypeExtension(type_definition) => {
                let type_name = type_definition.name();

                let description = type_definition
                    .description()
                    .map(|description| ctx.subgraphs.strings.intern(description.to_cow()));

                let definition_id = match type_definition {
                    ast::TypeDefinition::Object(_) if type_name == SERVICE_TYPE_NAME => continue,
                    ast::TypeDefinition::Union(_) if type_name == ENTITY_UNION_NAME => continue,

                    ast::TypeDefinition::Object(_) => {
                        let definition_id = ctx.subgraphs.get_or_push_definition(
                            subgraph_id,
                            type_name,
                            DefinitionKind::Object,
                            description,
                        );

                        match ctx.root_type_matcher.match_name(type_name) {
                            RootTypeMatch::Query => {
                                ctx.subgraphs.set_query_type(subgraph_id, definition_id);
                            }
                            RootTypeMatch::Mutation => {
                                ctx.subgraphs.set_mutation_type(subgraph_id, definition_id);
                            }
                            RootTypeMatch::Subscription => {
                                ctx.subgraphs.set_subscription_type(subgraph_id, definition_id);
                            }
                            RootTypeMatch::NotRootButHasDefaultRootName => {
                                ctx.subgraphs.push_ingestion_diagnostic(subgraph_id, format!("The {type_name} type has the default name for a root but is itself not a root. This is not valid in a federation context."));
                            }
                            RootTypeMatch::NotRoot => (),
                        }

                        definition_id
                    }
                    ast::TypeDefinition::Interface(_interface_type) => ctx.subgraphs.get_or_push_definition(
                        subgraph_id,
                        type_name,
                        DefinitionKind::Interface,
                        description,
                    ),
                    ast::TypeDefinition::Union(_) => {
                        ctx.subgraphs
                            .get_or_push_definition(subgraph_id, type_name, DefinitionKind::Union, description)
                    }
                    ast::TypeDefinition::InputObject(_) => ctx.subgraphs.get_or_push_definition(
                        subgraph_id,
                        type_name,
                        DefinitionKind::InputObject,
                        description,
                    ),

                    ast::TypeDefinition::Scalar(_) => ctx.subgraphs.get_or_push_definition(
                        subgraph_id,
                        type_name,
                        DefinitionKind::Scalar,
                        description,
                    ),

                    ast::TypeDefinition::Enum(_enum_type) => {
                        ctx.subgraphs
                            .get_or_push_definition(subgraph_id, type_name, DefinitionKind::Enum, description)
                    }
                };

                let directive_site_id = ctx.subgraphs.at(definition_id).directives;
                directives::ingest_directives(ctx, directive_site_id, type_definition.directives(), |_| {
                    type_name.to_owned()
                });

                directives::ingest_keys(definition_id, type_definition.directives(), ctx);
            }
            ast::Definition::Directive(_) | ast::Definition::Schema(_) | ast::Definition::SchemaExtension(_) => (),
        }
    }
}

fn ingest_definition_bodies(ctx: &mut Context<'_>) {
    let document = ctx.document;
    let subgraph_id = ctx.subgraph_id;

    let type_definitions = document.definitions().filter_map(|def| match def {
        ast::Definition::Type(ty) | ast::Definition::TypeExtension(ty) => Some(ty),
        _ => None,
    });

    for definition in type_definitions {
        match definition {
            ast::TypeDefinition::Union(_) if definition.name() == ENTITY_UNION_NAME => continue,
            ast::TypeDefinition::Union(union) => {
                let Some(union_id) = ctx.subgraphs.definition_by_name(definition.name(), subgraph_id) else {
                    ctx.subgraphs.push_ingestion_diagnostic(
                        subgraph_id,
                        format!(
                            "Union type `{}` is used but not defined in the subgraph.",
                            definition.name()
                        ),
                    );
                    continue;
                };

                for member in union.members() {
                    let Some(member_id) = ctx.subgraphs.definition_by_name(member.name(), subgraph_id) else {
                        ctx.subgraphs.push_ingestion_diagnostic(
                            subgraph_id,
                            format!(
                                "Union member `{}` is used but not defined in the subgraph.",
                                member.name()
                            ),
                        );
                        continue;
                    };
                    ctx.subgraphs.push_union_member(union_id, member_id);
                }
            }
            ast::TypeDefinition::InputObject(input_object) => {
                let Some(definition_id) = ctx.subgraphs.definition_by_name(definition.name(), subgraph_id) else {
                    ctx.subgraphs.push_ingestion_diagnostic(
                        subgraph_id,
                        format!(
                            "Input object type `{}` is used but not defined in the subgraph.",
                            definition.name()
                        ),
                    );
                    continue;
                };
                fields::ingest_input_fields(ctx, definition_id, input_object.fields());
            }
            ast::TypeDefinition::Interface(interface) => {
                let Some(definition_id) = ctx.subgraphs.definition_by_name(interface.name(), subgraph_id) else {
                    ctx.subgraphs.push_ingestion_diagnostic(
                        subgraph_id,
                        format!(
                            "Interface type `{}` is used but not defined in the subgraph.",
                            interface.name()
                        ),
                    );
                    continue;
                };

                for implemented_interface in interface.implements_interfaces() {
                    let Some(implemented_interface) =
                        ctx.subgraphs.definition_by_name(implemented_interface, subgraph_id)
                    else {
                        ctx.subgraphs.push_ingestion_diagnostic(
                            subgraph_id,
                            format!(
                                "Interface `{}` implements `{}`, but `{}` is not defined in the subgraph.",
                                interface.name(),
                                implemented_interface,
                                implemented_interface
                            ),
                        );
                        continue;
                    };

                    ctx.subgraphs.push_interface_impl(definition_id, implemented_interface);
                }

                let is_query_root_type = false; // interfaces can't be at the root

                fields::ingest_fields(ctx, definition_id, interface.fields(), is_query_root_type);
            }
            ast::TypeDefinition::Object(_) if definition.name() == SERVICE_TYPE_NAME => continue,
            ast::TypeDefinition::Object(object_type) => {
                let Some(definition_id) = ctx.subgraphs.definition_by_name(definition.name(), subgraph_id) else {
                    ctx.subgraphs.push_ingestion_diagnostic(
                        subgraph_id,
                        format!(
                            "Object type `{}` is used but not defined in the subgraph.",
                            definition.name()
                        ),
                    );
                    continue;
                };

                for implemented_interface in object_type.implements_interfaces() {
                    let Some(implemented_interface) =
                        ctx.subgraphs.definition_by_name(implemented_interface, subgraph_id)
                    else {
                        ctx.subgraphs.push_ingestion_diagnostic(
                            subgraph_id,
                            format!(
                                "Object type `{}` implements `{}`, but `{}` is not defined in the subgraph.",
                                object_type.name(),
                                implemented_interface,
                                implemented_interface
                            ),
                        );
                        continue;
                    };

                    ctx.subgraphs.push_interface_impl(definition_id, implemented_interface);
                }

                let is_query_root_type = ctx.root_type_matcher.is_query(definition.name());

                fields::ingest_fields(ctx, definition_id, object_type.fields(), is_query_root_type);
            }
            ast::TypeDefinition::Enum(enum_definition) => {
                let Some(enum_id) = ctx.subgraphs.definition_by_name(definition.name(), subgraph_id) else {
                    ctx.subgraphs.push_ingestion_diagnostic(
                        subgraph_id,
                        format!(
                            "Union type `{}` is used but not defined in the subgraph.",
                            definition.name()
                        ),
                    );
                    continue;
                };

                enums::ingest_enum_values(ctx, enum_id, enum_definition);
            }
            _ => (),
        }
    }
}

pub(super) fn ast_value_to_subgraph_value(value: ConstValue<'_>, subgraphs: &mut Subgraphs) -> subgraphs::Value {
    match &value {
        ConstValue::Null(_) => subgraphs::Value::Null,
        ConstValue::Int(n) => subgraphs::Value::Int(n.as_i64()),
        ConstValue::Float(n) => subgraphs::Value::Float(n.as_f64()),
        ConstValue::String(s) => subgraphs::Value::String(subgraphs.strings.intern(s.as_str())),
        ConstValue::Boolean(b) => subgraphs::Value::Boolean(b.value()),
        ConstValue::Enum(e) => subgraphs::Value::Enum(subgraphs.strings.intern(e.name())),
        ConstValue::List(l) => {
            subgraphs::Value::List(l.items().map(|v| ast_value_to_subgraph_value(v, subgraphs)).collect())
        }
        ConstValue::Object(o) => subgraphs::Value::Object(
            o.fields()
                .map(|field| {
                    (
                        subgraphs.strings.intern(field.name()),
                        ast_value_to_subgraph_value(field.value(), subgraphs),
                    )
                })
                .collect(),
        ),
    }
}