tinygql 0.2.0 → 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/make_hash.rb +51 -0
- data/lib/tinygql/lexer.rb +45 -50
- data/lib/tinygql/nodes.rb +5 -5
- data/lib/tinygql/nodes.rb.erb +5 -5
- data/lib/tinygql/parser.rb +18 -25
- data/lib/tinygql/version.rb +1 -1
- data/test/lexer_test.rb +23 -0
- data/test/parser_test.rb +2 -2
- 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/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
@@ -75,8 +75,6 @@ module TinyGQL
|
|
75
75
|
o[Literals.const_get(n).ord] = n
|
76
76
|
}
|
77
77
|
|
78
|
-
LIT = Regexp.union(Literals.constants.map { |n| Literals.const_get(n) })
|
79
|
-
|
80
78
|
QUOTED_STRING = %r{#{QUOTE} ((?:#{STRING_CHAR})*) #{QUOTE}}x
|
81
79
|
BLOCK_STRING = %r{
|
82
80
|
#{BLOCK_QUOTE}
|
@@ -95,11 +93,10 @@ module TinyGQL
|
|
95
93
|
|
96
94
|
@string = string
|
97
95
|
@scan = StringScanner.new string
|
96
|
+
@start = nil
|
98
97
|
end
|
99
98
|
|
100
|
-
|
101
|
-
@scan.pos
|
102
|
-
end
|
99
|
+
attr_reader :start
|
103
100
|
|
104
101
|
def line
|
105
102
|
@scan.string[0, @scan.pos].count("\n") + 1
|
@@ -109,60 +106,27 @@ module TinyGQL
|
|
109
106
|
@scan.eos?
|
110
107
|
end
|
111
108
|
|
112
|
-
KW_LUT = [:FRAGMENT,
|
113
|
-
:INTERFACE,
|
114
|
-
:MUTATION,
|
115
|
-
:EXTEND,
|
116
|
-
:FALSE,
|
117
|
-
:ENUM,
|
118
|
-
:TRUE,
|
119
|
-
:NULL,
|
120
|
-
nil,
|
121
|
-
nil,
|
122
|
-
nil,
|
123
|
-
nil,
|
124
|
-
nil,
|
125
|
-
nil,
|
126
|
-
:QUERY,
|
127
|
-
nil,
|
128
|
-
nil,
|
129
|
-
nil,
|
130
|
-
:REPEATABLE,
|
131
|
-
:IMPLEMENTS,
|
132
|
-
:INPUT,
|
133
|
-
:TYPE,
|
134
|
-
:SCHEMA,
|
135
|
-
nil,
|
136
|
-
nil,
|
137
|
-
nil,
|
138
|
-
:DIRECTIVE,
|
139
|
-
:UNION,
|
140
|
-
nil,
|
141
|
-
nil,
|
142
|
-
:SCALAR]
|
143
|
-
|
144
109
|
def advance
|
145
110
|
@scan.skip(IGNORE)
|
146
111
|
|
112
|
+
@start = @scan.pos
|
113
|
+
|
147
114
|
case
|
148
115
|
when @scan.eos? then false
|
149
116
|
when @scan.skip(ELLIPSIS) then :ELLIPSIS
|
150
|
-
when tok = LIT_NAME_LUT[@string.getbyte(@
|
117
|
+
when tok = LIT_NAME_LUT[@string.getbyte(@start)] then
|
151
118
|
@scan.pos += 1
|
152
119
|
tok
|
153
|
-
when @scan.skip(KW_RE) then
|
154
|
-
len = @scan.matched_size
|
120
|
+
when len = @scan.skip(KW_RE) then
|
155
121
|
return :ON if len == 2
|
156
122
|
return :SUBSCRIPTION if len == 12
|
157
123
|
|
158
|
-
pos = @
|
124
|
+
pos = @start
|
159
125
|
|
160
|
-
#
|
161
|
-
key = (@string.getbyte(pos + 2) <<
|
162
|
-
(@string.getbyte(pos + 1) << 8) |
|
163
|
-
@string.getbyte(pos + 0)
|
126
|
+
# Second 2 bytes are unique, so we'll hash on those
|
127
|
+
key = (@string.getbyte(pos + 2) << 8) | @string.getbyte(pos + 1)
|
164
128
|
|
165
|
-
KW_LUT[
|
129
|
+
KW_LUT[_hash(key)]
|
166
130
|
when @scan.skip(IDENTIFIER) then :IDENTIFIER
|
167
131
|
when @scan.skip(BLOCK_STRING) then :STRING
|
168
132
|
when @scan.skip(QUOTED_STRING) then :STRING
|
@@ -315,10 +279,41 @@ module TinyGQL
|
|
315
279
|
lines.size > 1 ? lines.join("\n") : (lines.first || "".dup)
|
316
280
|
end
|
317
281
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
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
|
322
317
|
end
|
323
318
|
end
|
324
319
|
end
|
data/lib/tinygql/nodes.rb
CHANGED
@@ -3,16 +3,16 @@ module TinyGQL
|
|
3
3
|
class Node
|
4
4
|
include Enumerable
|
5
5
|
|
6
|
-
# `
|
7
|
-
attr_reader :
|
6
|
+
# `start` is the start position of this node in the source document
|
7
|
+
attr_reader :start
|
8
8
|
|
9
|
-
def initialize
|
10
|
-
@
|
9
|
+
def initialize start
|
10
|
+
@start = start
|
11
11
|
end
|
12
12
|
|
13
13
|
# Return the line of this node given `doc`
|
14
14
|
def line doc
|
15
|
-
doc[0, @
|
15
|
+
doc[0, @start].count("\n") + 1
|
16
16
|
end
|
17
17
|
|
18
18
|
def document?; false; end
|
data/lib/tinygql/nodes.rb.erb
CHANGED
@@ -3,16 +3,16 @@ module TinyGQL
|
|
3
3
|
class Node
|
4
4
|
include Enumerable
|
5
5
|
|
6
|
-
# `
|
7
|
-
attr_reader :
|
6
|
+
# `start` is the start position of this node in the source document
|
7
|
+
attr_reader :start
|
8
8
|
|
9
|
-
def initialize
|
10
|
-
@
|
9
|
+
def initialize start
|
10
|
+
@start = start
|
11
11
|
end
|
12
12
|
|
13
13
|
# Return the line of this node given `doc`
|
14
14
|
def line doc
|
15
|
-
doc[0, @
|
15
|
+
doc[0, @start].count("\n") + 1
|
16
16
|
end
|
17
17
|
|
18
18
|
<%- nodes.each do |node| -%>
|
data/lib/tinygql/parser.rb
CHANGED
@@ -13,7 +13,7 @@ module TinyGQL
|
|
13
13
|
|
14
14
|
def initialize doc
|
15
15
|
@lexer = Lexer.new doc
|
16
|
-
|
16
|
+
accept_token
|
17
17
|
end
|
18
18
|
|
19
19
|
def parse
|
@@ -25,7 +25,7 @@ module TinyGQL
|
|
25
25
|
attr_reader :token_name
|
26
26
|
|
27
27
|
def pos
|
28
|
-
@lexer.
|
28
|
+
@lexer.start
|
29
29
|
end
|
30
30
|
|
31
31
|
def document
|
@@ -477,7 +477,17 @@ module TinyGQL
|
|
477
477
|
end
|
478
478
|
|
479
479
|
def operation_type
|
480
|
-
|
480
|
+
val = if at?(:QUERY)
|
481
|
+
"query"
|
482
|
+
elsif at?(:MUTATION)
|
483
|
+
"mutation"
|
484
|
+
elsif at?(:SUBSCRIPTION)
|
485
|
+
"subscription"
|
486
|
+
else
|
487
|
+
expect_token(:QUERY)
|
488
|
+
end
|
489
|
+
accept_token
|
490
|
+
val
|
481
491
|
end
|
482
492
|
|
483
493
|
def directives
|
@@ -528,7 +538,7 @@ module TinyGQL
|
|
528
538
|
|
529
539
|
def variable_definition
|
530
540
|
loc = pos
|
531
|
-
var = variable
|
541
|
+
var = variable if at?(:VAR_SIGN)
|
532
542
|
expect_token(:COLON)
|
533
543
|
type = self.type
|
534
544
|
default_value = if at?(:EQUALS)
|
@@ -608,7 +618,7 @@ module TinyGQL
|
|
608
618
|
accept_token
|
609
619
|
Nodes::BooleanValue.new(pos, "false")
|
610
620
|
else
|
611
|
-
|
621
|
+
expect_token(:TRUE)
|
612
622
|
end
|
613
623
|
end
|
614
624
|
|
@@ -643,15 +653,14 @@ module TinyGQL
|
|
643
653
|
end
|
644
654
|
|
645
655
|
def variable
|
646
|
-
return unless at?(:VAR_SIGN)
|
647
656
|
loc = pos
|
648
|
-
|
657
|
+
expect_token(:VAR_SIGN)
|
649
658
|
Nodes::Variable.new loc, name
|
650
659
|
end
|
651
660
|
|
652
661
|
def name
|
653
662
|
case token_name
|
654
|
-
when :IDENTIFIER then
|
663
|
+
when :IDENTIFIER then expect_token_value(:IDENTIFIER)
|
655
664
|
when :TYPE then
|
656
665
|
accept_token
|
657
666
|
"type"
|
@@ -662,7 +671,7 @@ module TinyGQL
|
|
662
671
|
accept_token
|
663
672
|
"input"
|
664
673
|
else
|
665
|
-
|
674
|
+
expect_token(:IDENTIFIER)
|
666
675
|
end
|
667
676
|
end
|
668
677
|
|
@@ -670,13 +679,6 @@ module TinyGQL
|
|
670
679
|
@token_name = @lexer.advance
|
671
680
|
end
|
672
681
|
|
673
|
-
# Only use when we care about the accepted token's value
|
674
|
-
def accept_token_value
|
675
|
-
token_value = @lexer.token_value
|
676
|
-
accept_token
|
677
|
-
token_value
|
678
|
-
end
|
679
|
-
|
680
682
|
def expect_token tok
|
681
683
|
unless at?(tok)
|
682
684
|
raise UnexpectedToken, "Expected token #{tok}, actual: #{token_name} #{@lexer.token_value} line: #{@lexer.line}"
|
@@ -697,15 +699,6 @@ module TinyGQL
|
|
697
699
|
token_value
|
698
700
|
end
|
699
701
|
|
700
|
-
def expect_tokens toks
|
701
|
-
token_value = @lexer.token_value
|
702
|
-
unless toks.any? { |tok| at?(tok) }
|
703
|
-
raise UnexpectedToken, "Expected token #{tok}, actual: #{token_name}"
|
704
|
-
end
|
705
|
-
accept_token
|
706
|
-
token_value
|
707
|
-
end
|
708
|
-
|
709
702
|
def at? tok
|
710
703
|
token_name == tok
|
711
704
|
end
|
data/lib/tinygql/version.rb
CHANGED
data/test/lexer_test.rb
CHANGED
@@ -179,5 +179,28 @@ eod
|
|
179
179
|
|
180
180
|
assert_equal words.map { |x| [x.upcase.to_sym, x] }, toks
|
181
181
|
end
|
182
|
+
|
183
|
+
def test_looks_like_kw
|
184
|
+
words = ["fragment", "fragments"]
|
185
|
+
doc = words.join(" ")
|
186
|
+
|
187
|
+
lexer = Lexer.new doc
|
188
|
+
toks = []
|
189
|
+
while tok = lexer.advance
|
190
|
+
toks << tok
|
191
|
+
end
|
192
|
+
|
193
|
+
assert_equal [:FRAGMENT, :IDENTIFIER], toks
|
194
|
+
end
|
195
|
+
|
196
|
+
def test_num_with_dots
|
197
|
+
lexer = Lexer.new "1...2"
|
198
|
+
toks = []
|
199
|
+
while tok = lexer.advance
|
200
|
+
toks << tok
|
201
|
+
end
|
202
|
+
|
203
|
+
assert_equal [:INT, :ELLIPSIS, :INT], toks
|
204
|
+
end
|
182
205
|
end
|
183
206
|
end
|
data/test/parser_test.rb
CHANGED
@@ -58,8 +58,8 @@ mutation {
|
|
58
58
|
eod
|
59
59
|
parser = Parser.new doc
|
60
60
|
ast = parser.parse
|
61
|
-
expected = ["likeStory", "story", "likeCount"].map { |str| doc.index(str)
|
62
|
-
assert_equal expected, ast.find_all(&:field?).map(&:
|
61
|
+
expected = ["likeStory", "story", "likeCount"].map { |str| doc.index(str) }
|
62
|
+
assert_equal expected, ast.find_all(&:field?).map(&:start)
|
63
63
|
assert_equal [2, 3, 4], ast.find_all(&:field?).map { |n| n.line(doc) }
|
64
64
|
end
|
65
65
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tinygql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Patterson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-08-
|
11
|
+
date: 2023-08-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -53,6 +53,7 @@ files:
|
|
53
53
|
- Rakefile
|
54
54
|
- benchmark/fixtures/negotiate.gql
|
55
55
|
- bin/bench.rb
|
56
|
+
- bin/make_hash.rb
|
56
57
|
- bin/profile.rb
|
57
58
|
- lib/tinygql.rb
|
58
59
|
- lib/tinygql/lexer.rb
|