tinygql 0.1.4 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +23 -0
- data/bin/bench.rb +38 -0
- data/bin/make_hash.rb +51 -0
- data/lib/tinygql/lexer.rb +115 -42
- data/lib/tinygql/nodes.rb +130 -81
- data/lib/tinygql/nodes.rb.erb +15 -2
- data/lib/tinygql/parser.rb +119 -65
- data/lib/tinygql/version.rb +1 -1
- data/test/lexer_test.rb +101 -0
- data/test/parser_test.rb +17 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e6f2ac9d1c4531277689c6eec21fd29cd1e6fe9715284813b57c741e3e9bace2
|
4
|
+
data.tar.gz: b9cd18ac29f1aeea347296f0408deadddec7a902d25495e3232b472180718695
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 63c931df357b6091070a7cd0adab859f7a7489f8fa1220ffa2cfb7fc6a7af6660ede87af6c3a8b804ef1a2114594aa3086a3792b5d3ef40e20ac1a4546d13120
|
7
|
+
data.tar.gz: 96e0a9164e835cb382391c9ea761cdf2405ac6f38e268034f810f8ce369d4e968ec5a2c174f22f03f50a99bd1f155449e5d9e7b46c65fc771263cbc03a8f2f8b
|
data/README.md
CHANGED
@@ -66,6 +66,29 @@ ast = TinyGQL.parse "{ neat { cool } }"
|
|
66
66
|
p ast.fold(Fold, []) # => ["neat", "cool"]
|
67
67
|
```
|
68
68
|
|
69
|
+
Nodes store their position in the source GraphQL document.
|
70
|
+
If you'd like to extract the line number of the node, you'll need to keep a reference to the document and pass it to the `line` method on the node:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
doc = <<-eod
|
74
|
+
mutation {
|
75
|
+
likeStory(sturyID: 12345) {
|
76
|
+
story {
|
77
|
+
likeCount
|
78
|
+
}
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
eod
|
83
|
+
|
84
|
+
parser = TinyGQL::Parser.new doc
|
85
|
+
ast = parser.parse
|
86
|
+
|
87
|
+
ast.find_all(&:field?).each { |node|
|
88
|
+
p node.name => node.line(doc)
|
89
|
+
}
|
90
|
+
```
|
91
|
+
|
69
92
|
## LICENSE:
|
70
93
|
|
71
94
|
I've licensed this code as Apache 2.0, but the lexer is from [GraphQL-Ruby](https://github.com/rmosolgo/graphql-ruby/blob/772734dfcc7aa0513c867259912474ef0ba799c3/lib/graphql/language/lexer.rb) and is under the MIT license.
|
data/bin/bench.rb
CHANGED
@@ -20,3 +20,41 @@ Benchmark.ips do |x|
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
23
|
+
|
24
|
+
module Benchmark
|
25
|
+
def self.allocs; yield Allocs; end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Allocs
|
29
|
+
def self.report name, &block
|
30
|
+
allocs = nil
|
31
|
+
|
32
|
+
2.times do # 2 times to heat caches
|
33
|
+
allocs = 10.times.map {
|
34
|
+
x = GC.stat(:total_allocated_objects)
|
35
|
+
yield
|
36
|
+
GC.stat(:total_allocated_objects) - x
|
37
|
+
}.inject(:+) / 10
|
38
|
+
end
|
39
|
+
|
40
|
+
puts name.rjust(20) + allocs.to_s.rjust(10)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
print "#" * 30
|
45
|
+
print " ALLOCATIONS "
|
46
|
+
puts "#" * 30
|
47
|
+
|
48
|
+
Benchmark.allocs do |x|
|
49
|
+
x.report "kitchen-sink" do
|
50
|
+
TinyGQL.parse source
|
51
|
+
end
|
52
|
+
|
53
|
+
files.each do |file_name|
|
54
|
+
data = File.read file_name
|
55
|
+
name = File.basename(file_name, File.extname(file_name))
|
56
|
+
x.report name do
|
57
|
+
TinyGQL.parse data
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/bin/make_hash.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require "tinygql"
|
2
|
+
|
3
|
+
# Calculate a perfect hash for GraphQL keywords
|
4
|
+
|
5
|
+
def bits x
|
6
|
+
count = 0
|
7
|
+
while x > 0
|
8
|
+
count += 1
|
9
|
+
x >>= 1
|
10
|
+
end
|
11
|
+
count
|
12
|
+
end
|
13
|
+
|
14
|
+
# on is too short, and subscription is the longest.
|
15
|
+
# The lexer can easily detect them by length, so lets calculate a perfect
|
16
|
+
# hash for the rest.
|
17
|
+
kws = TinyGQL::Lexer::KEYWORDS - ["on", "subscription"]
|
18
|
+
MASK = (1 << bits(kws.length)) - 1
|
19
|
+
|
20
|
+
prefixes = kws.map { |word| word[1, 2] }
|
21
|
+
|
22
|
+
# make sure they're unique
|
23
|
+
raise "Not unique" unless prefixes.uniq == prefixes
|
24
|
+
|
25
|
+
keys = prefixes.map { |prefix|
|
26
|
+
prefix.bytes.reverse.inject(0) { |c, byte|
|
27
|
+
c << 8 | byte
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
shift = 32 - bits(kws.length) # use the top bits
|
32
|
+
|
33
|
+
c = 13
|
34
|
+
loop do
|
35
|
+
z = keys.map { |k| ((k * c) >> shift) & MASK }
|
36
|
+
break if z.uniq.length == z.length
|
37
|
+
c += 1
|
38
|
+
end
|
39
|
+
|
40
|
+
table = kws.zip(keys).each_with_object([]) { |(word, k), o|
|
41
|
+
hash = ((k * c) >> shift) & MASK
|
42
|
+
o[hash] = word.upcase.to_sym
|
43
|
+
}
|
44
|
+
|
45
|
+
print "KW_LUT ="
|
46
|
+
pp table
|
47
|
+
puts <<-eomethod
|
48
|
+
def hash key
|
49
|
+
(key * #{c}) >> #{shift} & #{sprintf("%#0x", MASK)}
|
50
|
+
end
|
51
|
+
eomethod
|
data/lib/tinygql/lexer.rb
CHANGED
@@ -16,27 +16,29 @@ module TinyGQL
|
|
16
16
|
FLOAT_EXP = /[eE][+-]?[0-9]+/
|
17
17
|
NUMERIC = /#{INT}(#{FLOAT_DECIMAL}#{FLOAT_EXP}|#{FLOAT_DECIMAL}|#{FLOAT_EXP})?/
|
18
18
|
|
19
|
-
KEYWORDS =
|
20
|
-
"on"
|
21
|
-
"fragment"
|
22
|
-
"true"
|
23
|
-
"false"
|
24
|
-
"null"
|
25
|
-
"query"
|
26
|
-
"mutation"
|
27
|
-
"subscription"
|
28
|
-
"schema"
|
29
|
-
"scalar"
|
30
|
-
"type"
|
31
|
-
"extend"
|
32
|
-
"implements"
|
33
|
-
"interface"
|
34
|
-
"union"
|
35
|
-
"enum"
|
36
|
-
"input"
|
37
|
-
"directive"
|
38
|
-
"repeatable"
|
39
|
-
|
19
|
+
KEYWORDS = [
|
20
|
+
"on",
|
21
|
+
"fragment",
|
22
|
+
"true",
|
23
|
+
"false",
|
24
|
+
"null",
|
25
|
+
"query",
|
26
|
+
"mutation",
|
27
|
+
"subscription",
|
28
|
+
"schema",
|
29
|
+
"scalar",
|
30
|
+
"type",
|
31
|
+
"extend",
|
32
|
+
"implements",
|
33
|
+
"interface",
|
34
|
+
"union",
|
35
|
+
"enum",
|
36
|
+
"input",
|
37
|
+
"directive",
|
38
|
+
"repeatable"
|
39
|
+
].freeze
|
40
|
+
|
41
|
+
KW_RE = /#{Regexp.union(KEYWORDS.sort)}\b/
|
40
42
|
|
41
43
|
module Literals
|
42
44
|
LCURLY = '{'
|
@@ -48,13 +50,14 @@ module TinyGQL
|
|
48
50
|
COLON = ':'
|
49
51
|
VAR_SIGN = '$'
|
50
52
|
DIR_SIGN = '@'
|
51
|
-
ELLIPSIS = '...'
|
52
53
|
EQUALS = '='
|
53
54
|
BANG = '!'
|
54
55
|
PIPE = '|'
|
55
56
|
AMP = '&'
|
56
57
|
end
|
57
58
|
|
59
|
+
ELLIPSIS = '...'
|
60
|
+
|
58
61
|
include Literals
|
59
62
|
|
60
63
|
QUOTE = '"'
|
@@ -68,12 +71,10 @@ module TinyGQL
|
|
68
71
|
ESCAPED_QUOTE = /\\"/;
|
69
72
|
STRING_CHAR = /#{ESCAPED_QUOTE}|[^"\\]|#{UNICODE_ESCAPE}|#{STRING_ESCAPE}/
|
70
73
|
|
71
|
-
LIT_NAME_LUT = Literals.constants.each_with_object(
|
72
|
-
o[Literals.const_get(n)] = n
|
74
|
+
LIT_NAME_LUT = Literals.constants.each_with_object([]) { |n, o|
|
75
|
+
o[Literals.const_get(n).ord] = n
|
73
76
|
}
|
74
77
|
|
75
|
-
LIT = Regexp.union(Literals.constants.map { |n| Literals.const_get(n) })
|
76
|
-
|
77
78
|
QUOTED_STRING = %r{#{QUOTE} ((?:#{STRING_CHAR})*) #{QUOTE}}x
|
78
79
|
BLOCK_STRING = %r{
|
79
80
|
#{BLOCK_QUOTE}
|
@@ -90,11 +91,13 @@ module TinyGQL
|
|
90
91
|
def initialize string
|
91
92
|
raise unless string.valid_encoding?
|
92
93
|
|
94
|
+
@string = string
|
93
95
|
@scan = StringScanner.new string
|
94
|
-
@
|
95
|
-
@token_value = nil
|
96
|
+
@start = nil
|
96
97
|
end
|
97
98
|
|
99
|
+
attr_reader :start
|
100
|
+
|
98
101
|
def line
|
99
102
|
@scan.string[0, @scan.pos].count("\n") + 1
|
100
103
|
end
|
@@ -106,28 +109,61 @@ module TinyGQL
|
|
106
109
|
def advance
|
107
110
|
@scan.skip(IGNORE)
|
108
111
|
|
112
|
+
@start = @scan.pos
|
113
|
+
|
109
114
|
case
|
110
|
-
when
|
111
|
-
when
|
112
|
-
when @
|
113
|
-
|
114
|
-
|
115
|
-
when @scan.
|
115
|
+
when @scan.eos? then false
|
116
|
+
when @scan.skip(ELLIPSIS) then :ELLIPSIS
|
117
|
+
when tok = LIT_NAME_LUT[@string.getbyte(@start)] then
|
118
|
+
@scan.pos += 1
|
119
|
+
tok
|
120
|
+
when len = @scan.skip(KW_RE) then
|
121
|
+
return :ON if len == 2
|
122
|
+
return :SUBSCRIPTION if len == 12
|
123
|
+
|
124
|
+
pos = @start
|
125
|
+
|
126
|
+
# Second 2 bytes are unique, so we'll hash on those
|
127
|
+
key = (@string.getbyte(pos + 2) << 8) | @string.getbyte(pos + 1)
|
128
|
+
|
129
|
+
KW_LUT[_hash(key)]
|
130
|
+
when @scan.skip(IDENTIFIER) then :IDENTIFIER
|
131
|
+
when @scan.skip(BLOCK_STRING) then :STRING
|
132
|
+
when @scan.skip(QUOTED_STRING) then :STRING
|
133
|
+
when @scan.skip(NUMERIC) then (@scan[1] ? :FLOAT : :INT)
|
116
134
|
else
|
117
|
-
|
135
|
+
@scan.getch
|
136
|
+
:UNKNOWN_CHAR
|
118
137
|
end
|
119
138
|
end
|
120
139
|
|
121
|
-
|
140
|
+
def token_value
|
141
|
+
@string.byteslice(@scan.pos - @scan.matched_size, @scan.matched_size)
|
142
|
+
end
|
143
|
+
|
144
|
+
def string_value
|
145
|
+
str = token_value
|
146
|
+
block = str.start_with?('"""')
|
147
|
+
str.gsub!(/\A"*|"*\z/, '')
|
122
148
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
149
|
+
if block
|
150
|
+
emit_block str
|
151
|
+
else
|
152
|
+
emit_string str
|
153
|
+
end
|
127
154
|
end
|
128
155
|
|
129
156
|
def next_token
|
130
|
-
|
157
|
+
return unless tok = advance
|
158
|
+
val = case tok
|
159
|
+
when :STRING then string_value
|
160
|
+
when *Literals.constants
|
161
|
+
@string.byteslice(@scan.pos - 1, 1)
|
162
|
+
else
|
163
|
+
token_value
|
164
|
+
end
|
165
|
+
|
166
|
+
[tok, val]
|
131
167
|
end
|
132
168
|
|
133
169
|
# Replace any escaped unicode or whitespace with the _actual_ characters
|
@@ -184,7 +220,7 @@ module TinyGQL
|
|
184
220
|
if !value.valid_encoding?
|
185
221
|
emit(:BAD_UNICODE_ESCAPE, value)
|
186
222
|
else
|
187
|
-
|
223
|
+
value
|
188
224
|
end
|
189
225
|
end
|
190
226
|
end
|
@@ -242,5 +278,42 @@ module TinyGQL
|
|
242
278
|
# Rebuild the string
|
243
279
|
lines.size > 1 ? lines.join("\n") : (lines.first || "".dup)
|
244
280
|
end
|
281
|
+
|
282
|
+
KW_LUT =[:INTERFACE,
|
283
|
+
:MUTATION,
|
284
|
+
:EXTEND,
|
285
|
+
:FALSE,
|
286
|
+
:ENUM,
|
287
|
+
:TRUE,
|
288
|
+
:NULL,
|
289
|
+
nil,
|
290
|
+
nil,
|
291
|
+
nil,
|
292
|
+
nil,
|
293
|
+
nil,
|
294
|
+
nil,
|
295
|
+
nil,
|
296
|
+
:QUERY,
|
297
|
+
nil,
|
298
|
+
nil,
|
299
|
+
:REPEATABLE,
|
300
|
+
:IMPLEMENTS,
|
301
|
+
:INPUT,
|
302
|
+
:TYPE,
|
303
|
+
:SCHEMA,
|
304
|
+
nil,
|
305
|
+
nil,
|
306
|
+
nil,
|
307
|
+
:DIRECTIVE,
|
308
|
+
:UNION,
|
309
|
+
nil,
|
310
|
+
nil,
|
311
|
+
:SCALAR,
|
312
|
+
nil,
|
313
|
+
:FRAGMENT]
|
314
|
+
|
315
|
+
def _hash key
|
316
|
+
(key * 18592990) >> 27 & 0x1f
|
317
|
+
end
|
245
318
|
end
|
246
319
|
end
|