ar_serializer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,41 @@
1
+ require_relative 'graphql/types'
2
+ require_relative 'graphql/parser'
3
+
4
+ module ArSerializer::GraphQL
5
+ def self.definition(klass, use: nil)
6
+ ArSerializer::Serializer.with_namespaces(use) { _definition klass }
7
+ end
8
+
9
+ def self._definition(klass)
10
+ schema = SchemaClass.new(klass)
11
+ definitions = schema.types.map do |type|
12
+ next "scalar #{type.name}" if type.is_a? ScalarTypeClass
13
+ fields = type.fields.map do |field|
14
+ args = field.args.map { |arg| "#{arg.name}: #{arg.type.gql_type}" }
15
+ args_exp = "(#{args.join(', ')})" unless args.empty?
16
+ " #{field.name}#{args_exp}: #{field.type.gql_type}"
17
+ end
18
+ <<~TYPE
19
+ type #{type.name} {
20
+ #{fields.join("\n")}
21
+ }
22
+ TYPE
23
+ end
24
+ <<~SCHEMA
25
+ schema {
26
+ query: #{schema.query_type.name}
27
+ }
28
+
29
+ #{definitions.map(&:strip).join("\n\n")}
30
+ SCHEMA
31
+ end
32
+
33
+ def self.serialize(schema, gql_query, operation_name: nil, variables: {}, **args)
34
+ query = ArSerializer::GraphQL::Parser.parse(
35
+ gql_query,
36
+ operation_name: operation_name,
37
+ variables: variables
38
+ )
39
+ { data: ArSerializer::Serializer.serialize(schema, query, **args) }
40
+ end
41
+ end
@@ -0,0 +1,269 @@
1
+ class ArSerializer::GraphQL::Parser
2
+ class ParseError < StandardError; end
3
+
4
+ attr_reader :query, :operation_name, :variables, :chars
5
+ def initialize(query, operation_name: nil, variables: {})
6
+ @query = query
7
+ @operation_name = operation_name
8
+ @variables = variables
9
+ @chars = query.chars
10
+ end
11
+
12
+ def self.parse(*args)
13
+ new(*args).parse
14
+ end
15
+
16
+ def parse
17
+ definitions = []
18
+ consume_blank
19
+ loop do
20
+ definition = parse_definition
21
+ consume_blank
22
+ consume_text ','
23
+ consume_blank
24
+ break unless definition
25
+ definitions << definition
26
+ end
27
+ raise_expected_not_found 'definition or EOF' unless chars.empty?
28
+ query = definitions.find do |definition|
29
+ next unless definition[:type] == 'query'
30
+ operation_name.nil? || operation_name == definition[:args].first
31
+ end
32
+ raise ParseError, 'empty query' unless query
33
+ fragments = definitions.select { |definition| definition[:type] == 'fragment' }
34
+ fragments_by_name = fragments.index_by { |frag| frag[:args].first }
35
+ embed_fragment query[:fields], fragments_by_name
36
+ end
37
+
38
+ private
39
+
40
+ def consume_comment
41
+ return false if chars.first != '#'
42
+ until chars.blank?
43
+ c = chars.first
44
+ break if c == "\n"
45
+ chars.shift
46
+ end
47
+ true
48
+ end
49
+
50
+ def consume_blank
51
+ loop do
52
+ chars.shift while chars.first&.match?(/\s/)
53
+ return unless consume_comment
54
+ end
55
+ end
56
+
57
+ def consume_text(s)
58
+ return false unless chars.take(s.size).join == s
59
+ chars.shift s.size
60
+ true
61
+ end
62
+
63
+ def consume_text!(s)
64
+ return if consume_text s
65
+ raise_expected_not_found s.inspect
66
+ end
67
+
68
+ def raise_expected_not_found(expected, found = nil)
69
+ raise(
70
+ ParseError,
71
+ "expected #{expected} but found #{found || chars.first.inspect} #{current_position_message}"
72
+ )
73
+ end
74
+
75
+ def parse_name
76
+ name = ''
77
+ name << chars.shift while chars.first && chars.first =~ /[a-zA-Z0-9_]/
78
+ name unless name.empty?
79
+ end
80
+
81
+ def parse_name_alias
82
+ name = parse_name
83
+ return unless name
84
+ consume_blank
85
+ if consume_text ':'
86
+ consume_blank
87
+ [parse_name, name]
88
+ else
89
+ name
90
+ end
91
+ end
92
+
93
+ def parse_arg_value
94
+ case chars.first
95
+ when '"'
96
+ chars.shift
97
+ s = ''
98
+ loop do
99
+ if chars.first == '\\'
100
+ s << chars.shift
101
+ s << chars.shift
102
+ elsif chars.first == '"'
103
+ break
104
+ else
105
+ s << chars.shift
106
+ end
107
+ end
108
+ chars.shift
109
+ unescape_string s
110
+ when '['
111
+ chars.shift
112
+ result = []
113
+ loop do
114
+ consume_blank
115
+ value = parse_arg_value
116
+ consume_blank
117
+ consume_text ','
118
+ break if value == :none
119
+ result << value
120
+ end
121
+ consume_text! ']'
122
+ result
123
+ when '{'
124
+ chars.shift
125
+ consume_blank
126
+ result = parse_arg_fields
127
+ consume_blank
128
+ consume_text! '}'
129
+ result
130
+ when '$'
131
+ chars.shift
132
+ name = parse_name
133
+ variables[name]
134
+ when /[0-9+\-]/
135
+ s = ''
136
+ s << chars.shift while chars.first.match?(/[0-9.e+\-]/)
137
+ s.match?(/\.|e/) ? s.to_f : s.to_i
138
+ when /[a-zA-Z]/
139
+ s = parse_name
140
+ converts = { 'true' => true, 'false' => false, 'null' => nil }
141
+ converts.key?(s) ? converts[s] : s
142
+ else
143
+ :none
144
+ end
145
+ end
146
+
147
+ def unescape_string(s)
148
+ JSON.parse %("#{s}")
149
+ rescue JSON::ParserError # for old json gem
150
+ JSON.parse(%(["#{s}"])).first
151
+ end
152
+
153
+ def parse_arg_fields
154
+ result = {}
155
+ loop do
156
+ name = parse_name
157
+ break unless name
158
+ consume_blank
159
+ consume_text! ':'
160
+ consume_blank
161
+ value = parse_arg_value
162
+ if value == :none
163
+ raise(
164
+ ParseError,
165
+ "expected hash value but nothing found #{current_position_message}"
166
+ )
167
+ end
168
+ result[name] = value
169
+ consume_blank
170
+ consume_text ','
171
+ consume_blank
172
+ end
173
+ result
174
+ end
175
+
176
+ def parse_args
177
+ return unless consume_text '('
178
+ consume_blank
179
+ args = parse_arg_fields
180
+ consume_blank
181
+ consume_text! ')'
182
+ args
183
+ end
184
+
185
+ def parse_field
186
+ if chars[0, 3].join == '...'
187
+ 3.times { chars.shift }
188
+ name = parse_name
189
+ return ['...' + name, { fragment: name }]
190
+ end
191
+ name, alias_name = parse_name_alias
192
+ return unless name
193
+ consume_blank
194
+ args = parse_args
195
+ consume_blank
196
+ fields = parse_fields
197
+ [name, { as: alias_name, params: args, attributes: fields }.compact]
198
+ end
199
+
200
+ def parse_fields
201
+ return unless consume_text '{'
202
+ consume_blank
203
+ fields = {}
204
+ loop do
205
+ name, field = parse_field
206
+ consume_blank
207
+ consume_text ','
208
+ consume_blank
209
+ break unless name
210
+ fields[name] = field
211
+ end
212
+ consume_text! '}'
213
+ fields
214
+ end
215
+
216
+ def parse_definition
217
+ type = parse_name
218
+ consume_blank
219
+ args_text = ''
220
+ if type
221
+ args_text << chars.shift while chars.first && chars.first != '{'
222
+ end
223
+ args = args_text.split(/[\s()]+/)
224
+ fields = parse_fields
225
+ return if type.nil? && fields.nil?
226
+ type ||= 'query'
227
+ raise_expected_not_found '{'.inspect if fields.nil?
228
+ { type: type, args: args, fields: fields }
229
+ end
230
+
231
+ def current_position_message
232
+ pos = query.size - chars.size
233
+ code = query[[pos - 10, 0].max..pos + 10]
234
+ line_num = 0
235
+ query.each_line.with_index 1 do |l, i|
236
+ line_num = i
237
+ break if pos < l.size
238
+ pos -= l.size
239
+ end
240
+ "at #{line_num}:#{pos} near #{code.inspect}"
241
+ end
242
+
243
+ def embed_fragment(fields, fragments)
244
+ output = {}
245
+ fields.each do |key, value|
246
+ if value.is_a?(Hash) && (fragment_name = value[:fragment])
247
+ fragment = fragments[fragment_name]
248
+ extract_fragment fragment_name, fragments
249
+ output.update fragment[:fields]
250
+ else
251
+ output[key] = value
252
+ if (attrs = value[:attributes])
253
+ value[:attributes] = embed_fragment attrs, fragments
254
+ end
255
+ end
256
+ end
257
+ output
258
+ end
259
+
260
+ def extract_fragment(fragment_name, fragments)
261
+ fragment = fragments[fragment_name]
262
+ raise ParseError, "fragment named #{fragment_name.inspect} was not found" if fragment.nil?
263
+ raise ParseError, "fragment circular definition detected in #{fragment_name.inspect}" if fragment[:state] == :start
264
+ return if fragment[:state] == :done
265
+ fragment[:state] = :start
266
+ fragment[:fields] = embed_fragment fragment[:fields], fragments
267
+ fragment[:state] = :done
268
+ end
269
+ end
@@ -0,0 +1,442 @@
1
+ module ArSerializer::GraphQL
2
+ class ArgClass
3
+ include ::ArSerializer::Serializable
4
+ attr_reader :name, :type
5
+ def initialize(name, type)
6
+ @optional = name.to_s.end_with? '?' # TODO: refactor
7
+ @name = name.to_s.delete '?'
8
+ @type = TypeClass.from type
9
+ end
10
+ serializer_field :name
11
+ serializer_field :type, except: :fields
12
+ serializer_field(:defaultValue) { nil }
13
+ serializer_field(:description) { "#{'Optional: ' if @optional}#{type.description}" }
14
+ end
15
+
16
+ class FieldClass
17
+ include ::ArSerializer::Serializable
18
+ attr_reader :name, :field
19
+ def initialize(name, field)
20
+ @name = name
21
+ @field = field
22
+ end
23
+
24
+ def args
25
+ return [] if field.arguments == :any
26
+ field.arguments.map do |key, type|
27
+ ArgClass.new key, type
28
+ end
29
+ end
30
+
31
+ def type
32
+ TypeClass.from field.type, field.only, field.except
33
+ end
34
+
35
+ def collect_types(types)
36
+ types[:any] = true if field.arguments == :any
37
+ args.each { |arg| arg.type.collect_types types }
38
+ type.collect_types types
39
+ end
40
+
41
+ def args_ts_type
42
+ arg_types = field.arguments.map do |key, type|
43
+ "#{key}: #{TypeClass.from(type).ts_type}"
44
+ end
45
+ "{ #{arg_types.join '; '} }"
46
+ end
47
+
48
+ serializer_field :name, :args
49
+ serializer_field :type, except: :fields
50
+ serializer_field(:isDeprecated) { false }
51
+ serializer_field(:description) { type.description }
52
+ serializer_field(:deprecationReason) { nil }
53
+ end
54
+
55
+ class SchemaClass
56
+ include ::ArSerializer::Serializable
57
+ attr_reader :klass, :query_type
58
+ def initialize(klass)
59
+ @klass = klass
60
+ @query_type = SerializableTypeClass.new klass
61
+ end
62
+
63
+ def collect_types
64
+ types = {}
65
+ klass._serializer_field_keys.each do |name|
66
+ fc = FieldClass.new name, klass._serializer_field_info(name)
67
+ fc.collect_types types
68
+ end
69
+ type_symbols, type_classes = types.keys.partition { |t| t.is_a? Symbol }
70
+ type_classes << TypeClass.from(klass)
71
+ [type_symbols.sort, type_classes.sort_by(&:name)]
72
+ end
73
+
74
+ def types
75
+ types_symbols, klass_types = collect_types
76
+ types_symbols.map { |t| ScalarTypeClass.new t } + klass_types
77
+ end
78
+
79
+ serializer_field(:mutationType) { nil }
80
+ serializer_field(:subscriptionType) { nil }
81
+ serializer_field(:directives) { [] }
82
+ serializer_field :types, :queryType
83
+ end
84
+
85
+ class TypeClass
86
+ include ::ArSerializer::Serializable
87
+ attr_reader :type, :only, :except
88
+ def initialize(type, only = nil, except = nil)
89
+ @type = type
90
+ @only = only
91
+ @except = except
92
+ validate!
93
+ end
94
+
95
+ class InvalidType < StandardError; end
96
+
97
+ def validate!
98
+ valid_symbols = %i[number int float string boolean any]
99
+ invalids = []
100
+ recursive_validate = lambda do |t|
101
+ case t
102
+ when Array
103
+ t.each { |v| recursive_validate.call v }
104
+ when Hash
105
+ t.each_value { |v| recursive_validate.call v }
106
+ when String, Numeric, true, false, nil
107
+ return
108
+ when Class
109
+ invalids << t unless t.ancestors.include? ArSerializer::Serializable
110
+ when Symbol
111
+ invalids << t unless valid_symbols.include? t.to_s.gsub(/\?$/, '').to_sym
112
+ else
113
+ invalids << t
114
+ end
115
+ end
116
+ recursive_validate.call type
117
+ return if invalids.empty?
118
+ message = "Valid types are String, Numeric, Hash, Array, ArSerializer::Serializable, true, false, nil and Symbol#{valid_symbols}"
119
+ raise InvalidType, "Invalid type: #{invalids.map(&:inspect).join(', ')}. #{message}"
120
+ end
121
+
122
+ def collect_types(types); end
123
+
124
+ def description
125
+ ts_type
126
+ end
127
+
128
+ def name; end
129
+
130
+ def of_type; end
131
+
132
+ def fields; end
133
+
134
+ def sample; end
135
+
136
+ def ts_type; end
137
+
138
+ def association_type; end
139
+
140
+ serializer_field :kind, :name, :description, :fields
141
+ serializer_field :ofType, except: :fields
142
+ serializer_field(:interfaces) { [] }
143
+ %i[inputFields enumValues possibleTypes].each do |name|
144
+ serializer_field(name) { nil }
145
+ end
146
+
147
+ def self.from(type, only = nil, except = nil)
148
+ type = [type[0...-1].to_sym, nil] if type.is_a?(Symbol) && type.to_s.ends_with?('?')
149
+ type = [type[0...-1], nil] if type.is_a?(String) && type.ends_with?('?')
150
+ case type
151
+ when Class
152
+ SerializableTypeClass.new type, only, except
153
+ when Symbol, String, Numeric, true, false, nil
154
+ ScalarTypeClass.new type
155
+ when Array
156
+ if type.size == 1
157
+ ListTypeClass.new type.first, only, except
158
+ elsif type.size == 2 && type.last.nil?
159
+ OptionalTypeClass.new type
160
+ else
161
+ OrTypeClass.new type, only, except
162
+ end
163
+ when Hash
164
+ HashTypeClass.new type, only, except
165
+ end
166
+ end
167
+ end
168
+
169
+ class ScalarTypeClass < TypeClass
170
+ def initialize(type)
171
+ @type = type
172
+ end
173
+
174
+ def kind
175
+ 'SCALAR'
176
+ end
177
+
178
+ def name
179
+ case type
180
+ when String, :string
181
+ :string
182
+ when Integer, :int
183
+ :int
184
+ when Float, :float
185
+ :float
186
+ when true, false, :boolean
187
+ :boolean
188
+ when :other
189
+ :other
190
+ else
191
+ :any
192
+ end
193
+ end
194
+
195
+ def collect_types(types)
196
+ types[name] = true
197
+ end
198
+
199
+ def gql_type
200
+ type
201
+ end
202
+
203
+ def sample
204
+ case ts_type
205
+ when 'number'
206
+ 0
207
+ when 'string'
208
+ ''
209
+ when 'boolean'
210
+ true
211
+ when 'any'
212
+ nil
213
+ else
214
+ type
215
+ end
216
+ end
217
+
218
+ def ts_type
219
+ case type
220
+ when :int, :float
221
+ 'number'
222
+ when :string, :number, :boolean
223
+ type.to_s
224
+ when Symbol
225
+ 'any'
226
+ else
227
+ type.to_json
228
+ end
229
+ end
230
+ end
231
+
232
+ class HashTypeClass < TypeClass
233
+ def kind
234
+ 'SCALAR'
235
+ end
236
+
237
+ def name
238
+ :other
239
+ end
240
+
241
+ def collect_types(types)
242
+ types[:other] = true
243
+ type.values.map do |v|
244
+ TypeClass.from(v, only, except).collect_types(types)
245
+ end
246
+ end
247
+
248
+ def association_type
249
+ type.values.each do |v|
250
+ t = TypeClass.from(v, only, except).association_type
251
+ return t if t
252
+ end
253
+ nil
254
+ end
255
+
256
+ def gql_type
257
+ 'OBJECT'
258
+ end
259
+
260
+ def sample
261
+ type.reject { |k| k.to_s.ends_with? '?' }.transform_values do |v|
262
+ TypeClass.from(v).sample
263
+ end
264
+ end
265
+
266
+ def ts_type
267
+ fields = type.map do |key, value|
268
+ k = key.to_s == '*' ? '[key: string]' : key
269
+ "#{k}: #{TypeClass.from(value, only, except).ts_type}"
270
+ end
271
+ "{ #{fields.join('; ')} }"
272
+ end
273
+ end
274
+
275
+ class SerializableTypeClass < TypeClass
276
+ def field_only
277
+ [*only].map(&:to_s)
278
+ end
279
+
280
+ def field_except
281
+ [*except].map(&:to_s)
282
+ end
283
+
284
+ def kind
285
+ 'OBJECT'
286
+ end
287
+
288
+ def name
289
+ name_segments = [type.name.delete(':')]
290
+ unless field_only.empty?
291
+ name_segments << 'Only'
292
+ name_segments << field_only.map(&:camelize)
293
+ end
294
+ unless field_except.empty?
295
+ name_segments << 'Except'
296
+ name_segments << field_except.map(&:camelize)
297
+ end
298
+ name_segments.join
299
+ end
300
+
301
+ def fields
302
+ keys = type._serializer_field_keys - ['__schema'] - field_except
303
+ keys = field_only & keys unless field_only.empty?
304
+ keys.map do |name|
305
+ FieldClass.new name, type._serializer_field_info(name)
306
+ end
307
+ end
308
+
309
+ def collect_types(types)
310
+ return if types[self]
311
+ types[self] = true
312
+ fields.each { |field| field.collect_types types }
313
+ end
314
+
315
+ def association_type
316
+ self
317
+ end
318
+
319
+ def gql_type
320
+ name
321
+ end
322
+
323
+ def ts_type
324
+ "Type#{name}"
325
+ end
326
+
327
+ def eql?(t)
328
+ self.class == t.class && self.compare_elements == t.compare_elements
329
+ end
330
+
331
+ def == t
332
+ eql? t
333
+ end
334
+
335
+ def compare_elements
336
+ [type, field_only, field_except]
337
+ end
338
+
339
+ def hash
340
+ compare_elements.hash
341
+ end
342
+ end
343
+
344
+ class OptionalTypeClass < TypeClass
345
+ def kind
346
+ of_type.kind
347
+ end
348
+
349
+ def name
350
+ of_type.name
351
+ end
352
+
353
+ def of_type
354
+ TypeClass.from type.first, only, except
355
+ end
356
+
357
+ def association_type
358
+ of_type.association_type
359
+ end
360
+
361
+ def collect_types(types)
362
+ of_type.collect_types types
363
+ end
364
+
365
+ def gql_type
366
+ of_type.gql_type
367
+ end
368
+
369
+ def sample
370
+ nil
371
+ end
372
+
373
+ def ts_type
374
+ "(#{of_type.ts_type} | null)"
375
+ end
376
+ end
377
+
378
+ class OrTypeClass < TypeClass
379
+ def kind
380
+ 'OBJECT'
381
+ end
382
+
383
+ def name
384
+ :other
385
+ end
386
+
387
+ def of_types
388
+ type.map { |t| TypeClass.from t, only, except }
389
+ end
390
+
391
+ def collect_types(types)
392
+ types[:other] = true
393
+ of_types.map { |t| t.collect_types types }
394
+ end
395
+
396
+ def gql_type
397
+ kind
398
+ end
399
+
400
+ def sample
401
+ of_types.first.sample
402
+ end
403
+
404
+ def ts_type
405
+ '(' + of_types.map(&:ts_type).join(' | ') + ')'
406
+ end
407
+ end
408
+
409
+ class ListTypeClass < TypeClass
410
+ def kind
411
+ 'LIST'
412
+ end
413
+
414
+ def name
415
+ 'LIST'
416
+ end
417
+
418
+ def of_type
419
+ TypeClass.from type, only, except
420
+ end
421
+
422
+ def collect_types(types)
423
+ of_type.collect_types types
424
+ end
425
+
426
+ def association_type
427
+ of_type.association_type
428
+ end
429
+
430
+ def gql_type
431
+ "[#{of_type.gql_type}]"
432
+ end
433
+
434
+ def sample
435
+ []
436
+ end
437
+
438
+ def ts_type
439
+ "(#{of_type.ts_type} [])"
440
+ end
441
+ end
442
+ end