tinygql 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 114da94b6827645fb92f0f3e85b9e4f34b64aaee8449c143217cc75fc903ab3c
4
- data.tar.gz: 27031f9423d30f0453a6737be1184cb8c724a6290bc432b7e35dea255fa549a5
3
+ metadata.gz: e6f2ac9d1c4531277689c6eec21fd29cd1e6fe9715284813b57c741e3e9bace2
4
+ data.tar.gz: b9cd18ac29f1aeea347296f0408deadddec7a902d25495e3232b472180718695
5
5
  SHA512:
6
- metadata.gz: a4362f8d3c3d638c715c4686ec9714d59f59d411be390cf115d0b7e8bc4f710fcbfe935ca058092f24935964fd29905643e6f1f4edeb84e238e1af0d5d2eff4e
7
- data.tar.gz: 5ec4f82ff306221b2c8e008b3bb76feb1c55917ab26334e1531f7eb8e58f03e44a29901fe92765585cfa195c41a107fdd13337b3bb844f13bb9985f1394f8ee0
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
- def pos
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(@scan.pos)] then
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 = @scan.pos - len
124
+ pos = @start
159
125
 
160
- # First 3 bytes are unique, so we'll hash on those
161
- key = (@string.getbyte(pos + 2) << 16) |
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[hash(key)]
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
- def hash key
319
- # Constant came from perfect hash generation
320
- m = key * 145291
321
- (m >> 28) & 0x1f
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
- # `pos` is the position of the _end_ of a token
7
- attr_reader :pos
6
+ # `start` is the start position of this node in the source document
7
+ attr_reader :start
8
8
 
9
- def initialize pos
10
- @pos = pos
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, @pos].count("\n") + 1
15
+ doc[0, @start].count("\n") + 1
16
16
  end
17
17
 
18
18
  def document?; false; end
@@ -3,16 +3,16 @@ module TinyGQL
3
3
  class Node
4
4
  include Enumerable
5
5
 
6
- # `pos` is the position of the _end_ of a token
7
- attr_reader :pos
6
+ # `start` is the start position of this node in the source document
7
+ attr_reader :start
8
8
 
9
- def initialize pos
10
- @pos = pos
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, @pos].count("\n") + 1
15
+ doc[0, @start].count("\n") + 1
16
16
  end
17
17
 
18
18
  <%- nodes.each do |node| -%>
@@ -13,7 +13,7 @@ module TinyGQL
13
13
 
14
14
  def initialize doc
15
15
  @lexer = Lexer.new doc
16
- @token_name = @lexer.advance
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.pos
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
- expect_tokens([:QUERY, :MUTATION, :SUBSCRIPTION])
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
- expect_tokens([:TRUE, :FALSE])
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
- accept_token
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 accept_token_value
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
- expect_token_value(:IDENTIFIER)
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
@@ -1,3 +1,3 @@
1
1
  module TinyGQL
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
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) + str.bytesize }
62
- assert_equal expected, ast.find_all(&:field?).map(&:pos)
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.2.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-28 00:00:00.000000000 Z
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