graphql-schema-diff 0.5.0

Semantic diffing for GraphQL schemas
Documentation
use crate::{DiffMap, DiffState, ast};
use std::{collections::hash_map::Entry, hash::Hash};

/// Traverse the source and target schemas, populating the `DiffState`.
pub(crate) fn traverse_schemas<'a>(
    [source, target]: [Option<&'a ast::TypeSystemDocument>; 2],
    state: &mut DiffState<'a>,
) {
    let [source_definitions_len, target_definitions_len] =
        [source, target].map(|schema| schema.map(|schema| schema.definitions().len()).unwrap_or_default());
    let schema_size_approx = source_definitions_len.max(target_definitions_len);
    state.types_map.reserve(schema_size_approx);
    state.fields_map.reserve(schema_size_approx);

    if let Some(source) = source {
        traverse_source(source, state);
    }

    if let Some(target) = target {
        traverse_target(target, state);
    }
}

fn traverse_source<'a>(source: &'a ast::TypeSystemDocument, state: &mut DiffState<'a>) {
    for definition in source.definitions() {
        match definition {
            ast::Definition::SchemaExtension(def) => {
                state.schema_extensions.push([Some(def), None]);
            }
            ast::Definition::Schema(def) => {
                state.schema_definition_map[0] = Some(def);
            }
            ast::Definition::Directive(directive_def) => {
                insert_source(&mut state.types_map, directive_def.name(), definition);
            }
            ast::Definition::Type(tpe) | ast::Definition::TypeExtension(tpe) => {
                let type_name = tpe.name();

                match &tpe {
                    ast::TypeDefinition::Scalar(scalar) => {
                        state.types_map.insert(type_name, (Some(definition), None));
                        state.type_directives.entry(type_name).or_default()[0].extend(scalar.directives());
                    }
                    ast::TypeDefinition::Object(obj) => {
                        state.types_map.insert(type_name, (Some(definition), None));
                        state.type_directives.entry(type_name).or_default()[0].extend(obj.directives());
                        insert_source(
                            &mut state.interface_impls,
                            type_name,
                            obj.implements_interfaces().collect(),
                        );

                        for field in obj.fields() {
                            let field_name = field.name();

                            insert_source(
                                &mut state.fields_map,
                                [type_name, field_name],
                                (Some(field.ty()), field.span().into()),
                            );
                            state.field_directives.entry([type_name, field_name]).or_default()[0]
                                .extend(field.directives());
                            state.field_descriptions.entry([type_name, field_name]).or_default()[0] =
                                field.description();

                            let mut args = field.arguments();
                            fill_args_src(&mut state.arguments_map, type_name, field_name, &mut args);

                            for arg in field.arguments() {
                                state
                                    .argument_directives
                                    .entry([type_name, field_name, arg.name()])
                                    .or_default()[0]
                                    .extend(arg.directives());
                            }
                        }
                    }
                    ast::TypeDefinition::Interface(iface) => {
                        state.types_map.insert(type_name, (Some(definition), None));
                        state.type_directives.entry(type_name).or_default()[0].extend(iface.directives());
                        insert_source(
                            &mut state.interface_impls,
                            type_name,
                            iface.implements_interfaces().collect(),
                        );

                        for field in iface.fields() {
                            let field_name = field.name();

                            insert_source(
                                &mut state.fields_map,
                                [type_name, field_name],
                                (Some(field.ty()), field.span().into()),
                            );
                            state.field_directives.entry([type_name, field_name]).or_default()[0]
                                .extend(field.directives());
                            state.field_descriptions.entry([type_name, field_name]).or_default()[0] =
                                field.description();

                            fill_args_src(&mut state.arguments_map, type_name, field_name, &mut field.arguments());

                            for arg in field.arguments() {
                                state
                                    .argument_directives
                                    .entry([type_name, field_name, arg.name()])
                                    .or_default()[0]
                                    .extend(arg.directives());
                            }
                        }
                    }
                    ast::TypeDefinition::Union(union) => {
                        state.types_map.insert(type_name, (Some(definition), None));
                        state.type_directives.entry(type_name).or_default()[0].extend(union.directives());

                        for member in union.members() {
                            insert_source(
                                &mut state.fields_map,
                                [type_name, member.name()],
                                (None, member.span().into()),
                            );
                        }
                    }
                    ast::TypeDefinition::Enum(enm) => {
                        state.types_map.insert(type_name, (Some(definition), None));
                        state.type_directives.entry(type_name).or_default()[0].extend(enm.directives());

                        for value in enm.values() {
                            insert_source(
                                &mut state.fields_map,
                                [type_name, value.value()],
                                (None, value.span().into()),
                            );
                            state.field_directives.entry([type_name, value.value()]).or_default()[0]
                                .extend(value.directives());
                            state.field_descriptions.entry([type_name, value.value()]).or_default()[0] =
                                value.description();
                        }
                    }
                    ast::TypeDefinition::InputObject(input) => {
                        state.types_map.insert(type_name, (Some(definition), None));
                        state.type_directives.entry(type_name).or_default()[0].extend(input.directives());

                        for field in input.fields() {
                            insert_source(
                                &mut state.fields_map,
                                [type_name, field.name()],
                                (Some(field.ty()), field.span().into()),
                            );
                            state.field_directives.entry([type_name, field.name()]).or_default()[0]
                                .extend(field.directives());
                            state.field_descriptions.entry([type_name, field.name()]).or_default()[0] =
                                field.description();
                        }
                    }
                }
            }
        }
    }
}

fn traverse_target<'a>(target: &'a ast::TypeSystemDocument, state: &mut DiffState<'a>) {
    let mut schema_extensions_count = 0usize;

    for definition in target.definitions() {
        match definition {
            ast::Definition::SchemaExtension(def) => {
                if state.schema_extensions.len() == schema_extensions_count {
                    state.schema_extensions.push([None, Some(def)]);
                } else {
                    state.schema_extensions[schema_extensions_count][1] = Some(def);
                }

                schema_extensions_count += 1;
            }
            ast::Definition::Schema(def) => {
                state.schema_definition_map[1] = Some(def);
            }
            ast::Definition::Directive(directive_def) => {
                merge_target(state.types_map.entry(directive_def.name()), definition);
            }
            ast::Definition::Type(tpe) | ast::Definition::TypeExtension(tpe) => {
                let type_name = tpe.name();

                match tpe {
                    ast::TypeDefinition::Scalar(scalar) => {
                        state.types_map.entry(type_name).or_default().1 = Some(definition);
                        state.type_directives.entry(type_name).or_default()[1].extend(scalar.directives());
                    }
                    ast::TypeDefinition::Object(obj) => {
                        state.types_map.entry(type_name).or_default().1 = Some(definition);
                        state.type_directives.entry(type_name).or_default()[1].extend(obj.directives());
                        merge_target(
                            state.interface_impls.entry(type_name),
                            obj.implements_interfaces().collect(),
                        );

                        for field in obj.fields() {
                            merge_target(
                                state.fields_map.entry([type_name, field.name()]),
                                (Some(field.ty()), field.span().into()),
                            );
                            state.field_directives.entry([type_name, field.name()]).or_default()[1]
                                .extend(field.directives());
                            state.field_descriptions.entry([type_name, field.name()]).or_default()[1] =
                                field.description();
                            let mut args = field.arguments();
                            args_target(&mut state.arguments_map, type_name, field.name(), &mut args);

                            for arg in field.arguments() {
                                state
                                    .argument_directives
                                    .entry([type_name, field.name(), arg.name()])
                                    .or_default()[1]
                                    .extend(arg.directives());
                            }
                        }
                    }
                    ast::TypeDefinition::Interface(iface) => {
                        state.types_map.entry(type_name).or_default().1 = Some(definition);
                        state.type_directives.entry(type_name).or_default()[1].extend(iface.directives());
                        merge_target(
                            state.interface_impls.entry(type_name),
                            iface.implements_interfaces().collect(),
                        );

                        for field in iface.fields() {
                            let field_name = field.name();

                            merge_target(
                                state.fields_map.entry([type_name, field_name]),
                                (Some(field.ty()), field.span().into()),
                            );
                            state.field_directives.entry([type_name, field_name]).or_default()[1]
                                .extend(field.directives());
                            state.field_descriptions.entry([type_name, field_name]).or_default()[1] =
                                field.description();
                            args_target(&mut state.arguments_map, type_name, field_name, &mut field.arguments());

                            for arg in field.arguments() {
                                state
                                    .argument_directives
                                    .entry([type_name, field_name, arg.name()])
                                    .or_default()[1]
                                    .extend(arg.directives());
                            }
                        }
                    }
                    ast::TypeDefinition::Union(union) => {
                        state.types_map.entry(type_name).or_default().1 = Some(definition);
                        state.type_directives.entry(type_name).or_default()[1].extend(union.directives());

                        for member in union.members() {
                            merge_target(
                                state.fields_map.entry([type_name, member.name()]),
                                (None, member.span().into()),
                            );
                        }
                    }
                    ast::TypeDefinition::Enum(enm) => {
                        state.types_map.entry(type_name).or_default().1 = Some(definition);
                        state.type_directives.entry(type_name).or_default()[1].extend(enm.directives());

                        for value in enm.values() {
                            merge_target(
                                state.fields_map.entry([type_name, value.value()]),
                                (None, value.span().into()),
                            );
                            state.field_directives.entry([type_name, value.value()]).or_default()[1]
                                .extend(value.directives());
                            state.field_descriptions.entry([type_name, value.value()]).or_default()[1] =
                                value.description();
                        }
                    }
                    ast::TypeDefinition::InputObject(input) => {
                        state.types_map.entry(type_name).or_default().1 = Some(definition);
                        state.type_directives.entry(type_name).or_default()[1].extend(input.directives());

                        for field in input.fields() {
                            merge_target(
                                state.fields_map.entry([type_name, field.name()]),
                                (Some(field.ty()), field.span().into()),
                            );
                            state.field_directives.entry([type_name, field.name()]).or_default()[1]
                                .extend(field.directives());
                            state.field_descriptions.entry([type_name, field.name()]).or_default()[1] =
                                field.description();
                        }
                    }
                }
            }
        }
    }
}

// Insert the arguments of a field into the DiffState.
fn fill_args_src<'a>(
    arguments_map: &mut DiffMap<[&'a str; 3], ast::InputValueDefinition<'a>>,
    parent: &'a str,
    field: &'a str,
    args: &mut (dyn Iterator<Item = ast::InputValueDefinition<'a>> + 'a),
) {
    for arg in args {
        insert_source(arguments_map, [parent, field, (arg.name())], arg)
    }
}

// Merge the arguments of a field in the target schema into the DiffState.
fn args_target<'a>(
    arguments_map: &mut DiffMap<[&'a str; 3], ast::InputValueDefinition<'a>>,
    parent: &'a str,
    field: &'a str,
    args: &mut (dyn Iterator<Item = ast::InputValueDefinition<'a>> + 'a),
) {
    for arg in args {
        merge_target(arguments_map.entry([parent, field, arg.name()]), arg)
    }
}

fn insert_source<K: Hash + Eq, V>(map: &mut DiffMap<K, V>, key: K, source: V) {
    map.insert(key, (Some(source), None));
}

fn merge_target<K, V>(entry: Entry<'_, K, (Option<V>, Option<V>)>, target: V) {
    entry.or_default().1 = Some(target);
}