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,266 @@
1
+ module GraphQLite
2
+ # Lexer tokenizes GraphQL documents according to the spec
3
+ class Lexer
4
+ TOKEN_TYPES = %i[
5
+ name int float string boolean null
6
+ lparen rparen lbracket rbracket lbrace rbrace
7
+ colon pipe equals bang dollar at spread
8
+ eof
9
+ ].freeze
10
+
11
+ Token = Struct.new(:type, :value, :line, :column)
12
+
13
+ attr_reader :source, :pos, :line, :column
14
+
15
+ def initialize(source)
16
+ @source = source
17
+ @pos = 0
18
+ @line = 1
19
+ @column = 1
20
+ end
21
+
22
+ def tokenize
23
+ tokens = []
24
+ loop do
25
+ token = next_token
26
+ tokens << token
27
+ break if token.type == :eof
28
+ end
29
+ tokens
30
+ end
31
+
32
+ def next_token
33
+ skip_ignored
34
+ return Token.new(:eof, nil, @line, @column) if @pos >= @source.length
35
+
36
+ char = current_char
37
+ start_line = @line
38
+ start_column = @column
39
+
40
+ token = case char
41
+ when '{'
42
+ advance
43
+ Token.new(:lbrace, '{', start_line, start_column)
44
+ when '}'
45
+ advance
46
+ Token.new(:rbrace, '}', start_line, start_column)
47
+ when '('
48
+ advance
49
+ Token.new(:lparen, '(', start_line, start_column)
50
+ when ')'
51
+ advance
52
+ Token.new(:rparen, ')', start_line, start_column)
53
+ when '['
54
+ advance
55
+ Token.new(:lbracket, '[', start_line, start_column)
56
+ when ']'
57
+ advance
58
+ Token.new(:rbracket, ']', start_line, start_column)
59
+ when ':'
60
+ advance
61
+ Token.new(:colon, ':', start_line, start_column)
62
+ when '|'
63
+ advance
64
+ Token.new(:pipe, '|', start_line, start_column)
65
+ when '='
66
+ advance
67
+ Token.new(:equals, '=', start_line, start_column)
68
+ when '!'
69
+ advance
70
+ Token.new(:bang, '!', start_line, start_column)
71
+ when '$'
72
+ advance
73
+ Token.new(:dollar, '$', start_line, start_column)
74
+ when '@'
75
+ advance
76
+ Token.new(:at, '@', start_line, start_column)
77
+ when '.'
78
+ if peek(1) == '.' && peek(2) == '.'
79
+ advance(3)
80
+ Token.new(:spread, '...', start_line, start_column)
81
+ else
82
+ raise ParseError, "Unexpected character '.' at #{start_line}:#{start_column}"
83
+ end
84
+ when '"'
85
+ read_string(start_line, start_column)
86
+ when '-', '0'..'9'
87
+ read_number(start_line, start_column)
88
+ when 'a'..'z', 'A'..'Z', '_'
89
+ read_name(start_line, start_column)
90
+ else
91
+ raise ParseError, "Unexpected character '#{char}' at #{start_line}:#{start_column}"
92
+ end
93
+
94
+ token
95
+ end
96
+
97
+ private
98
+
99
+ def current_char
100
+ @source[@pos]
101
+ end
102
+
103
+ def peek(offset = 1)
104
+ @source[@pos + offset]
105
+ end
106
+
107
+ def advance(count = 1)
108
+ count.times do
109
+ if current_char == "\n"
110
+ @line += 1
111
+ @column = 1
112
+ else
113
+ @column += 1
114
+ end
115
+ @pos += 1
116
+ end
117
+ end
118
+
119
+ def skip_ignored
120
+ loop do
121
+ break if @pos >= @source.length
122
+ char = current_char
123
+
124
+ case char
125
+ when ' ', "\t", "\r", "\n", ','
126
+ advance
127
+ when '#'
128
+ skip_comment
129
+ else
130
+ break
131
+ end
132
+ end
133
+ end
134
+
135
+ def skip_comment
136
+ advance while @pos < @source.length && current_char != "\n"
137
+ end
138
+
139
+ def read_string(start_line, start_column)
140
+ advance # skip opening quote
141
+ value = ""
142
+
143
+ loop do
144
+ raise ParseError, "Unterminated string at #{start_line}:#{start_column}" if @pos >= @source.length
145
+
146
+ char = current_char
147
+
148
+ if char == '"'
149
+ advance
150
+ break
151
+ elsif char == '\\'
152
+ advance
153
+ escape_char = current_char
154
+ value += case escape_char
155
+ when '"', '\\', '/'
156
+ escape_char
157
+ when 'n'
158
+ "\n"
159
+ when 't'
160
+ "\t"
161
+ when 'r'
162
+ "\r"
163
+ when 'b'
164
+ "\b"
165
+ when 'f'
166
+ "\f"
167
+ when 'u'
168
+ # Unicode escape sequence \uXXXX
169
+ advance
170
+ hex = @source[@pos, 4]
171
+ raise ParseError, "Invalid unicode escape at #{@line}:#{@column}" unless hex =~ /\A[0-9a-fA-F]{4}\z/
172
+ advance(3)
173
+ [hex.to_i(16)].pack('U')
174
+ else
175
+ raise ParseError, "Invalid escape sequence \\#{escape_char} at #{@line}:#{@column}"
176
+ end
177
+ advance
178
+ else
179
+ value += char
180
+ advance
181
+ end
182
+ end
183
+
184
+ Token.new(:string, value, start_line, start_column)
185
+ end
186
+
187
+ def read_number(start_line, start_column)
188
+ value = ""
189
+ is_float = false
190
+
191
+ # Optional negative sign
192
+ if current_char == '-'
193
+ value += current_char
194
+ advance
195
+ end
196
+
197
+ # Integer part
198
+ if current_char == '0'
199
+ value += current_char
200
+ advance
201
+ else
202
+ while current_char =~ /[0-9]/
203
+ value += current_char
204
+ advance
205
+ end
206
+ end
207
+
208
+ # Fractional part
209
+ if current_char == '.'
210
+ is_float = true
211
+ value += current_char
212
+ advance
213
+
214
+ raise ParseError, "Invalid number at #{start_line}:#{start_column}" unless current_char =~ /[0-9]/
215
+
216
+ while current_char =~ /[0-9]/
217
+ value += current_char
218
+ advance
219
+ end
220
+ end
221
+
222
+ # Exponent part
223
+ if current_char =~ /[eE]/
224
+ is_float = true
225
+ value += current_char
226
+ advance
227
+
228
+ if current_char =~ /[+-]/
229
+ value += current_char
230
+ advance
231
+ end
232
+
233
+ raise ParseError, "Invalid number at #{start_line}:#{start_column}" unless current_char =~ /[0-9]/
234
+
235
+ while current_char =~ /[0-9]/
236
+ value += current_char
237
+ advance
238
+ end
239
+ end
240
+
241
+ type = is_float ? :float : :int
242
+ Token.new(type, value, start_line, start_column)
243
+ end
244
+
245
+ def read_name(start_line, start_column)
246
+ value = ""
247
+
248
+ while current_char =~ /[a-zA-Z0-9_]/
249
+ value += current_char
250
+ advance
251
+ end
252
+
253
+ # Check for keywords
254
+ type = case value
255
+ when "true", "false"
256
+ :boolean
257
+ when "null"
258
+ :null
259
+ else
260
+ :name
261
+ end
262
+
263
+ Token.new(type, value, start_line, start_column)
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,354 @@
1
+ module GraphQLite
2
+ # Parser builds an AST from tokens
3
+ class Parser
4
+ # AST Node types
5
+ Document = Struct.new(:definitions)
6
+ OperationDefinition = Struct.new(:operation_type, :name, :variable_definitions, :directives, :selection_set)
7
+ FragmentDefinition = Struct.new(:name, :type_condition, :directives, :selection_set)
8
+ VariableDefinition = Struct.new(:variable, :type, :default_value, :directives)
9
+ SelectionSet = Struct.new(:selections)
10
+ Field = Struct.new(:alias, :name, :arguments, :directives, :selection_set)
11
+ FragmentSpread = Struct.new(:name, :directives)
12
+ InlineFragment = Struct.new(:type_condition, :directives, :selection_set)
13
+ Argument = Struct.new(:name, :value)
14
+ Directive = Struct.new(:name, :arguments)
15
+ Variable = Struct.new(:name)
16
+
17
+ # Values
18
+ IntValue = Struct.new(:value)
19
+ FloatValue = Struct.new(:value)
20
+ StringValue = Struct.new(:value)
21
+ BooleanValue = Struct.new(:value)
22
+ NullValue = Struct.new(:value)
23
+ EnumValue = Struct.new(:value)
24
+ ListValue = Struct.new(:values)
25
+ ObjectValue = Struct.new(:fields)
26
+ ObjectField = Struct.new(:name, :value)
27
+
28
+ # Types
29
+ NamedType = Struct.new(:name)
30
+ ListType = Struct.new(:type)
31
+ NonNullType = Struct.new(:type)
32
+
33
+ attr_reader :tokens, :pos
34
+
35
+ def initialize(source)
36
+ @lexer = Lexer.new(source)
37
+ @tokens = @lexer.tokenize
38
+ @pos = 0
39
+ end
40
+
41
+ def parse
42
+ definitions = []
43
+
44
+ while current_token.type != :eof
45
+ definitions << parse_definition
46
+ end
47
+
48
+ Document.new(definitions)
49
+ end
50
+
51
+ private
52
+
53
+ def current_token
54
+ @tokens[@pos]
55
+ end
56
+
57
+ def peek_token(offset = 1)
58
+ @tokens[@pos + offset] || @tokens.last
59
+ end
60
+
61
+ def advance
62
+ @pos += 1
63
+ end
64
+
65
+ def expect(type)
66
+ token = current_token
67
+ raise ParseError, "Expected #{type} but got #{token.type} at #{token.line}:#{token.column}" unless token.type == type
68
+ advance
69
+ token
70
+ end
71
+
72
+ def skip_token(type)
73
+ advance if current_token.type == type
74
+ end
75
+
76
+ def parse_definition
77
+ token = current_token
78
+
79
+ case token.type
80
+ when :name
81
+ case token.value
82
+ when 'query', 'mutation', 'subscription'
83
+ parse_operation_definition
84
+ when 'fragment'
85
+ parse_fragment_definition
86
+ else
87
+ # Short-hand query (no operation keyword)
88
+ parse_operation_definition
89
+ end
90
+ when :lbrace
91
+ # Short-hand query
92
+ parse_operation_definition
93
+ else
94
+ raise ParseError, "Unexpected token #{token.type} at #{token.line}:#{token.column}"
95
+ end
96
+ end
97
+
98
+ def parse_operation_definition
99
+ operation_type = 'query' # default
100
+ name = nil
101
+ variable_definitions = []
102
+ directives = []
103
+
104
+ if current_token.type == :name && %w[query mutation subscription].include?(current_token.value)
105
+ operation_type = current_token.value
106
+ advance
107
+
108
+ # Operation name (optional)
109
+ if current_token.type == :name
110
+ name = current_token.value
111
+ advance
112
+ end
113
+
114
+ # Variable definitions (optional)
115
+ if current_token.type == :lparen
116
+ variable_definitions = parse_variable_definitions
117
+ end
118
+
119
+ # Directives (optional)
120
+ directives = parse_directives
121
+ end
122
+
123
+ # Selection set (required)
124
+ selection_set = parse_selection_set
125
+
126
+ OperationDefinition.new(operation_type, name, variable_definitions, directives, selection_set)
127
+ end
128
+
129
+ def parse_fragment_definition
130
+ expect(:name) # 'fragment'
131
+ name = expect(:name).value
132
+ expect(:name) # 'on'
133
+ type_condition = parse_named_type
134
+ directives = parse_directives
135
+ selection_set = parse_selection_set
136
+
137
+ FragmentDefinition.new(name, type_condition, directives, selection_set)
138
+ end
139
+
140
+ def parse_variable_definitions
141
+ expect(:lparen)
142
+ definitions = []
143
+
144
+ while current_token.type != :rparen
145
+ definitions << parse_variable_definition
146
+ end
147
+
148
+ expect(:rparen)
149
+ definitions
150
+ end
151
+
152
+ def parse_variable_definition
153
+ expect(:dollar)
154
+ variable_name = expect(:name).value
155
+ expect(:colon)
156
+ type = parse_type
157
+
158
+ default_value = nil
159
+ if current_token.type == :equals
160
+ advance
161
+ default_value = parse_value_literal
162
+ end
163
+
164
+ directives = parse_directives
165
+
166
+ VariableDefinition.new(Variable.new(variable_name), type, default_value, directives)
167
+ end
168
+
169
+ def parse_type
170
+ type = if current_token.type == :lbracket
171
+ advance
172
+ inner_type = parse_type
173
+ expect(:rbracket)
174
+ ListType.new(inner_type)
175
+ else
176
+ parse_named_type
177
+ end
178
+
179
+ if current_token.type == :bang
180
+ advance
181
+ type = NonNullType.new(type)
182
+ end
183
+
184
+ type
185
+ end
186
+
187
+ def parse_named_type
188
+ name = expect(:name).value
189
+ NamedType.new(name)
190
+ end
191
+
192
+ def parse_selection_set
193
+ expect(:lbrace)
194
+ selections = []
195
+
196
+ while current_token.type != :rbrace
197
+ selections << parse_selection
198
+ end
199
+
200
+ expect(:rbrace)
201
+ SelectionSet.new(selections)
202
+ end
203
+
204
+ def parse_selection
205
+ if current_token.type == :spread
206
+ advance
207
+ if current_token.type == :name && current_token.value != 'on'
208
+ # Fragment spread
209
+ name = expect(:name).value
210
+ directives = parse_directives
211
+ FragmentSpread.new(name, directives)
212
+ else
213
+ # Inline fragment
214
+ type_condition = nil
215
+ if current_token.type == :name && current_token.value == 'on'
216
+ advance
217
+ type_condition = parse_named_type
218
+ end
219
+ directives = parse_directives
220
+ selection_set = parse_selection_set
221
+ InlineFragment.new(type_condition, directives, selection_set)
222
+ end
223
+ else
224
+ parse_field
225
+ end
226
+ end
227
+
228
+ def parse_field
229
+ # Alias or field name
230
+ name_or_alias = expect(:name).value
231
+
232
+ field_name = name_or_alias
233
+ field_alias = nil
234
+
235
+ if current_token.type == :colon
236
+ advance
237
+ field_alias = name_or_alias
238
+ field_name = expect(:name).value
239
+ end
240
+
241
+ # Arguments
242
+ arguments = current_token.type == :lparen ? parse_arguments : []
243
+
244
+ # Directives
245
+ directives = parse_directives
246
+
247
+ # Selection set (optional)
248
+ selection_set = current_token.type == :lbrace ? parse_selection_set : nil
249
+
250
+ Field.new(field_alias, field_name, arguments, directives, selection_set)
251
+ end
252
+
253
+ def parse_arguments
254
+ expect(:lparen)
255
+ arguments = []
256
+
257
+ while current_token.type != :rparen
258
+ arguments << parse_argument
259
+ end
260
+
261
+ expect(:rparen)
262
+ arguments
263
+ end
264
+
265
+ def parse_argument
266
+ name = expect(:name).value
267
+ expect(:colon)
268
+ value = parse_value
269
+ Argument.new(name, value)
270
+ end
271
+
272
+ def parse_directives
273
+ directives = []
274
+
275
+ while current_token.type == :at
276
+ advance
277
+ name = expect(:name).value
278
+ arguments = current_token.type == :lparen ? parse_arguments : []
279
+ directives << Directive.new(name, arguments)
280
+ end
281
+
282
+ directives
283
+ end
284
+
285
+ def parse_value
286
+ case current_token.type
287
+ when :dollar
288
+ advance
289
+ name = expect(:name).value
290
+ Variable.new(name)
291
+ else
292
+ parse_value_literal
293
+ end
294
+ end
295
+
296
+ def parse_value_literal
297
+ token = current_token
298
+
299
+ case token.type
300
+ when :int
301
+ advance
302
+ IntValue.new(token.value.to_i)
303
+ when :float
304
+ advance
305
+ FloatValue.new(token.value.to_f)
306
+ when :string
307
+ advance
308
+ StringValue.new(token.value)
309
+ when :boolean
310
+ advance
311
+ BooleanValue.new(token.value == 'true')
312
+ when :null
313
+ advance
314
+ NullValue.new(nil)
315
+ when :name
316
+ advance
317
+ EnumValue.new(token.value)
318
+ when :lbracket
319
+ parse_list_value
320
+ when :lbrace
321
+ parse_object_value
322
+ else
323
+ raise ParseError, "Unexpected token #{token.type} at #{token.line}:#{token.column}"
324
+ end
325
+ end
326
+
327
+ def parse_list_value
328
+ expect(:lbracket)
329
+ values = []
330
+
331
+ while current_token.type != :rbracket
332
+ values << parse_value_literal
333
+ end
334
+
335
+ expect(:rbracket)
336
+ ListValue.new(values)
337
+ end
338
+
339
+ def parse_object_value
340
+ expect(:lbrace)
341
+ fields = []
342
+
343
+ while current_token.type != :rbrace
344
+ name = expect(:name).value
345
+ expect(:colon)
346
+ value = parse_value_literal
347
+ fields << ObjectField.new(name, value)
348
+ end
349
+
350
+ expect(:rbrace)
351
+ ObjectValue.new(fields)
352
+ end
353
+ end
354
+ end