# frozen_string_literal: true module GraphQL class Schema # You can use the result of {GraphQL::Introspection::INTROSPECTION_QUERY} # to make a schema. This schema is missing some important details like # `resolve` functions, but it does include the full type system, # so you can use it to validate queries. # # @see GraphQL::Schema.from_introspection for a public API module Loader extend self # Create schema with the result of an introspection query. # @param introspection_result [Hash] A response from {GraphQL::Introspection::INTROSPECTION_QUERY} # @return [Class] the schema described by `input` def load(introspection_result) schema = introspection_result.fetch("data").fetch("__schema") types = {} type_resolver = ->(type) { resolve_type(types, type) } schema.fetch("types").each do |type| next if type.fetch("name").start_with?("__") type_object = define_type(type, type_resolver) types[type["name"]] = type_object end Class.new(GraphQL::Schema) do orphan_types(types.values) def self.resolve_type(*) raise(GraphQL::RequiredImplementationMissingError, "This schema was loaded from string, so it can't resolve types for objects") end [:query, :mutation, :subscription].each do |root| type = schema["#{root}Type"] if type type_defn = types.fetch(type.fetch("name")) self.public_send(root, type_defn) end end end end NullScalarCoerce = ->(val, _ctx) { val } class << self private def resolve_type(types, type) case kind = type.fetch("kind") when "ENUM", "INTERFACE", "INPUT_OBJECT", "OBJECT", "SCALAR", "UNION" type_name = type.fetch("name") type = types[type_name] || Schema::BUILT_IN_TYPES[type_name] if type.nil? GraphQL::Schema::LateBoundType.new(type_name) else type end when "LIST" Schema::List.new(resolve_type(types, type.fetch("ofType"))) when "NON_NULL" Schema::NonNull.new(resolve_type(types, type.fetch("ofType"))) else fail GraphQL::RequiredImplementationMissingError, "#{kind} not implemented" end end def extract_default_value(default_value_str, input_value_ast) case input_value_ast when String, Integer, Float, TrueClass, FalseClass input_value_ast when GraphQL::Language::Nodes::Enum input_value_ast.name when GraphQL::Language::Nodes::NullValue nil when GraphQL::Language::Nodes::InputObject input_value_ast.to_h when Array input_value_ast.map { |element| extract_default_value(default_value_str, element) } else raise( "Encountered unexpected type when loading default value. "\ "input_value_ast.class is #{input_value_ast.class} "\ "default_value is #{default_value_str}" ) end end def define_type(type, type_resolver) loader = self case type.fetch("kind") when "ENUM" Class.new(GraphQL::Schema::Enum) do graphql_name(type["name"]) description(type["description"]) type["enumValues"].each do |enum_value| value( enum_value["name"], description: enum_value["description"], deprecation_reason: enum_value["deprecation_reason"], ) end end when "INTERFACE" Module.new do include GraphQL::Schema::Interface graphql_name(type["name"]) description(type["description"]) loader.build_fields(self, type["fields"] || [], type_resolver) end when "INPUT_OBJECT" Class.new(GraphQL::Schema::InputObject) do graphql_name(type["name"]) description(type["description"]) loader.build_arguments(self, type["inputFields"], type_resolver) end when "OBJECT" Class.new(GraphQL::Schema::Object) do graphql_name(type["name"]) description(type["description"]) if type["interfaces"] type["interfaces"].each do |interface_type| implements(type_resolver.call(interface_type)) end end loader.build_fields(self, type["fields"], type_resolver) end when "SCALAR" type_name = type.fetch("name") if (builtin = GraphQL::Schema::BUILT_IN_TYPES[type_name]) builtin else Class.new(GraphQL::Schema::Scalar) do graphql_name(type["name"]) description(type["description"]) end end when "UNION" Class.new(GraphQL::Schema::Union) do graphql_name(type["name"]) description(type["description"]) possible_types(*(type["possibleTypes"].map { |pt| type_resolver.call(pt) })) end else fail GraphQL::RequiredImplementationMissingError, "#{type["kind"]} not implemented" end end public def build_fields(type_defn, fields, type_resolver) loader = self fields.each do |field_hash| type_defn.field( field_hash["name"], type: type_resolver.call(field_hash["type"]), description: field_hash["description"], null: true, camelize: false, ) do if field_hash["args"].any? loader.build_arguments(self, field_hash["args"], type_resolver) end end end end def build_arguments(arg_owner, args, type_resolver) args.each do |arg| kwargs = { type: type_resolver.call(arg["type"]), description: arg["description"], required: false, method_access: false, camelize: false, } if arg["defaultValue"] default_value_str = arg["defaultValue"] dummy_query_str = "query getStuff($var: InputObj = #{default_value_str}) { __typename }" # Returns a `GraphQL::Language::Nodes::Document`: dummy_query_ast = GraphQL.parse(dummy_query_str) # Reach into the AST for the default value: input_value_ast = dummy_query_ast.definitions.first.variables.first.default_value kwargs[:default_value] = extract_default_value(default_value_str, input_value_ast) end arg_owner.argument(arg["name"], **kwargs) end end end end end end