jmespath 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of jmespath might be problematic. Click here for more details.

@@ -0,0 +1,6 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ Index = Field
5
+ end
6
+ end
@@ -0,0 +1,16 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Literal < Node
5
+ attr_reader :value
6
+
7
+ def initialize(value)
8
+ @value = value
9
+ end
10
+
11
+ def visit(value)
12
+ @value
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class MultiSelectHash < Node
5
+ def initialize(kv_pairs)
6
+ @kv_pairs = kv_pairs
7
+ end
8
+
9
+ def visit(value)
10
+ if value.nil?
11
+ nil
12
+ else
13
+ @kv_pairs.each_with_object({}) do |pair, hash|
14
+ hash[pair.key] = pair.value.visit(value)
15
+ end
16
+ end
17
+ end
18
+
19
+ def optimize
20
+ self.class.new(@kv_pairs.map(&:optimize))
21
+ end
22
+
23
+ class KeyValuePair
24
+ attr_reader :key, :value
25
+
26
+ def initialize(key, value)
27
+ @key = key
28
+ @value = value
29
+ end
30
+
31
+ def optimize
32
+ self.class.new(@key, @value.optimize)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class MultiSelectList < Node
5
+ def initialize(children)
6
+ @children = children
7
+ end
8
+
9
+ def visit(value)
10
+ if value.nil?
11
+ value
12
+ else
13
+ @children.map { |n| n.visit(value) }
14
+ end
15
+ end
16
+
17
+ def optimize
18
+ self.class.new(@children.map(&:optimize))
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Or < Node
5
+ def initialize(left, right)
6
+ @left = left
7
+ @right = right
8
+ end
9
+
10
+ def visit(value)
11
+ result = @left.visit(value)
12
+ if result == false || result.nil? || result.empty?
13
+ @right.visit(value)
14
+ else
15
+ result
16
+ end
17
+ end
18
+
19
+ def optimize
20
+ self.class.new(@left.optimize, @right.optimize)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,6 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ Pipe = Subexpression
5
+ end
6
+ end
@@ -0,0 +1,82 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Projection < Node
5
+ def initialize(target, projection)
6
+ @target = target
7
+ @projection = projection
8
+ end
9
+
10
+ def visit(value)
11
+ if (targets = extract_targets(@target.visit(value)))
12
+ list = []
13
+ targets.each do |v|
14
+ vv = @projection.visit(v)
15
+ unless vv.nil?
16
+ list << vv
17
+ end
18
+ end
19
+ list
20
+ end
21
+ end
22
+
23
+ def optimize
24
+ if @projection.is_a?(Current)
25
+ fast_instance
26
+ else
27
+ self.class.new(@target.optimize, @projection.optimize)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def extract_targets(left_value)
34
+ nil
35
+ end
36
+ end
37
+
38
+ module FastProjector
39
+ def visit(value)
40
+ if (targets = extract_targets(@target.visit(value)))
41
+ targets.compact
42
+ end
43
+ end
44
+ end
45
+
46
+ class ArrayProjection < Projection
47
+ def extract_targets(target)
48
+ if Array === target
49
+ target
50
+ else
51
+ nil
52
+ end
53
+ end
54
+
55
+ def fast_instance
56
+ FastArrayProjection.new(@target.optimize, @projection.optimize)
57
+ end
58
+ end
59
+
60
+ class FastArrayProjection < ArrayProjection
61
+ include FastProjector
62
+ end
63
+
64
+ class ObjectProjection < Projection
65
+ def extract_targets(target)
66
+ if hash_like?(target)
67
+ target.values
68
+ else
69
+ nil
70
+ end
71
+ end
72
+
73
+ def fast_instance
74
+ FastObjectProjection.new(@target.optimize, @projection.optimize)
75
+ end
76
+ end
77
+
78
+ class FastObjectProjection < ObjectProjection
79
+ include FastProjector
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,92 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Slice < Node
5
+ def initialize(start, stop, step)
6
+ @start = start
7
+ @stop = stop
8
+ @step = step
9
+ end
10
+
11
+ def visit(value)
12
+ if String === value || Array === value
13
+ start, stop, step = adjust_slice(value.size, @start, @stop, @step)
14
+ result = []
15
+ if step > 0
16
+ i = start
17
+ while i < stop
18
+ result << value[i]
19
+ i += step
20
+ end
21
+ else
22
+ i = start
23
+ while i > stop
24
+ result << value[i]
25
+ i += step
26
+ end
27
+ end
28
+ String === value ? result.join : result
29
+ else
30
+ nil
31
+ end
32
+ end
33
+
34
+ def optimize
35
+ if (@step.nil? || @step == 1) && @start && @stop && @start > 0 && @stop > @start
36
+ SimpleSlice.new(@start, @stop)
37
+ else
38
+ self
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def adjust_slice(length, start, stop, step)
45
+ if step.nil?
46
+ step = 1
47
+ elsif step == 0
48
+ raise Errors::InvalidValueError, 'slice step cannot be 0'
49
+ end
50
+
51
+ if start.nil?
52
+ start = step < 0 ? length - 1 : 0
53
+ else
54
+ start = adjust_endpoint(length, start, step)
55
+ end
56
+
57
+ if stop.nil?
58
+ stop = step < 0 ? -1 : length
59
+ else
60
+ stop = adjust_endpoint(length, stop, step)
61
+ end
62
+ [start, stop, step]
63
+ end
64
+
65
+ def adjust_endpoint(length, endpoint, step)
66
+ if endpoint < 0
67
+ endpoint += length
68
+ endpoint = step < 0 ? -1 : 0 if endpoint < 0
69
+ endpoint
70
+ elsif endpoint >= length
71
+ step < 0 ? length - 1 : length
72
+ else
73
+ endpoint
74
+ end
75
+ end
76
+ end
77
+
78
+ class SimpleSlice < Slice
79
+ def initialize(start, stop)
80
+ super(start, stop, 1)
81
+ end
82
+
83
+ def visit(value)
84
+ if String === value || Array === value
85
+ value[@start, @stop - @start]
86
+ else
87
+ nil
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,63 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Subexpression < Node
5
+ def initialize(left, right)
6
+ @left = left
7
+ @right = right
8
+ end
9
+
10
+ def visit(value)
11
+ @right.visit(@left.visit(value))
12
+ end
13
+
14
+ def optimize
15
+ Chain.new(flatten).optimize
16
+ end
17
+
18
+ protected
19
+
20
+ attr_reader :left, :right
21
+
22
+ def flatten
23
+ nodes = [@left, @right]
24
+ until nodes.none? { |node| node.is_a?(Subexpression) }
25
+ nodes = nodes.flat_map do |node|
26
+ if node.is_a?(Subexpression)
27
+ [node.left, node.right]
28
+ else
29
+ [node]
30
+ end
31
+ end
32
+ end
33
+ nodes.map(&:optimize)
34
+ end
35
+ end
36
+
37
+ class Chain
38
+ def initialize(children)
39
+ @children = children
40
+ end
41
+
42
+ def visit(value)
43
+ @children.reduce(value) do |v, child|
44
+ child.visit(v)
45
+ end
46
+ end
47
+
48
+ def optimize
49
+ children = @children.map(&:optimize)
50
+ index = 0
51
+ while index < children.size - 1
52
+ if children[index].chains_with?(children[index + 1])
53
+ children[index] = children[index].chain(children[index + 1])
54
+ children.delete_at(index + 1)
55
+ else
56
+ index += 1
57
+ end
58
+ end
59
+ Chain.new(children)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -4,29 +4,39 @@ module JMESPath
4
4
  # @api private
5
5
  class Parser
6
6
 
7
- # @api private
8
7
  AFTER_DOT = Set.new([
9
- :identifier, # foo.bar
10
- :quoted_identifier, # foo."bar"
11
- :star, # foo.*
12
- :lbrace, # foo[1]
13
- :lbracket, # foo{a: 0}
14
- :function, # foo.*.to_string(@)
15
- :filter, # foo.[?bar==10]
8
+ Lexer::T_IDENTIFIER, # foo.bar
9
+ Lexer::T_QUOTED_IDENTIFIER, # foo."bar"
10
+ Lexer::T_STAR, # foo.*
11
+ Lexer::T_LBRACE, # foo{a: 0}
12
+ Lexer::T_LBRACKET, # foo[1]
13
+ Lexer::T_FILTER, # foo.[?bar==10]
14
+ ])
15
+
16
+ NUM_COLON_RBRACKET = Set.new([
17
+ Lexer::T_NUMBER,
18
+ Lexer::T_COLON,
19
+ Lexer::T_RBRACKET,
16
20
  ])
17
21
 
18
- CURRENT_NODE = { type: :current }
22
+ COLON_RBRACKET = Set.new([
23
+ Lexer::T_COLON,
24
+ Lexer::T_RBRACKET,
25
+ ])
26
+
27
+ CURRENT_NODE = Nodes::Current.new
19
28
 
20
29
  # @option options [Lexer] :lexer
21
30
  def initialize(options = {})
22
- @lexer = options[:lexer] || Lexer.new()
31
+ @lexer = options[:lexer] || Lexer.new
23
32
  end
24
33
 
25
34
  # @param [String<JMESPath>] expression
26
35
  def parse(expression)
27
- stream = TokenStream.new(expression, @lexer.tokenize(expression))
36
+ tokens = @lexer.tokenize(expression)
37
+ stream = TokenStream.new(expression, tokens)
28
38
  result = expr(stream)
29
- if stream.token.type != :eof
39
+ if stream.token.type != Lexer::T_EOF
30
40
  raise Errors::SyntaxError, "expected :eof got #{stream.token.type}"
31
41
  else
32
42
  result
@@ -61,10 +71,7 @@ module JMESPath
61
71
 
62
72
  def nud_expref(stream)
63
73
  stream.next
64
- {
65
- type: :expression,
66
- children: [expr(stream, 2)]
67
- }
74
+ Nodes::Expression.new(expr(stream, 2))
68
75
  end
69
76
 
70
77
  def nud_filter(stream)
@@ -77,8 +84,12 @@ module JMESPath
77
84
 
78
85
  def nud_identifier(stream)
79
86
  token = stream.token
80
- stream.next
81
- { type: :field, key: token.value }
87
+ n = stream.next
88
+ if n.type == :lparen
89
+ Nodes::Function::FunctionName.new(token.value)
90
+ else
91
+ Nodes::Field.new(token.value)
92
+ end
82
93
  end
83
94
 
84
95
  def nud_lbrace(stream)
@@ -92,10 +103,7 @@ module JMESPath
92
103
  end
93
104
  end while stream.token.type != :rbrace
94
105
  stream.next
95
- {
96
- type: :multi_select_hash,
97
- children: pairs
98
- }
106
+ Nodes::MultiSelectHash.new(pairs)
99
107
  end
100
108
 
101
109
  def nud_lbracket(stream)
@@ -113,10 +121,7 @@ module JMESPath
113
121
  def nud_literal(stream)
114
122
  value = stream.token.value
115
123
  stream.next
116
- {
117
- type: :literal,
118
- value: value
119
- }
124
+ Nodes::Literal.new(value)
120
125
  end
121
126
 
122
127
  def nud_quoted_identifier(stream)
@@ -126,7 +131,7 @@ module JMESPath
126
131
  msg = 'quoted identifiers are not allowed for function names'
127
132
  raise Errors::SyntaxError, msg
128
133
  else
129
- { type: :field, key: token[:value] }
134
+ Nodes::Field.new(token[:value])
130
135
  end
131
136
  end
132
137
 
@@ -137,14 +142,8 @@ module JMESPath
137
142
  def led_comparator(stream, left)
138
143
  token = stream.token
139
144
  stream.next
140
- {
141
- type: :comparator,
142
- relation: token.value,
143
- children: [
144
- left,
145
- expr(stream),
146
- ]
147
- }
145
+ right = expr(stream)
146
+ Nodes::Comparator.create(token.value, left, right)
148
147
  end
149
148
 
150
149
  def led_dot(stream, left)
@@ -152,60 +151,37 @@ module JMESPath
152
151
  if stream.token.type == :star
153
152
  parse_wildcard_object(stream, left)
154
153
  else
155
- {
156
- type: :subexpression,
157
- children: [
158
- left,
159
- parse_dot(stream, Token::BINDING_POWER[:dot])
160
- ]
161
- }
154
+ right = parse_dot(stream, Token::BINDING_POWER[:dot])
155
+ Nodes::Subexpression.new(left, right)
162
156
  end
163
157
  end
164
158
 
165
159
  def led_filter(stream, left)
166
160
  stream.next
167
161
  expression = expr(stream)
168
- if stream.token.type != :rbracket
162
+ if stream.token.type != Lexer::T_RBRACKET
169
163
  raise Errors::SyntaxError, 'expected a closing rbracket for the filter'
170
164
  end
171
165
  stream.next
172
- rhs = parse_projection(stream, Token::BINDING_POWER[:filter])
173
- {
174
- type: :projection,
175
- from: :array,
176
- children: [
177
- left ? left : CURRENT_NODE,
178
- {
179
- type: :condition,
180
- children: [expression, rhs],
181
- }
182
- ]
183
- }
166
+ rhs = parse_projection(stream, Token::BINDING_POWER[Lexer::T_FILTER])
167
+ left ||= CURRENT_NODE
168
+ right = Nodes::Condition.new(expression, rhs)
169
+ Nodes::ArrayProjection.new(left, right)
184
170
  end
185
171
 
186
172
  def led_flatten(stream, left)
187
173
  stream.next
188
- {
189
- type: :projection,
190
- from: :array,
191
- children: [
192
- { type: :flatten, children: [left] },
193
- parse_projection(stream, Token::BINDING_POWER[:flatten])
194
- ]
195
- }
174
+ left = Nodes::Flatten.new(left)
175
+ right = parse_projection(stream, Token::BINDING_POWER[:flatten])
176
+ Nodes::ArrayProjection.new(left, right)
196
177
  end
197
178
 
198
179
  def led_lbracket(stream, left)
199
180
  stream.next(match: Set.new([:number, :colon, :star]))
200
181
  type = stream.token.type
201
182
  if type == :number || type == :colon
202
- {
203
- type: :subexpression,
204
- children: [
205
- left,
206
- parse_array_index_expression(stream)
207
- ]
208
- }
183
+ right = parse_array_index_expression(stream)
184
+ Nodes::Subexpression.new(left, right)
209
185
  else
210
186
  parse_wildcard_array(stream, left)
211
187
  end
@@ -213,7 +189,7 @@ module JMESPath
213
189
 
214
190
  def led_lparen(stream, left)
215
191
  args = []
216
- name = left[:key]
192
+ name = left.name
217
193
  stream.next
218
194
  while stream.token.type != :rparen
219
195
  args << expr(stream, 0)
@@ -222,47 +198,50 @@ module JMESPath
222
198
  end
223
199
  end
224
200
  stream.next
225
- {
226
- type: :function,
227
- fn: name,
228
- children: args,
229
- }
201
+ Nodes::Function.create(name, args)
230
202
  end
231
203
 
232
204
  def led_or(stream, left)
233
205
  stream.next
234
- {
235
- type: :or,
236
- children: [left, expr(stream, Token::BINDING_POWER[:or])]
237
- }
206
+ right = expr(stream, Token::BINDING_POWER[:or])
207
+ Nodes::Or.new(left, right)
238
208
  end
239
209
 
240
210
  def led_pipe(stream, left)
241
211
  stream.next
242
- {
243
- type: :pipe,
244
- children: [left, expr(stream, Token::BINDING_POWER[:pipe])],
245
- }
212
+ right = expr(stream, Token::BINDING_POWER[:pipe])
213
+ Nodes::Pipe.new(left, right)
246
214
  end
247
215
 
216
+ # parse array index expressions, for example [0], [1:2:3], etc.
248
217
  def parse_array_index_expression(stream)
249
218
  pos = 0
250
219
  parts = [nil, nil, nil]
220
+ expected = NUM_COLON_RBRACKET
221
+
251
222
  begin
252
- if stream.token.type == :colon
223
+ if stream.token.type == Lexer::T_COLON
253
224
  pos += 1
254
- else
225
+ expected = NUM_COLON_RBRACKET
226
+ elsif stream.token.type == Lexer::T_NUMBER
255
227
  parts[pos] = stream.token.value
228
+ expected = COLON_RBRACKET
256
229
  end
257
- stream.next(match:Set.new([:number, :colon, :rbracket]))
258
- end while stream.token.type != :rbracket
259
- stream.next
230
+ stream.next(match: expected)
231
+ end while stream.token.type != Lexer::T_RBRACKET
232
+
233
+ stream.next # consume the closing bracket
234
+
260
235
  if pos == 0
261
- { type: :index, index: parts[0] }
236
+ # no colons found, this is a single index extraction
237
+ Nodes::Index.new(parts[0])
262
238
  elsif pos > 2
263
239
  raise Errors::SyntaxError, 'invalid array slice syntax: too many colons'
264
240
  else
265
- { type: :slice, args: parts }
241
+ Nodes::ArrayProjection.new(
242
+ Nodes::Slice.new(*parts),
243
+ parse_projection(stream, Token::BINDING_POWER[Lexer::T_STAR])
244
+ )
266
245
  end
267
246
  end
268
247
 
@@ -279,11 +258,7 @@ module JMESPath
279
258
  key = stream.token.value
280
259
  stream.next(match:Set.new([:colon]))
281
260
  stream.next
282
- {
283
- type: :key_value_pair,
284
- key: key,
285
- children: [expr(stream)]
286
- }
261
+ Nodes::MultiSelectHash::KeyValuePair.new(key, expr(stream))
287
262
  end
288
263
 
289
264
  def parse_multi_select_list(stream)
@@ -298,10 +273,7 @@ module JMESPath
298
273
  end
299
274
  end while stream.token.type != :rbracket
300
275
  stream.next
301
- {
302
- type: :multi_select_list,
303
- children: nodes
304
- }
276
+ Nodes::MultiSelectList.new(nodes)
305
277
  end
306
278
 
307
279
  def parse_projection(stream, binding_power)
@@ -321,26 +293,16 @@ module JMESPath
321
293
  def parse_wildcard_array(stream, left = nil)
322
294
  stream.next(match:Set.new([:rbracket]))
323
295
  stream.next
324
- {
325
- type: :projection,
326
- from: :array,
327
- children: [
328
- left ? left : CURRENT_NODE,
329
- parse_projection(stream, Token::BINDING_POWER[:star])
330
- ]
331
- }
296
+ left ||= CURRENT_NODE
297
+ right = parse_projection(stream, Token::BINDING_POWER[:star])
298
+ Nodes::ArrayProjection.new(left, right)
332
299
  end
333
300
 
334
301
  def parse_wildcard_object(stream, left = nil)
335
302
  stream.next
336
- {
337
- type: :projection,
338
- from: :object,
339
- children: [
340
- left ? left : CURRENT_NODE,
341
- parse_projection(stream, Token::BINDING_POWER[:star])
342
- ]
343
- }
303
+ left ||= CURRENT_NODE
304
+ right = parse_projection(stream, Token::BINDING_POWER[:star])
305
+ Nodes::ObjectProjection.new(left, right)
344
306
  end
345
307
 
346
308
  end