File: introspection_system.rb

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (166 lines) | stat: -rw-r--r-- 5,442 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# frozen_string_literal: true
module GraphQL
  class Schema
    class IntrospectionSystem
      attr_reader :types, :possible_types

      def initialize(schema)
        @schema = schema
        @class_based = !!@schema.is_a?(Class)
        @built_in_namespace = GraphQL::Introspection
        @custom_namespace = if @class_based
          schema.introspection || @built_in_namespace
        else
          schema.introspection_namespace || @built_in_namespace
        end

        type_defns = [
          load_constant(:SchemaType),
          load_constant(:TypeType),
          load_constant(:FieldType),
          load_constant(:DirectiveType),
          load_constant(:EnumValueType),
          load_constant(:InputValueType),
          load_constant(:TypeKindEnum),
          load_constant(:DirectiveLocationEnum)
        ]
        @types = {}
        @possible_types = {}
        type_defns.each do |t|
          @types[t.graphql_name] = t
          @possible_types[t.graphql_name] = [t]
        end
        @entry_point_fields =
          if schema.disable_introspection_entry_points?
            {}
          else
            entry_point_fields = get_fields_from_class(class_sym: :EntryPoints)
            entry_point_fields.delete('__schema') if schema.disable_schema_introspection_entry_point?
            entry_point_fields.delete('__type') if schema.disable_type_introspection_entry_point?
            entry_point_fields
          end
        @entry_point_fields.each { |k, v| v.dynamic_introspection = true }
        @dynamic_fields = get_fields_from_class(class_sym: :DynamicFields)
        @dynamic_fields.each { |k, v| v.dynamic_introspection = true }
      end

      def entry_points
        @entry_point_fields.values
      end

      def entry_point(name:)
        @entry_point_fields[name]
      end

      def dynamic_fields
        @dynamic_fields.values
      end

      def dynamic_field(name:)
        @dynamic_fields[name]
      end

      # The introspection system is prepared with a bunch of LateBoundTypes.
      # Replace those with the objects that they refer to, since LateBoundTypes
      # aren't handled at runtime.
      #
      # @api private
      # @return void
      def resolve_late_bindings
        @types.each do |name, t|
          if t.kind.fields?
            t.fields.each do |_name, field_defn|
              field_defn.type = resolve_late_binding(field_defn.type)
            end
          end
        end

        @entry_point_fields.each do |name, f|
          f.type = resolve_late_binding(f.type)
        end

        @dynamic_fields.each do |name, f|
          f.type = resolve_late_binding(f.type)
        end
        nil
      end

      private

      def resolve_late_binding(late_bound_type)
        case late_bound_type
        when GraphQL::Schema::LateBoundType
          @schema.get_type(late_bound_type.name)
        when GraphQL::Schema::List
          resolve_late_binding(late_bound_type.of_type).to_list_type
        when GraphQL::Schema::NonNull
          resolve_late_binding(late_bound_type.of_type).to_non_null_type
        when Module
          # It's a normal type -- no change required
          late_bound_type
        else
          raise "Invariant: unexpected type: #{late_bound_type} (#{late_bound_type.class})"
        end
      end

      def load_constant(class_name)
        const = @custom_namespace.const_get(class_name)
        dup_type_class(const)
      rescue NameError
        # Dup the built-in so that the cached fields aren't shared
        dup_type_class(@built_in_namespace.const_get(class_name))
      end

      def get_fields_from_class(class_sym:)
        object_type_defn = load_constant(class_sym)

        if object_type_defn.is_a?(Module)
          object_type_defn.fields
        else
          extracted_field_defns = {}
          object_class = object_type_defn.metadata[:type_class]
          object_type_defn.all_fields.each do |field_defn|
            inner_resolve = field_defn.resolve_proc
            resolve_with_instantiate = PerFieldProxyResolve.new(object_class: object_class, inner_resolve: inner_resolve)
            extracted_field_defns[field_defn.name] = field_defn.redefine(resolve: resolve_with_instantiate)
          end
          extracted_field_defns
        end
      end

      # This is probably not 100% robust -- but it has to be good enough to avoid modifying the built-in introspection types
      def dup_type_class(type_class)
        type_name = type_class.graphql_name
        Class.new(type_class) do
          # This won't be inherited like other things will
          graphql_name(type_name)

          if type_class.kind.fields?
            type_class.fields.each do |_name, field_defn|
              dup_field = field_defn.dup
              dup_field.owner = self
              add_field(dup_field)
            end
          end
        end
      end

      class PerFieldProxyResolve
        def initialize(object_class:, inner_resolve:)
          @object_class = object_class
          @inner_resolve = inner_resolve
        end

        def call(obj, args, ctx)
          query_ctx = ctx.query.context
          # Remove the QueryType wrapper
          if obj.is_a?(GraphQL::Schema::Object)
            obj = obj.object
          end
          wrapped_object = @object_class.wrap(obj, query_ctx)
          @inner_resolve.call(wrapped_object, args, ctx)
        end
      end
    end
  end
end