diff --git a/.gitignore b/.gitignore index 75af42eaa2..18ded80b82 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ gh-pages/ tmp/* __*.db node_modules/ +.byebug_history yarn.lock OperationStoreClient.js spec/integration/tmp diff --git a/lib/graphql/language/printer.rb b/lib/graphql/language/printer.rb index e1dda9835c..e0d45d4470 100644 --- a/lib/graphql/language/printer.rb +++ b/lib/graphql/language/printer.rb @@ -161,6 +161,12 @@ def print_scalar_type_definition(scalar_type) out << print_directives(scalar_type.directives) end + def print_scalar_type_extension(scalar_type) + out = ''.dup + out << "extend scalar #{scalar_type.name}" + out << print_directives(scalar_type.directives) + end + def print_object_type_definition(object_type) out = print_description(object_type) out << "type #{object_type.name}" @@ -169,6 +175,14 @@ def print_object_type_definition(object_type) out << print_field_definitions(object_type.fields) end + def print_object_type_extension(object_type) + out = ''.dup + out << "extend type #{object_type.name}" + out << " implements " << object_type.interfaces.map(&:name).join(" & ") unless object_type.interfaces.empty? + out << print_directives(object_type.directives) + out << print_field_definitions(object_type.fields) + end + def print_input_value_definition(input_value) out = "#{input_value.name}: #{print_node(input_value.type)}".dup out << " = #{print_node(input_value.default_value)}" unless input_value.default_value.nil? @@ -204,6 +218,13 @@ def print_interface_type_definition(interface_type) out << print_field_definitions(interface_type.fields) end + def print_interface_type_extension(interface_type) + out = ''.dup + out << "extend interface #{interface_type.name}" + out << print_directives(interface_type.directives) + out << print_field_definitions(interface_type.fields) + end + def print_union_type_definition(union_type) out = print_description(union_type) out << "union #{union_type.name}" @@ -211,6 +232,13 @@ def print_union_type_definition(union_type) out << " = " + union_type.types.map(&:name).join(" | ") end + def print_union_type_extension(union_type) + out = ''.dup + out << "extend union #{union_type.name}" + out << print_directives(union_type.directives) + out << " = " + union_type.types.map(&:name).join(" | ") + end + def print_enum_type_definition(enum_type) out = print_description(enum_type) out << "enum #{enum_type.name}#{print_directives(enum_type.directives)} {\n" @@ -221,6 +249,16 @@ def print_enum_type_definition(enum_type) out << "}" end + def print_enum_type_extension(enum_type) + out = ''.dup + out << "extend enum #{enum_type.name}#{print_directives(enum_type.directives)} {\n" + enum_type.values.each.with_index do |value, i| + out << print_description(value, indent: ' ', first_in_block: i == 0) + out << print_enum_value_definition(value) + end + out << "}" + end + def print_enum_value_definition(enum_value) out = " #{enum_value.name}".dup out << print_directives(enum_value.directives) @@ -239,6 +277,18 @@ def print_input_object_type_definition(input_object_type) out << "}" end + def print_input_object_type_extension(input_object_type) + out = ''.dup + out << "extend input #{input_object_type.name}" + out << print_directives(input_object_type.directives) + out << " {\n" + input_object_type.fields.each.with_index do |field, i| + out << print_description(field, indent: ' ', first_in_block: i == 0) + out << " #{print_input_value_definition(field)}\n" + end + out << "}" + end + def print_directive_definition(directive) out = print_description(directive) out << "directive @#{directive.name}" @@ -342,6 +392,18 @@ def print_node(node, indent: "") print_input_object_type_definition(node) when Nodes::DirectiveDefinition print_directive_definition(node) + when Nodes::ScalarTypeExtension + print_scalar_type_extension(node) + when Nodes::ObjectTypeExtension + print_object_type_extension(node) + when Nodes::InterfaceTypeExtension + print_interface_type_extension(node) + when Nodes::UnionTypeExtension + print_union_type_extension(node) + when Nodes::EnumTypeExtension + print_enum_type_extension(node) + when Nodes::InputObjectTypeExtension + print_input_object_type_extension(node) when FalseClass, Float, Integer, NilClass, String, TrueClass, Symbol GraphQL::Language.serialize(node) when Array diff --git a/lib/graphql/schema/build_from_definition.rb b/lib/graphql/schema/build_from_definition.rb index 0e52c31c31..cd8e9b64f2 100644 --- a/lib/graphql/schema/build_from_definition.rb +++ b/lib/graphql/schema/build_from_definition.rb @@ -40,6 +40,7 @@ def build(document, default_resolve: DefaultResolve) types = {} types.merge!(GraphQL::Schema::BUILT_IN_TYPES) directives = {} + extensions = [] type_resolver = ->(type) { -> { resolve_type(types, type) } } document.definitions.each do |definition| @@ -47,6 +48,13 @@ def build(document, default_resolve: DefaultResolve) when GraphQL::Language::Nodes::SchemaDefinition raise InvalidDocumentError.new('Must provide only one schema definition.') if schema_definition schema_definition = definition + when GraphQL::Language::Nodes::ScalarTypeExtension, + GraphQL::Language::Nodes::ObjectTypeExtension, + GraphQL::Language::Nodes::InterfaceTypeExtension, + GraphQL::Language::Nodes::UnionTypeExtension, + GraphQL::Language::Nodes::EnumTypeExtension, + GraphQL::Language::Nodes::InputObjectTypeExtension + extensions << definition when GraphQL::Language::Nodes::EnumTypeDefinition types[definition.name] = build_enum_type(definition, type_resolver) when GraphQL::Language::Nodes::ObjectTypeDefinition @@ -66,6 +74,32 @@ def build(document, default_resolve: DefaultResolve) directives = GraphQL::Schema.default_directives.merge(directives) + for ext in extensions + type = types[ext.name] + case ext + when GraphQL::Language::Nodes::ScalarTypeExtension + # TODO: Where are the directives on the scalar type? + when GraphQL::Language::Nodes::ObjectTypeExtension + type.fields.merge! Hash[build_fields(ext.fields, type_resolver, default_resolve: default_resolve)] + type.interfaces.concat ext.interfaces.map{ |interface_name| type_resolver.call(interface_name) } + when GraphQL::Language::Nodes::InterfaceTypeExtension + type.fields.merge! Hash[build_fields(ext.fields, type_resolver, default_resolve: nil)] + when GraphQL::Language::Nodes::UnionTypeExtension + type.possible_types.concat ext.types.map { |x|types[x.name] } + when GraphQL::Language::Nodes::EnumTypeExtension + ext.values.each do |enum_value_definition| + type.add_value EnumType::EnumValue.define( + name: enum_value_definition.name, + value: enum_value_definition.name, + deprecation_reason: build_deprecation_reason(enum_value_definition.directives), + description: enum_value_definition.description, + ) + end + when GraphQL::Language::Nodes::InputObjectTypeExtension + type.arguments.merge! Hash[build_input_arguments(ext, type_resolver)] + end + end + if schema_definition if schema_definition.query raise InvalidDocumentError.new("Specified query type \"#{schema_definition.query}\" not found in document.") unless types[schema_definition.query] diff --git a/spec/graphql/language/extend_document_from_schema_definition_spec.rb b/spec/graphql/language/extend_document_from_schema_definition_spec.rb new file mode 100644 index 0000000000..6d1b169823 --- /dev/null +++ b/spec/graphql/language/extend_document_from_schema_definition_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true +require "spec_helper" + +describe GraphQL::Language::DocumentFromSchemaDefinition do + let(:subject) { GraphQL::Language::DocumentFromSchemaDefinition } + + describe "#document" do + let(:schema_idl) { <<-GRAPHQL + scalar SomeScalar + extend scalar SomeScalar @foo + + type Foo { + name: String + } + + type User { + name: String + } + + type Bar { + name: String + } + + union SomeUnion = User | Foo + extend union SomeUnion = Bar + + extend type User { + email: String! + } + + interface SomeInterface { + otherField: String + } + + extend interface SomeInterface { + newField: String + } + + input SomeInput { + fooArg: String + newField: String + } + + extend input SomeInput { + newField: String + } + + enum SomeEnum { + ONE + } + + extend enum SomeEnum { + NEW_ENUM + } + + type Query { + user: User + } + + GRAPHQL + } + + let(:expected_document) { GraphQL.parse(schema_idl) } + let(:schema) { GraphQL::Schema.from_definition(schema_idl) } + + let(:document) { + subject.new( + schema + ).document + } + + it "returns the IDL without introspection, built ins and schema root" do + assert equivalent_node?(expected_document, document) + end + + it "print GraphQL::Language::Document with extend" do + document = GraphQL.parse(schema_idl) + GraphQL::Language::Printer.new.print(document) + end + + it "generates GraphQL::Language::Document from the GraphQL::Schema build" do + end + end + + private + + def equivalent_node?(expected, node) + return false unless expected.is_a?(node.class) + + if expected.respond_to?(:children) && expected.respond_to?(:scalars) + children_equal = expected.children.all? do |expected_child| + node.children.find { |child| equivalent_node?(expected_child, child) } + end + + scalars_equal = expected.children.all? do |expected_child| + node.children.find { |child| equivalent_node?(expected_child, child) } + end + + children_equal && scalars_equal + else + expected == node + end + end +end