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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef7a07443c070f78f1ad6324890611e1ea70298c7d67eac591911c208b42aa0f
4
- data.tar.gz: 3311b13953c075059a737b46f82f50aa3fd2428d844499c64354a285aa2774cd
3
+ metadata.gz: e6f2ac9d1c4531277689c6eec21fd29cd1e6fe9715284813b57c741e3e9bace2
4
+ data.tar.gz: b9cd18ac29f1aeea347296f0408deadddec7a902d25495e3232b472180718695
5
5
  SHA512:
6
- metadata.gz: 814659e285e05236a4861931cf548eabab4abeaccdbc5b959adc12e3a3e6f6b9a67ea5626117ae44f46a9316c8d3210adafbf269730aec23b8c9d5ac0a538832
7
- data.tar.gz: 3603440c4c6aa2aed7c361439299b8218b2f01a2c5ea96ed100da5028ac6fe391d62322a4431124861db7de50c60c69fd50d3da92178c12ef04dd58fbe63e2d3
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" => :ON,
21
- "fragment" => :FRAGMENT,
22
- "true" => :TRUE,
23
- "false" => :FALSE,
24
- "null" => :NULL,
25
- "query" => :QUERY,
26
- "mutation" => :MUTATION,
27
- "subscription" => :SUBSCRIPTION,
28
- "schema" => :SCHEMA,
29
- "scalar" => :SCALAR,
30
- "type" => :TYPE,
31
- "extend" => :EXTEND,
32
- "implements" => :IMPLEMENTS,
33
- "interface" => :INTERFACE,
34
- "union" => :UNION,
35
- "enum" => :ENUM,
36
- "input" => :INPUT,
37
- "directive" => :DIRECTIVE,
38
- "repeatable" => :REPEATABLE
39
- }.freeze
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({}) { |n, o|
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
- @token_name = nil
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 str = @scan.scan(LIT) then return emit(LIT_NAME_LUT[str], str)
111
- when str = @scan.scan(IDENTIFIER) then return emit(KEYWORDS.fetch(str, :IDENTIFIER), str)
112
- when @scan.skip(BLOCK_STRING) then return emit_block(@scan[1])
113
- when @scan.skip(QUOTED_STRING) then return emit_string(@scan[1])
114
- when str = @scan.scan(NUMERIC) then return emit(@scan[1] ? :FLOAT : :INT, str)
115
- when @scan.eos? then emit(nil, nil) and return false
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
- emit(:UNKNOWN_CHAR, @scan.getch)
135
+ @scan.getch
136
+ :UNKNOWN_CHAR
118
137
  end
119
138
  end
120
139
 
121
- attr_reader :token_name, :token_value
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
- def emit token_name, token_value
124
- @token_name = token_name
125
- @token_value = token_value
126
- true
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
- advance && [@token_name, @token_value]
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
- emit(:STRING, value)
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