ar_serializer 1.0.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,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