# frozen_string_literal: true require "graphql/schema/build_from_definition/resolve_map" module GraphQL class Schema module BuildFromDefinition class << self def from_definition(definition_string, default_resolve:, parser: DefaultParser) document = parser.parse(definition_string) Builder.build(document, default_resolve: default_resolve) end end # @api private DefaultParser = GraphQL::Language::Parser # @api private module DefaultResolve def self.call(type, field, obj, args, ctx) if field.arguments.any? obj.public_send(field.name, args, ctx) else obj.public_send(field.name) end end end # @api private module Builder extend self def build(document, default_resolve: DefaultResolve) raise InvalidDocumentError.new('Must provide a document ast.') if !document || !document.is_a?(GraphQL::Language::Nodes::Document) if default_resolve.is_a?(Hash) default_resolve = ResolveMap.new(default_resolve) end schema_definition = nil types = {} types.merge!(GraphQL::Schema::BUILT_IN_TYPES) directives = {} type_resolver = ->(type) { -> { resolve_type(types, type) } } document.definitions.each do |definition| case definition when GraphQL::Language::Nodes::SchemaDefinition raise InvalidDocumentError.new('Must provide only one schema definition.') if schema_definition schema_definition = definition when GraphQL::Language::Nodes::EnumTypeDefinition types[definition.name] = build_enum_type(definition, type_resolver).graphql_definition when GraphQL::Language::Nodes::ObjectTypeDefinition types[definition.name] = build_object_type(definition, type_resolver, default_resolve: default_resolve).graphql_definition when GraphQL::Language::Nodes::InterfaceTypeDefinition types[definition.name] = build_interface_type(definition, type_resolver).graphql_definition when GraphQL::Language::Nodes::UnionTypeDefinition types[definition.name] = build_union_type(definition, type_resolver).graphql_definition when GraphQL::Language::Nodes::ScalarTypeDefinition types[definition.name] = build_scalar_type(definition, type_resolver, default_resolve: default_resolve).graphql_definition when GraphQL::Language::Nodes::InputObjectTypeDefinition types[definition.name] = build_input_object_type(definition, type_resolver) when GraphQL::Language::Nodes::DirectiveDefinition directives[definition.name] = build_directive(definition, type_resolver) end end directives = GraphQL::Schema.default_directives.merge(directives) 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] query_root_type = types[schema_definition.query] end if schema_definition.mutation raise InvalidDocumentError.new("Specified mutation type \"#{schema_definition.mutation}\" not found in document.") unless types[schema_definition.mutation] mutation_root_type = types[schema_definition.mutation] end if schema_definition.subscription raise InvalidDocumentError.new("Specified subscription type \"#{schema_definition.subscription}\" not found in document.") unless types[schema_definition.subscription] subscription_root_type = types[schema_definition.subscription] end else query_root_type = types['Query'] mutation_root_type = types['Mutation'] subscription_root_type = types['Subscription'] end raise InvalidDocumentError.new('Must provide schema definition with query type or a type named Query.') unless query_root_type Class.new(GraphQL::Schema) do query query_root_type mutation mutation_root_type subscription subscription_root_type orphan_types types.values if default_resolve.respond_to?(:resolve_type) define_singleton_method(:resolve_type) do |*args| default_resolve.resolve_type(*args) end else define_singleton_method(:resolve_type) do |*args| NullResolveType.call(*args) end end directives directives.values if schema_definition ast_node(schema_definition) end # Load caches, check for errors graphql_definition end end NullResolveType = ->(type, obj, ctx) { raise(NotImplementedError, "Generated Schema cannot use Interface or Union types for execution. Implement resolve_type on your resolver.") } def build_enum_type(enum_type_definition, type_resolver) builder = self Class.new(GraphQL::Schema::Enum) do graphql_name(enum_type_definition.name) description(enum_type_definition.description) ast_node(enum_type_definition) enum_type_definition.values.each do |enum_value_definition| value(enum_value_definition.name, value: enum_value_definition.name, deprecation_reason: builder.build_deprecation_reason(enum_value_definition.directives), description: enum_value_definition.description, ast_node: enum_value_definition, ) end end end def build_deprecation_reason(directives) deprecated_directive = directives.find{ |d| d.name == 'deprecated' } return unless deprecated_directive reason = deprecated_directive.arguments.find{ |a| a.name == 'reason' } return GraphQL::Directive::DEFAULT_DEPRECATION_REASON unless reason reason.value end def build_scalar_type(scalar_type_definition, type_resolver, default_resolve:) Class.new(GraphQL::Schema::Scalar) do graphql_name(scalar_type_definition.name) description(scalar_type_definition.description) ast_node(scalar_type_definition) if default_resolve.respond_to?(:coerce_input) define_singleton_method(:coerce_input) do |val, ctx| default_resolve.coerce_input(self, val, ctx) end define_singleton_method(:coerce_result) do |val, ctx| default_resolve.coerce_result(self, val, ctx) end end end end def build_union_type(union_type_definition, type_resolver) Class.new(GraphQL::Schema::Union) do graphql_name(union_type_definition.name) description(union_type_definition.description) possible_types(*union_type_definition.types.map { |type_name| type_resolver.call(type_name) }) ast_node(union_type_definition) end end def build_object_type(object_type_definition, type_resolver, default_resolve:) builder = self type_def = nil typed_resolve_fn = ->(field, obj, args, ctx) { default_resolve.call(type_def.graphql_definition, field, obj, args, ctx) } Class.new(GraphQL::Schema::Object) do type_def = self graphql_name(object_type_definition.name) description(object_type_definition.description) ast_node(object_type_definition) object_type_definition.interfaces.each do |interface_name| interface_defn = type_resolver.call(interface_name) implements(interface_defn) end builder.build_fields(self, object_type_definition.fields, type_resolver, default_resolve: typed_resolve_fn) end end def build_input_object_type(input_object_type_definition, type_resolver) builder = self Class.new(GraphQL::Schema::InputObject) do graphql_name(input_object_type_definition.name) description(input_object_type_definition.description) ast_node(input_object_type_definition) builder.build_arguments(self, input_object_type_definition.fields, type_resolver) end end def build_default_value(default_value) case default_value when GraphQL::Language::Nodes::Enum default_value.name when GraphQL::Language::Nodes::NullValue nil when GraphQL::Language::Nodes::InputObject default_value.to_h when Array default_value.map { |v| build_default_value(v) } else default_value end end def build_arguments(type_class, arguments, type_resolver) builder = self arguments.each do |argument_defn| default_value_kwargs = {} if !argument_defn.default_value.nil? default_value_kwargs[:default_value] = builder.build_default_value(argument_defn.default_value) end type_class.argument( argument_defn.name, type: type_resolver.call(argument_defn.type), required: false, description: argument_defn.description, ast_node: argument_defn, camelize: false, method_access: false, **default_value_kwargs ) end end def build_directive(directive_definition, type_resolver) builder = self Class.new(GraphQL::Schema::Directive) do graphql_name(directive_definition.name) description(directive_definition.description) locations(*directive_definition.locations.map { |location| location.name.to_sym }) ast_node(directive_definition) builder.build_arguments(self, directive_definition.arguments, type_resolver) end end def build_interface_type(interface_type_definition, type_resolver) builder = self Module.new do include GraphQL::Schema::Interface graphql_name(interface_type_definition.name) description(interface_type_definition.description) ast_node(interface_type_definition) builder.build_fields(self, interface_type_definition.fields, type_resolver, default_resolve: nil) end end def build_fields(owner, field_definitions, type_resolver, default_resolve:) builder = self field_definitions.map do |field_definition| type_name = resolve_type_name(field_definition.type) field = owner.field( field_definition.name, description: field_definition.description, type: type_resolver.call(field_definition.type), null: true, connection: type_name.end_with?("Connection"), resolve: ->(obj, args, ctx) { default_resolve.call(field.graphql_definition, obj, args, ctx) }, deprecation_reason: build_deprecation_reason(field_definition.directives), ast_node: field_definition, method_conflict_warning: false, camelize: false, ) do builder.build_arguments(self, field_definition.arguments, type_resolver) end end end def resolve_type(types, ast_node) type = GraphQL::Schema::TypeExpression.build_type(types, ast_node) if type.nil? while ast_node.respond_to?(:of_type) ast_node = ast_node.of_type end raise InvalidDocumentError.new("Type \"#{ast_node.name}\" not found in document.") end type end def resolve_type_name(type) case type when GraphQL::Language::Nodes::TypeName return type.name else resolve_type_name(type.of_type) end end end private_constant :Builder end end end