graphqlite 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,380 @@
1
+ module GraphQLite
2
+ # Executor executes GraphQL operations against a schema
3
+ class Executor
4
+ attr_reader :schema
5
+
6
+ def initialize(schema)
7
+ @schema = schema
8
+ end
9
+
10
+ def execute(document, variables: {}, operation_name: nil, context: {})
11
+ # Find the operation to execute
12
+ operation = find_operation(document, operation_name)
13
+ raise ExecutionError, "No operation found" unless operation
14
+
15
+ # Get the root type based on operation type
16
+ root_type = case operation.operation_type
17
+ when 'query'
18
+ @schema.query_type
19
+ when 'mutation'
20
+ @schema.mutation_type
21
+ when 'subscription'
22
+ @schema.subscription_type
23
+ end
24
+
25
+ raise ExecutionError, "Schema does not support #{operation.operation_type}" unless root_type
26
+
27
+ # Coerce variable values
28
+ variable_values = coerce_variable_values(operation.variable_definitions, variables)
29
+
30
+ # Execute the operation
31
+ data = execute_selection_set(
32
+ operation.selection_set,
33
+ root_type,
34
+ nil, # root value
35
+ variable_values,
36
+ context
37
+ )
38
+
39
+ { 'data' => data }
40
+ rescue ExecutionError => e
41
+ { 'data' => nil, 'errors' => [{ 'message' => e.message }] }
42
+ end
43
+
44
+ private
45
+
46
+ def find_operation(document, operation_name)
47
+ operations = document.definitions.select { |d| d.is_a?(Parser::OperationDefinition) }
48
+
49
+ if operation_name
50
+ operations.find { |op| op.name == operation_name }
51
+ elsif operations.length == 1
52
+ operations.first
53
+ else
54
+ raise ExecutionError, "Must provide operation name if query contains multiple operations"
55
+ end
56
+ end
57
+
58
+ def coerce_variable_values(variable_definitions, variables)
59
+ return {} if variable_definitions.empty?
60
+
61
+ variable_values = {}
62
+
63
+ variable_definitions.each do |var_def|
64
+ var_name = var_def.variable.name
65
+ var_type = resolve_type_ref(var_def.type)
66
+ default_value = var_def.default_value
67
+
68
+ if variables.key?(var_name) || variables.key?(var_name.to_sym)
69
+ value = variables[var_name] || variables[var_name.to_sym]
70
+ variable_values[var_name] = coerce_input_value(value, var_type)
71
+ elsif default_value
72
+ variable_values[var_name] = coerce_literal_value(default_value, var_type)
73
+ elsif var_type.is_a?(Types::NonNullType)
74
+ raise ExecutionError, "Variable $#{var_name} is required but not provided"
75
+ end
76
+ end
77
+
78
+ variable_values
79
+ end
80
+
81
+ def execute_selection_set(selection_set, object_type, object_value, variable_values, context)
82
+ return nil unless selection_set
83
+
84
+ # Collect fields
85
+ fields = collect_fields(selection_set, object_type, variable_values)
86
+
87
+ # Execute fields
88
+ result = {}
89
+
90
+ fields.each do |response_key, field_list|
91
+ field_value = execute_field(object_type, object_value, field_list, variable_values, context)
92
+ result[response_key] = field_value unless field_value == :skip
93
+ end
94
+
95
+ result
96
+ end
97
+
98
+ def collect_fields(selection_set, object_type, variable_values, visited_fragments = {})
99
+ fields = Hash.new { |h, k| h[k] = [] }
100
+
101
+ selection_set.selections.each do |selection|
102
+ # Skip if directives say so
103
+ next if skip_selection?(selection, variable_values)
104
+
105
+ case selection
106
+ when Parser::Field
107
+ response_key = selection.alias || selection.name
108
+ fields[response_key] << selection
109
+ when Parser::FragmentSpread
110
+ fragment_name = selection.name
111
+ next if visited_fragments[fragment_name]
112
+ visited_fragments[fragment_name] = true
113
+
114
+ # Note: Fragment definitions would need to be passed through context
115
+ # For now, we'll skip fragment spreads in this minimal implementation
116
+ when Parser::InlineFragment
117
+ # Check if type condition matches
118
+ if !selection.type_condition || type_applies?(selection.type_condition, object_type)
119
+ fragment_fields = collect_fields(selection.selection_set, object_type, variable_values, visited_fragments)
120
+ fragment_fields.each do |key, field_list|
121
+ fields[key].concat(field_list)
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ fields
128
+ end
129
+
130
+ def skip_selection?(selection, variable_values)
131
+ return false unless selection.respond_to?(:directives)
132
+
133
+ selection.directives.each do |directive|
134
+ case directive.name
135
+ when 'skip'
136
+ if_arg = directive.arguments.find { |arg| arg.name == 'if' }
137
+ return true if if_arg && evaluate_argument(if_arg, variable_values)
138
+ when 'include'
139
+ if_arg = directive.arguments.find { |arg| arg.name == 'if' }
140
+ return true if if_arg && !evaluate_argument(if_arg, variable_values)
141
+ end
142
+ end
143
+
144
+ false
145
+ end
146
+
147
+ def type_applies?(type_condition, object_type)
148
+ # For now, simple name matching
149
+ # TODO: Handle interfaces and unions properly
150
+ type_condition.name == object_type.name
151
+ end
152
+
153
+ def execute_field(object_type, object_value, field_list, variable_values, context)
154
+ field_ast = field_list.first
155
+ field_name = field_ast.name
156
+
157
+ # Handle introspection fields
158
+ case field_name
159
+ when '__typename'
160
+ return object_type.name
161
+ when '__schema'
162
+ return @schema if object_type == @schema.query_type
163
+ when '__type'
164
+ type_name_arg = field_ast.arguments.find { |arg| arg.name == 'name' }
165
+ type_name = evaluate_argument(type_name_arg, variable_values) if type_name_arg
166
+ return @schema.get_type(type_name) if object_type == @schema.query_type
167
+ end
168
+
169
+ # Get field definition
170
+ field_def = object_type.fields[field_name]
171
+ return nil unless field_def
172
+
173
+ # Coerce arguments
174
+ args = coerce_arguments(field_ast.arguments, field_def, variable_values)
175
+
176
+ # Resolve field value
177
+ resolved_value = resolve_field_value(field_def, object_value, args, context)
178
+
179
+ # Complete value
180
+ complete_value(field_def.type, resolved_value, field_ast.selection_set, variable_values, context)
181
+ end
182
+
183
+ def coerce_arguments(argument_asts, field_def, variable_values)
184
+ return {} if argument_asts.empty?
185
+
186
+ args = {}
187
+
188
+ argument_asts.each do |arg_ast|
189
+ arg_def = field_def.arguments[arg_ast.name]
190
+ next unless arg_def
191
+
192
+ value = evaluate_argument(arg_ast, variable_values)
193
+ args[arg_ast.name] = value
194
+ args[arg_ast.name.to_sym] = value # Allow both string and symbol access
195
+ end
196
+
197
+ args
198
+ end
199
+
200
+ def evaluate_argument(argument, variable_values)
201
+ evaluate_value(argument.value, variable_values)
202
+ end
203
+
204
+ def evaluate_value(value, variable_values)
205
+ case value
206
+ when Parser::Variable
207
+ variable_values[value.name]
208
+ when Parser::IntValue
209
+ value.value
210
+ when Parser::FloatValue
211
+ value.value
212
+ when Parser::StringValue
213
+ value.value
214
+ when Parser::BooleanValue
215
+ value.value
216
+ when Parser::NullValue
217
+ nil
218
+ when Parser::EnumValue
219
+ value.value
220
+ when Parser::ListValue
221
+ value.values.map { |v| evaluate_value(v, variable_values) }
222
+ when Parser::ObjectValue
223
+ obj = {}
224
+ value.fields.each do |field|
225
+ obj[field.name] = evaluate_value(field.value, variable_values)
226
+ obj[field.name.to_sym] = obj[field.name]
227
+ end
228
+ obj
229
+ else
230
+ value
231
+ end
232
+ end
233
+
234
+ def resolve_field_value(field_def, object_value, args, context)
235
+ if field_def.resolve
236
+ # Custom resolver
237
+ if field_def.resolve.arity == 0
238
+ field_def.resolve.call
239
+ elsif field_def.resolve.arity == 1
240
+ field_def.resolve.call(args)
241
+ else
242
+ field_def.resolve.call(object_value, args, context)
243
+ end
244
+ elsif object_value.is_a?(Hash)
245
+ # Hash object
246
+ object_value[field_def.name] || object_value[field_def.name.to_sym]
247
+ elsif object_value.respond_to?(field_def.name)
248
+ # Method call
249
+ object_value.public_send(field_def.name)
250
+ else
251
+ nil
252
+ end
253
+ end
254
+
255
+ def complete_value(field_type, result, selection_set, variable_values, context)
256
+ # Resolve lazy type references
257
+ field_type = field_type.resolve if field_type.is_a?(Schema::TypeReference)
258
+
259
+ # Handle non-null types
260
+ if field_type.is_a?(Types::NonNullType)
261
+ completed = complete_value(field_type.of_type, result, selection_set, variable_values, context)
262
+ raise ExecutionError, "Cannot return null for non-null field" if completed.nil?
263
+ return completed
264
+ end
265
+
266
+ # Handle null values
267
+ return nil if result.nil?
268
+
269
+ # Handle list types
270
+ if field_type.is_a?(Types::ListType)
271
+ raise ExecutionError, "Expected list but got #{result.class}" unless result.is_a?(Array)
272
+ return result.map { |item| complete_value(field_type.of_type, item, selection_set, variable_values, context) }
273
+ end
274
+
275
+ # Handle scalar types
276
+ if field_type.is_a?(Types::ScalarType)
277
+ return field_type.serialize.call(result)
278
+ end
279
+
280
+ # Handle enum types
281
+ if field_type.is_a?(Types::EnumType)
282
+ return result.to_s
283
+ end
284
+
285
+ # Handle object types
286
+ if field_type.is_a?(Types::ObjectType)
287
+ return execute_selection_set(selection_set, field_type, result, variable_values, context)
288
+ end
289
+
290
+ # Handle interface and union types
291
+ if field_type.is_a?(Types::InterfaceType) || field_type.is_a?(Types::UnionType)
292
+ runtime_type = resolve_runtime_type(field_type, result)
293
+ return execute_selection_set(selection_set, runtime_type, result, variable_values, context)
294
+ end
295
+
296
+ result
297
+ end
298
+
299
+ def resolve_runtime_type(abstract_type, object_value)
300
+ if abstract_type.resolve_type
301
+ type_name = abstract_type.resolve_type.call(object_value)
302
+ @schema.get_type(type_name)
303
+ else
304
+ # Try to infer from object class name
305
+ class_name = object_value.class.name.split('::').last
306
+ @schema.get_type(class_name)
307
+ end
308
+ end
309
+
310
+ def resolve_type_ref(type_ref)
311
+ case type_ref
312
+ when Parser::NamedType
313
+ @schema.get_type(type_ref.name)
314
+ when Parser::ListType
315
+ Types::ListType.new(resolve_type_ref(type_ref.type))
316
+ when Parser::NonNullType
317
+ Types::NonNullType.new(resolve_type_ref(type_ref.type))
318
+ when Schema::TypeReference
319
+ type_ref.resolve
320
+ else
321
+ type_ref
322
+ end
323
+ end
324
+
325
+ def coerce_input_value(value, type)
326
+ if type.is_a?(Types::NonNullType)
327
+ raise ExecutionError, "Expected non-null value" if value.nil?
328
+ return coerce_input_value(value, type.of_type)
329
+ end
330
+
331
+ return nil if value.nil?
332
+
333
+ if type.is_a?(Types::ListType)
334
+ return [coerce_input_value(value, type.of_type)] unless value.is_a?(Array)
335
+ return value.map { |v| coerce_input_value(v, type.of_type) }
336
+ end
337
+
338
+ if type.is_a?(Types::ScalarType)
339
+ return type.parse_value.call(value)
340
+ end
341
+
342
+ if type.is_a?(Types::EnumType)
343
+ return value.to_s
344
+ end
345
+
346
+ if type.is_a?(Types::InputObjectType)
347
+ return {} unless value.is_a?(Hash)
348
+ result = {}
349
+ type.fields.each do |field_name, field_def|
350
+ field_value = value[field_name] || value[field_name.to_sym]
351
+ result[field_name] = coerce_input_value(field_value, field_def.type)
352
+ end
353
+ return result
354
+ end
355
+
356
+ value
357
+ end
358
+
359
+ def coerce_literal_value(value, type)
360
+ # Similar to coerce_input_value but for AST values
361
+ if type.is_a?(Types::NonNullType)
362
+ raise ExecutionError, "Expected non-null value" if value.is_a?(Parser::NullValue)
363
+ return coerce_literal_value(value, type.of_type)
364
+ end
365
+
366
+ return nil if value.is_a?(Parser::NullValue)
367
+
368
+ if type.is_a?(Types::ListType)
369
+ return [coerce_literal_value(value, type.of_type)] unless value.is_a?(Parser::ListValue)
370
+ return value.values.map { |v| coerce_literal_value(v, type.of_type) }
371
+ end
372
+
373
+ if type.is_a?(Types::ScalarType)
374
+ return type.parse_literal.call(value)
375
+ end
376
+
377
+ evaluate_value(value, {})
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,222 @@
1
+ module GraphQLite
2
+ # Introspection adds schema introspection capabilities
3
+ module Introspection
4
+ class << self
5
+ def add_introspection_types(schema)
6
+ # Add __DirectiveLocation enum first
7
+ directive_location_enum = schema.enum('__DirectiveLocation', values: {
8
+ 'QUERY' => { value: 'QUERY' },
9
+ 'MUTATION' => { value: 'MUTATION' },
10
+ 'SUBSCRIPTION' => { value: 'SUBSCRIPTION' },
11
+ 'FIELD' => { value: 'FIELD' },
12
+ 'FRAGMENT_DEFINITION' => { value: 'FRAGMENT_DEFINITION' },
13
+ 'FRAGMENT_SPREAD' => { value: 'FRAGMENT_SPREAD' },
14
+ 'INLINE_FRAGMENT' => { value: 'INLINE_FRAGMENT' },
15
+ 'SCHEMA' => { value: 'SCHEMA' },
16
+ 'SCALAR' => { value: 'SCALAR' },
17
+ 'OBJECT' => { value: 'OBJECT' },
18
+ 'FIELD_DEFINITION' => { value: 'FIELD_DEFINITION' },
19
+ 'ARGUMENT_DEFINITION' => { value: 'ARGUMENT_DEFINITION' },
20
+ 'INTERFACE' => { value: 'INTERFACE' },
21
+ 'UNION' => { value: 'UNION' },
22
+ 'ENUM' => { value: 'ENUM' },
23
+ 'ENUM_VALUE' => { value: 'ENUM_VALUE' },
24
+ 'INPUT_OBJECT' => { value: 'INPUT_OBJECT' },
25
+ 'INPUT_FIELD_DEFINITION' => { value: 'INPUT_FIELD_DEFINITION' }
26
+ })
27
+
28
+ # Add __TypeKind enum
29
+ type_kind_enum = schema.enum('__TypeKind', values: {
30
+ 'SCALAR' => { value: 'SCALAR' },
31
+ 'OBJECT' => { value: 'OBJECT' },
32
+ 'INTERFACE' => { value: 'INTERFACE' },
33
+ 'UNION' => { value: 'UNION' },
34
+ 'ENUM' => { value: 'ENUM' },
35
+ 'INPUT_OBJECT' => { value: 'INPUT_OBJECT' },
36
+ 'LIST' => { value: 'LIST' },
37
+ 'NON_NULL' => { value: 'NON_NULL' }
38
+ })
39
+
40
+ # Add __InputValue type (needed by __Field and __Directive)
41
+ input_value_type = schema.object('__InputValue', description: 'An input value (argument or input field)') do
42
+ field :name, 'String', null: false do
43
+ resolve { |value| value.name }
44
+ end
45
+
46
+ field :description, 'String' do
47
+ resolve { |value| value.description }
48
+ end
49
+
50
+ field :type, '__Type', null: false do
51
+ resolve { |value| value.type }
52
+ end
53
+
54
+ field :defaultValue, 'String' do
55
+ resolve { |value| value.default_value&.to_s }
56
+ end
57
+ end
58
+
59
+ # Add __EnumValue type
60
+ enum_value_type = schema.object('__EnumValue', description: 'An enum value') do
61
+ field :name, 'String', null: false do
62
+ resolve { |value| value.value.to_s }
63
+ end
64
+
65
+ field :description, 'String' do
66
+ resolve { |value| value.description }
67
+ end
68
+
69
+ field :isDeprecated, 'Boolean', null: false do
70
+ resolve { |value| !value.deprecation_reason.nil? }
71
+ end
72
+
73
+ field :deprecationReason, 'String' do
74
+ resolve { |value| value.deprecation_reason }
75
+ end
76
+ end
77
+
78
+ # Add __Field type
79
+ field_type = schema.object('__Field', description: 'A field in an object type') do
80
+ field :name, 'String', null: false do
81
+ resolve { |field| field.name }
82
+ end
83
+
84
+ field :description, 'String' do
85
+ resolve { |field| field.description }
86
+ end
87
+
88
+ field :args, ['__InputValue'], null: false do
89
+ resolve { |field| field.arguments.values }
90
+ end
91
+
92
+ field :type, '__Type', null: false do
93
+ resolve { |field| field.type }
94
+ end
95
+
96
+ field :isDeprecated, 'Boolean', null: false do
97
+ resolve { |field| field.deprecated? }
98
+ end
99
+
100
+ field :deprecationReason, 'String' do
101
+ resolve { |field| field.deprecation_reason }
102
+ end
103
+ end
104
+
105
+ # Add __Directive type (before __Schema since __Schema references it)
106
+ directive_type = schema.object('__Directive', description: 'A directive') do
107
+ field :name, 'String', null: false do
108
+ resolve { |directive| directive.name }
109
+ end
110
+
111
+ field :description, 'String' do
112
+ resolve { |directive| directive.description }
113
+ end
114
+
115
+ field :locations, ['__DirectiveLocation'], null: false do
116
+ resolve { |directive| directive.locations }
117
+ end
118
+
119
+ field :args, ['__InputValue'], null: false do
120
+ resolve { |directive| directive.arguments.values }
121
+ end
122
+ end
123
+
124
+ # Add __Schema type
125
+ schema_type = schema.object('__Schema', description: 'A GraphQL schema') do
126
+ field :types, [schema.object('__Type')] do
127
+ resolve { |args, ctx| schema.types.values }
128
+ end
129
+
130
+ field :queryType, '__Type' do
131
+ resolve { schema.query_type }
132
+ end
133
+
134
+ field :mutationType, '__Type' do
135
+ resolve { schema.mutation_type }
136
+ end
137
+
138
+ field :subscriptionType, '__Type' do
139
+ resolve { schema.subscription_type }
140
+ end
141
+
142
+ field :directives, ['__Directive'] do
143
+ resolve { schema.directives.values }
144
+ end
145
+ end
146
+
147
+ # Add __Type type
148
+ type_type = schema.object('__Type', description: 'A type in the GraphQL schema') do
149
+ field :kind, '__TypeKind', null: false do
150
+ resolve { |type| type.kind }
151
+ end
152
+
153
+ field :name, 'String' do
154
+ resolve { |type| type.respond_to?(:name) ? type.name : nil }
155
+ end
156
+
157
+ field :description, 'String' do
158
+ resolve { |type| type.respond_to?(:description) ? type.description : nil }
159
+ end
160
+
161
+ field :fields, ['__Field'] do
162
+ argument :includeDeprecated, 'Boolean'
163
+ resolve do |type, args|
164
+ next nil unless type.respond_to?(:fields)
165
+ fields = type.fields.values
166
+ fields = fields.reject(&:deprecated?) unless args[:includeDeprecated]
167
+ fields
168
+ end
169
+ end
170
+
171
+ field :interfaces, ['__Type'] do
172
+ resolve do |type|
173
+ type.respond_to?(:interfaces) ? type.interfaces : nil
174
+ end
175
+ end
176
+
177
+ field :possibleTypes, ['__Type'] do
178
+ resolve do |type|
179
+ type.respond_to?(:types) ? type.types : nil
180
+ end
181
+ end
182
+
183
+ field :enumValues, ['__EnumValue'] do
184
+ argument :includeDeprecated, 'Boolean'
185
+ resolve do |type, args|
186
+ next nil unless type.is_a?(Types::EnumType)
187
+ values = type.values.values
188
+ values = values.reject { |v| v.deprecation_reason } unless args[:includeDeprecated]
189
+ values
190
+ end
191
+ end
192
+
193
+ field :inputFields, ['__InputValue'] do
194
+ resolve do |type|
195
+ type.is_a?(Types::InputObjectType) ? type.fields.values : nil
196
+ end
197
+ end
198
+
199
+ field :ofType, '__Type' do
200
+ resolve do |type|
201
+ type.respond_to?(:of_type) ? type.of_type : nil
202
+ end
203
+ end
204
+ end
205
+
206
+ # Add introspection fields to query type
207
+ if schema.query_type
208
+ schema.query_type.field('__schema', schema_type, description: 'Access the schema introspection system') do
209
+ resolve { schema }
210
+ end
211
+
212
+ schema.query_type.field('__type', type_type, description: 'Query a type by name') do |field|
213
+ field.argument('name', Types::STRING.!)
214
+ field.resolve do |_, args|
215
+ schema.get_type(args[:name] || args['name'])
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end