tinygql 0.1.4 → 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: 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