gql 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +160 -0
- data/Rakefile +22 -0
- data/bin/console +25 -0
- data/bin/setup +5 -0
- data/gql.gemspec +30 -0
- data/lib/gql.rb +68 -0
- data/lib/gql/call.rb +22 -0
- data/lib/gql/connection.rb +32 -0
- data/lib/gql/errors.rb +58 -0
- data/lib/gql/executor.rb +18 -0
- data/lib/gql/field.rb +7 -0
- data/lib/gql/fields/boolean.rb +6 -0
- data/lib/gql/fields/connection.rb +29 -0
- data/lib/gql/fields/float.rb +6 -0
- data/lib/gql/fields/integer.rb +10 -0
- data/lib/gql/fields/object.rb +17 -0
- data/lib/gql/fields/string.rb +19 -0
- data/lib/gql/node.rb +121 -0
- data/lib/gql/parser.rb +704 -0
- data/lib/gql/parser.y +281 -0
- data/lib/gql/schema.rb +21 -0
- data/lib/gql/tokenizer.rb +130 -0
- data/lib/gql/tokenizer.rex +46 -0
- data/lib/gql/version.rb +3 -0
- metadata +156 -0
data/lib/gql/parser.y
ADDED
@@ -0,0 +1,281 @@
|
|
1
|
+
class GQL::Parser
|
2
|
+
token STRING NUMBER TRUE FALSE NULL AS IDENT
|
3
|
+
rule
|
4
|
+
query
|
5
|
+
: variables call variables { result = QueryNode.new(val[1], convert_variables(val[0], val[2])) }
|
6
|
+
;
|
7
|
+
|
8
|
+
call
|
9
|
+
: identifier arguments fields { result = CallNode.new(val[0], val[1], nil, val[2].presence) }
|
10
|
+
| identifier arguments sub_call { result = CallNode.new(val[0], val[1], val[2], nil) }
|
11
|
+
| identifier arguments { result = CallNode.new(val[0], val[1], nil, nil) }
|
12
|
+
;
|
13
|
+
|
14
|
+
sub_call
|
15
|
+
: '.' call { result = val[1] }
|
16
|
+
;
|
17
|
+
|
18
|
+
arguments
|
19
|
+
: /* empty */ { result = [] }
|
20
|
+
| '(' ')' { result = [] }
|
21
|
+
| '(' argument_list ')' { result = val[1] }
|
22
|
+
;
|
23
|
+
|
24
|
+
argument_list
|
25
|
+
: argument_list ',' argument { result.push val[2] }
|
26
|
+
| argument { result = val }
|
27
|
+
;
|
28
|
+
|
29
|
+
argument
|
30
|
+
: variable_identifier
|
31
|
+
| json_text
|
32
|
+
;
|
33
|
+
|
34
|
+
fields
|
35
|
+
: '{' '}' { result = [] }
|
36
|
+
| '{' field_list '}' { result = val[1] }
|
37
|
+
;
|
38
|
+
|
39
|
+
field_list
|
40
|
+
: field_list ',' field { result.push val[2] }
|
41
|
+
| field { result = val }
|
42
|
+
;
|
43
|
+
|
44
|
+
field
|
45
|
+
: identifier fields alias_identifier { result = FieldNode.new(val[0], val[2], nil, val[1].presence) }
|
46
|
+
| identifier sub_call alias_identifier { result = FieldNode.new(val[0], val[2], val[1], nil) }
|
47
|
+
| identifier alias_identifier { result = FieldNode.new(val[0], val[1], nil, nil) }
|
48
|
+
| identifier fields { result = FieldNode.new(val[0], nil, nil, val[1].presence) }
|
49
|
+
| identifier sub_call { result = FieldNode.new(val[0], nil, val[1], nil) }
|
50
|
+
| identifier { result = FieldNode.new(val[0], nil, nil, nil) }
|
51
|
+
;
|
52
|
+
|
53
|
+
alias_identifier
|
54
|
+
: AS identifier { result = val[1] }
|
55
|
+
;
|
56
|
+
|
57
|
+
variables
|
58
|
+
: /* empty */ { result = [] }
|
59
|
+
| variable_list { result = val[0] }
|
60
|
+
;
|
61
|
+
|
62
|
+
variable_list
|
63
|
+
: variable { result = val }
|
64
|
+
| variable_list variable { result.push val[1] }
|
65
|
+
;
|
66
|
+
|
67
|
+
variable
|
68
|
+
: variable_identifier '=' variable_value { result = [val[0], val[2]] }
|
69
|
+
;
|
70
|
+
|
71
|
+
variable_identifier
|
72
|
+
: '<' identifier '>' { result = val[1] }
|
73
|
+
;
|
74
|
+
|
75
|
+
variable_value
|
76
|
+
: json_text
|
77
|
+
;
|
78
|
+
|
79
|
+
json_text
|
80
|
+
: json_value { result = @json.result }
|
81
|
+
|
82
|
+
json_value
|
83
|
+
: object
|
84
|
+
| array
|
85
|
+
| scalar
|
86
|
+
;
|
87
|
+
|
88
|
+
object
|
89
|
+
: start_object end_object
|
90
|
+
| start_object pairs end_object
|
91
|
+
;
|
92
|
+
|
93
|
+
start_object : '{' { @json.start_object } ;
|
94
|
+
end_object : '}' { @json.end_object } ;
|
95
|
+
|
96
|
+
pairs
|
97
|
+
: pairs ',' pair
|
98
|
+
| pair
|
99
|
+
;
|
100
|
+
|
101
|
+
pair
|
102
|
+
: string ':' json_value
|
103
|
+
;
|
104
|
+
|
105
|
+
array
|
106
|
+
: start_array end_array
|
107
|
+
| start_array values end_array
|
108
|
+
;
|
109
|
+
|
110
|
+
start_array : '[' { @json.start_array } ;
|
111
|
+
end_array : ']' { @json.end_array } ;
|
112
|
+
|
113
|
+
values
|
114
|
+
: values ',' json_value
|
115
|
+
| json_value
|
116
|
+
;
|
117
|
+
|
118
|
+
scalar
|
119
|
+
: string
|
120
|
+
| literal { @json.scalar val[0] }
|
121
|
+
;
|
122
|
+
|
123
|
+
string
|
124
|
+
: STRING { @json.scalar unescape_string(val[0]) }
|
125
|
+
;
|
126
|
+
|
127
|
+
literal
|
128
|
+
: NUMBER { result = convert_number(val[0]) }
|
129
|
+
| TRUE { result = true }
|
130
|
+
| FALSE { result = false }
|
131
|
+
| NULL { result = nil }
|
132
|
+
;
|
133
|
+
|
134
|
+
identifier
|
135
|
+
: IDENT { result = val[0].to_sym }
|
136
|
+
;
|
137
|
+
end
|
138
|
+
|
139
|
+
---- header
|
140
|
+
|
141
|
+
require 'json'
|
142
|
+
require 'active_support/core_ext/object/blank'
|
143
|
+
|
144
|
+
---- inner
|
145
|
+
|
146
|
+
class QueryNode < Struct.new(:call, :variables)
|
147
|
+
end
|
148
|
+
|
149
|
+
class FieldNode < Struct.new(:name, :alias_name, :call, :fields)
|
150
|
+
end
|
151
|
+
|
152
|
+
class CallNode < Struct.new(:name, :arguments, :call, :fields)
|
153
|
+
end
|
154
|
+
|
155
|
+
class JSONHandler
|
156
|
+
attr_reader :stack
|
157
|
+
|
158
|
+
def initialize
|
159
|
+
clear
|
160
|
+
end
|
161
|
+
|
162
|
+
def start_object
|
163
|
+
push [:hash]
|
164
|
+
end
|
165
|
+
|
166
|
+
def start_array
|
167
|
+
push [:array]
|
168
|
+
end
|
169
|
+
|
170
|
+
def end_array
|
171
|
+
@stack.pop
|
172
|
+
end
|
173
|
+
|
174
|
+
alias :end_object :end_array
|
175
|
+
|
176
|
+
def scalar(s)
|
177
|
+
@stack.last << [:scalar, s]
|
178
|
+
end
|
179
|
+
|
180
|
+
def push(o)
|
181
|
+
@stack.last << o
|
182
|
+
@stack << o
|
183
|
+
end
|
184
|
+
|
185
|
+
def result
|
186
|
+
root = @stack.first.last
|
187
|
+
value = process(root.first, root.drop(1))
|
188
|
+
clear
|
189
|
+
value
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
def clear
|
194
|
+
@stack = [[:json]]
|
195
|
+
end
|
196
|
+
|
197
|
+
def process(type, rest)
|
198
|
+
case type
|
199
|
+
when :array
|
200
|
+
rest.map { |x| process(x.first, x.drop(1)) }
|
201
|
+
when :hash
|
202
|
+
Hash[rest.map { |x|
|
203
|
+
process(x.first, x.drop(1))
|
204
|
+
}.each_slice(2).to_a]
|
205
|
+
when :scalar
|
206
|
+
rest.first
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
UNESCAPE_MAP = Hash.new { |h, k| h[k] = k.chr }
|
212
|
+
|
213
|
+
UNESCAPE_MAP.update(
|
214
|
+
?" => '"',
|
215
|
+
?\\ => '\\',
|
216
|
+
?/ => '/',
|
217
|
+
?b => "\b",
|
218
|
+
?f => "\f",
|
219
|
+
?n => "\n",
|
220
|
+
?r => "\r",
|
221
|
+
?t => "\t",
|
222
|
+
?u => nil,
|
223
|
+
)
|
224
|
+
|
225
|
+
EMPTY_8BIT_STRING = ''
|
226
|
+
|
227
|
+
if String.method_defined? :encode
|
228
|
+
EMPTY_8BIT_STRING.force_encoding Encoding::ASCII_8BIT
|
229
|
+
end
|
230
|
+
|
231
|
+
def initialize(tokenizer)
|
232
|
+
super()
|
233
|
+
|
234
|
+
@tokenizer = tokenizer
|
235
|
+
end
|
236
|
+
|
237
|
+
def next_token
|
238
|
+
@tokenizer.next_token
|
239
|
+
end
|
240
|
+
|
241
|
+
def parse
|
242
|
+
@json = JSONHandler.new
|
243
|
+
do_parse
|
244
|
+
end
|
245
|
+
|
246
|
+
def on_error(token, value, vstack)
|
247
|
+
raise Errors::ParseError.new(value, token_to_str(token))
|
248
|
+
end
|
249
|
+
|
250
|
+
private
|
251
|
+
def unescape_string(str)
|
252
|
+
string = str.gsub(%r((?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff]))n) do |c|
|
253
|
+
if u = UNESCAPE_MAP[$&[1]]
|
254
|
+
u
|
255
|
+
else # \uXXXX
|
256
|
+
bytes = EMPTY_8BIT_STRING.dup
|
257
|
+
i = 0
|
258
|
+
|
259
|
+
while c[6 * i] == ?\\ && c[6 * i + 1] == ?u
|
260
|
+
bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16)
|
261
|
+
i += 1
|
262
|
+
end
|
263
|
+
|
264
|
+
JSON.iconv('utf-8', 'utf-16be', bytes)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
if string.respond_to? :force_encoding
|
269
|
+
string.force_encoding ::Encoding::UTF_8
|
270
|
+
end
|
271
|
+
|
272
|
+
string
|
273
|
+
end
|
274
|
+
|
275
|
+
def convert_number(str)
|
276
|
+
str.count('.') > 0 ? str.to_f : str.to_i
|
277
|
+
end
|
278
|
+
|
279
|
+
def convert_variables(arr1, arr2)
|
280
|
+
Hash[*arr1.flatten(1)].merge Hash[*arr2.flatten(1)]
|
281
|
+
end
|
data/lib/gql/schema.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module GQL
|
5
|
+
class Schema
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
class << self
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
delegate [:root, :root=, :fields] => :instance
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_accessor :root
|
15
|
+
attr_reader :fields
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@fields = {}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
#--
|
2
|
+
# DO NOT MODIFY!!!!
|
3
|
+
# This file is automatically generated by rex 1.0.5
|
4
|
+
# from lexical definition file "lib/gql/tokenizer.rex".
|
5
|
+
#++
|
6
|
+
|
7
|
+
require 'racc/parser'
|
8
|
+
class GQL::Tokenizer < Racc::Parser
|
9
|
+
require 'strscan'
|
10
|
+
|
11
|
+
class ScanError < StandardError ; end
|
12
|
+
|
13
|
+
attr_reader :lineno
|
14
|
+
attr_reader :filename
|
15
|
+
attr_accessor :state
|
16
|
+
|
17
|
+
def scan_setup(str)
|
18
|
+
@ss = StringScanner.new(str)
|
19
|
+
@lineno = 1
|
20
|
+
@state = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def action
|
24
|
+
yield
|
25
|
+
end
|
26
|
+
|
27
|
+
def scan_str(str)
|
28
|
+
scan_setup(str)
|
29
|
+
do_parse
|
30
|
+
end
|
31
|
+
alias :scan :scan_str
|
32
|
+
|
33
|
+
def load_file( filename )
|
34
|
+
@filename = filename
|
35
|
+
open(filename, "r") do |f|
|
36
|
+
scan_setup(f.read)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def scan_file( filename )
|
41
|
+
load_file(filename)
|
42
|
+
do_parse
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def next_token
|
47
|
+
return if @ss.eos?
|
48
|
+
|
49
|
+
# skips empty actions
|
50
|
+
until token = _next_token or @ss.eos?; end
|
51
|
+
token
|
52
|
+
end
|
53
|
+
|
54
|
+
def _next_token
|
55
|
+
text = @ss.peek(1)
|
56
|
+
@lineno += 1 if text == "\n"
|
57
|
+
token = case @state
|
58
|
+
when nil
|
59
|
+
case
|
60
|
+
when (text = @ss.scan(/\/\*/))
|
61
|
+
action { @state = :REMS; nil }
|
62
|
+
|
63
|
+
when (text = @ss.scan(/\/\//))
|
64
|
+
action { @state = :REM; nil }
|
65
|
+
|
66
|
+
when (text = @ss.scan(/"(?:[^"\\]|\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4}))*"/))
|
67
|
+
action { [:STRING, text.gsub(/^"|"$/, '')] }
|
68
|
+
|
69
|
+
when (text = @ss.scan(/-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/))
|
70
|
+
action { [:NUMBER, text] }
|
71
|
+
|
72
|
+
when (text = @ss.scan(/true/))
|
73
|
+
action { [:TRUE, text] }
|
74
|
+
|
75
|
+
when (text = @ss.scan(/false/))
|
76
|
+
action { [:FALSE, text] }
|
77
|
+
|
78
|
+
when (text = @ss.scan(/null/))
|
79
|
+
action { [:NULL, text] }
|
80
|
+
|
81
|
+
when (text = @ss.scan(/[aA][sS]/))
|
82
|
+
action { [:AS, text] }
|
83
|
+
|
84
|
+
when (text = @ss.scan(/[a-zA-Z_][a-zA-Z0-9_]*/))
|
85
|
+
action { [:IDENT, text] }
|
86
|
+
|
87
|
+
when (text = @ss.scan(/\s+/))
|
88
|
+
;
|
89
|
+
|
90
|
+
when (text = @ss.scan(/./))
|
91
|
+
action { [text, text] }
|
92
|
+
|
93
|
+
else
|
94
|
+
text = @ss.string[@ss.pos .. -1]
|
95
|
+
raise ScanError, "can not match: '" + text + "'"
|
96
|
+
end # if
|
97
|
+
|
98
|
+
when :REMS
|
99
|
+
case
|
100
|
+
when (text = @ss.scan(/\*\//))
|
101
|
+
action { @state = nil; nil }
|
102
|
+
|
103
|
+
when (text = @ss.scan(/.*(?=\*\/)/))
|
104
|
+
;
|
105
|
+
|
106
|
+
else
|
107
|
+
text = @ss.string[@ss.pos .. -1]
|
108
|
+
raise ScanError, "can not match: '" + text + "'"
|
109
|
+
end # if
|
110
|
+
|
111
|
+
when :REM
|
112
|
+
case
|
113
|
+
when (text = @ss.scan(/\n/))
|
114
|
+
action { @state = nil; nil }
|
115
|
+
|
116
|
+
when (text = @ss.scan(/.*(?=$)/))
|
117
|
+
;
|
118
|
+
|
119
|
+
else
|
120
|
+
text = @ss.string[@ss.pos .. -1]
|
121
|
+
raise ScanError, "can not match: '" + text + "'"
|
122
|
+
end # if
|
123
|
+
|
124
|
+
else
|
125
|
+
raise ScanError, "undefined state: '" + state.to_s + "'"
|
126
|
+
end # case state
|
127
|
+
token
|
128
|
+
end # def _next_token
|
129
|
+
|
130
|
+
end # class
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class GQL::Tokenizer
|
2
|
+
macro
|
3
|
+
BLANK \s+
|
4
|
+
REM_IN \/\*
|
5
|
+
REM_OUT \*\/
|
6
|
+
REM \/\/
|
7
|
+
STRING "(?:[^"\\]|\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4}))*"
|
8
|
+
NUMBER -?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?
|
9
|
+
TRUE true
|
10
|
+
FALSE false
|
11
|
+
NULL null
|
12
|
+
IDENT [a-zA-Z_][a-zA-Z0-9_]*
|
13
|
+
AS [aA][sS]
|
14
|
+
|
15
|
+
rule
|
16
|
+
|
17
|
+
# [:state] pattern [action]
|
18
|
+
|
19
|
+
# remarks
|
20
|
+
{REM_IN} { @state = :REMS; nil }
|
21
|
+
:REMS {REM_OUT} { @state = nil; nil }
|
22
|
+
:REMS .*(?={REM_OUT}) # ignore
|
23
|
+
{REM} { @state = :REM; nil }
|
24
|
+
:REM \n { @state = nil; nil }
|
25
|
+
:REM .*(?=$) # ignore
|
26
|
+
|
27
|
+
# scalars
|
28
|
+
{STRING} { [:STRING, text.gsub(/^"|"$/, '')] }
|
29
|
+
{NUMBER} { [:NUMBER, text] }
|
30
|
+
{TRUE} { [:TRUE, text] }
|
31
|
+
{FALSE} { [:FALSE, text] }
|
32
|
+
{NULL} { [:NULL, text] }
|
33
|
+
|
34
|
+
# keywords
|
35
|
+
{AS} { [:AS, text] }
|
36
|
+
|
37
|
+
# identifier
|
38
|
+
{IDENT} { [:IDENT, text] }
|
39
|
+
|
40
|
+
# whitespace
|
41
|
+
{BLANK} # ignore
|
42
|
+
|
43
|
+
# rest
|
44
|
+
. { [text, text] }
|
45
|
+
|
46
|
+
end
|