burtpath 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,93 @@
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::RuntimeError, '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
+
63
+ [start, stop, step]
64
+ end
65
+
66
+ def adjust_endpoint(length, endpoint, step)
67
+ if endpoint < 0
68
+ endpoint += length
69
+ endpoint = 0 if endpoint < 0
70
+ endpoint
71
+ elsif endpoint >= length
72
+ step < 0 ? length - 1 : length
73
+ else
74
+ endpoint
75
+ end
76
+ end
77
+ end
78
+
79
+ class SimpleSlice < Slice
80
+ def initialize(start, stop)
81
+ super(start, stop, 1)
82
+ end
83
+
84
+ def visit(value)
85
+ if String === value || Array === value
86
+ value[@start, @stop - @start]
87
+ else
88
+ nil
89
+ end
90
+ end
91
+ end
92
+ end
93
+ 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
@@ -0,0 +1,40 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Node
5
+ def visit(value)
6
+ end
7
+
8
+ def hash_like?(value)
9
+ Hash === value || Struct === value
10
+ end
11
+
12
+ def optimize
13
+ self
14
+ end
15
+
16
+ def chains_with?(other)
17
+ false
18
+ end
19
+ end
20
+
21
+ autoload :Comparator, 'jmespath/nodes/comparator'
22
+ autoload :Condition, 'jmespath/nodes/condition'
23
+ autoload :Current, 'jmespath/nodes/current'
24
+ autoload :Expression, 'jmespath/nodes/expression'
25
+ autoload :Field, 'jmespath/nodes/field'
26
+ autoload :Flatten, 'jmespath/nodes/flatten'
27
+ autoload :Function, 'jmespath/nodes/function'
28
+ autoload :Index, 'jmespath/nodes/index'
29
+ autoload :Literal, 'jmespath/nodes/literal'
30
+ autoload :MultiSelectHash, 'jmespath/nodes/multi_select_hash'
31
+ autoload :MultiSelectList, 'jmespath/nodes/multi_select_list'
32
+ autoload :Or, 'jmespath/nodes/or'
33
+ autoload :Pipe, 'jmespath/nodes/pipe'
34
+ autoload :Projection, 'jmespath/nodes/projection'
35
+ autoload :ArrayProjection, 'jmespath/nodes/projection'
36
+ autoload :ObjectProjection, 'jmespath/nodes/projection'
37
+ autoload :Slice, 'jmespath/nodes/slice'
38
+ autoload :Subexpression, 'jmespath/nodes/subexpression'
39
+ end
40
+ end
@@ -0,0 +1,12 @@
1
+ module JMESPath
2
+ class OptimizingParser
3
+
4
+ def initialize(parser)
5
+ @parser = parser
6
+ end
7
+
8
+ def parse(expression)
9
+ @parser.parse(expression).optimize
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,288 @@
1
+ require 'set'
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Parser
6
+
7
+ # @api private
8
+ 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]
16
+ ])
17
+
18
+ CURRENT_NODE = Nodes::Current.new
19
+
20
+ # @option options [Lexer] :lexer
21
+ def initialize(options = {})
22
+ @lexer = options[:lexer] || Lexer.new()
23
+ end
24
+
25
+ # @param [String<JMESPath>] expression
26
+ def parse(expression)
27
+ stream = TokenStream.new(expression, @lexer.tokenize(expression))
28
+ result = expr(stream)
29
+ if stream.token.type != :eof
30
+ raise Errors::SyntaxError, "expected :eof got #{stream.token.type}"
31
+ else
32
+ result
33
+ end
34
+ end
35
+
36
+ # @api private
37
+ def method_missing(method_name, *args)
38
+ if matches = method_name.match(/^(nud_|led_)(.*)/)
39
+ raise Errors::SyntaxError, "unexpected token #{matches[2]}"
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # @param [TokenStream] stream
48
+ # @param [Integer] rbp Right binding power
49
+ def expr(stream, rbp = 0)
50
+ left = send("nud_#{stream.token.type}", stream)
51
+ while rbp < stream.token.binding_power
52
+ left = send("led_#{stream.token.type}", stream, left)
53
+ end
54
+ left
55
+ end
56
+
57
+ def nud_current(stream)
58
+ stream.next
59
+ CURRENT_NODE
60
+ end
61
+
62
+ def nud_expref(stream)
63
+ stream.next
64
+ Nodes::Expression.new(expr(stream, 2))
65
+ end
66
+
67
+ def nud_filter(stream)
68
+ led_filter(stream, CURRENT_NODE)
69
+ end
70
+
71
+ def nud_flatten(stream)
72
+ led_flatten(stream, CURRENT_NODE)
73
+ end
74
+
75
+ def nud_identifier(stream)
76
+ token = stream.token
77
+ n = stream.next
78
+ if n.type == :lparen
79
+ Nodes::Function::FunctionName.new(token.value)
80
+ else
81
+ Nodes::Field.new(token.value)
82
+ end
83
+ end
84
+
85
+ def nud_lbrace(stream)
86
+ valid_keys = Set.new([:quoted_identifier, :identifier])
87
+ stream.next(match:valid_keys)
88
+ pairs = []
89
+ begin
90
+ pairs << parse_key_value_pair(stream)
91
+ if stream.token.type == :comma
92
+ stream.next(match:valid_keys)
93
+ end
94
+ end while stream.token.type != :rbrace
95
+ stream.next
96
+ Nodes::MultiSelectHash.new(pairs)
97
+ end
98
+
99
+ def nud_lbracket(stream)
100
+ stream.next
101
+ type = stream.token.type
102
+ if type == :number || type == :colon
103
+ parse_array_index_expression(stream)
104
+ elsif type == :star && stream.lookahead(1).type == :rbracket
105
+ parse_wildcard_array(stream)
106
+ else
107
+ parse_multi_select_list(stream)
108
+ end
109
+ end
110
+
111
+ def nud_literal(stream)
112
+ value = stream.token.value
113
+ stream.next
114
+ Nodes::Literal.new(value)
115
+ end
116
+
117
+ def nud_quoted_identifier(stream)
118
+ token = stream.token
119
+ next_token = stream.next
120
+ if next_token.type == :lparen
121
+ msg = 'quoted identifiers are not allowed for function names'
122
+ raise Errors::SyntaxError, msg
123
+ else
124
+ Nodes::Field.new(token[:value])
125
+ end
126
+ end
127
+
128
+ def nud_star(stream)
129
+ parse_wildcard_object(stream, CURRENT_NODE)
130
+ end
131
+
132
+ def led_comparator(stream, left)
133
+ token = stream.token
134
+ stream.next
135
+ right = expr(stream)
136
+ Nodes::Comparator.create(token.value, left, right)
137
+ end
138
+
139
+ def led_dot(stream, left)
140
+ stream.next(match:AFTER_DOT)
141
+ if stream.token.type == :star
142
+ parse_wildcard_object(stream, left)
143
+ else
144
+ right = parse_dot(stream, Token::BINDING_POWER[:dot])
145
+ Nodes::Subexpression.new(left, right)
146
+ end
147
+ end
148
+
149
+ def led_filter(stream, left)
150
+ stream.next
151
+ expression = expr(stream)
152
+ if stream.token.type != :rbracket
153
+ raise Errors::SyntaxError, 'expected a closing rbracket for the filter'
154
+ end
155
+ stream.next
156
+ rhs = parse_projection(stream, Token::BINDING_POWER[:filter])
157
+ left ||= CURRENT_NODE
158
+ right = Nodes::Condition.new(expression, rhs)
159
+ Nodes::ArrayProjection.new(left, right)
160
+ end
161
+
162
+ def led_flatten(stream, left)
163
+ stream.next
164
+ left = Nodes::Flatten.new(left)
165
+ right = parse_projection(stream, Token::BINDING_POWER[:flatten])
166
+ Nodes::ArrayProjection.new(left, right)
167
+ end
168
+
169
+ def led_lbracket(stream, left)
170
+ stream.next(match: Set.new([:number, :colon, :star]))
171
+ type = stream.token.type
172
+ if type == :number || type == :colon
173
+ right = parse_array_index_expression(stream)
174
+ Nodes::Subexpression.new(left, right)
175
+ else
176
+ parse_wildcard_array(stream, left)
177
+ end
178
+ end
179
+
180
+ def led_lparen(stream, left)
181
+ args = []
182
+ name = left.name
183
+ stream.next
184
+ while stream.token.type != :rparen
185
+ args << expr(stream, 0)
186
+ if stream.token.type == :comma
187
+ stream.next
188
+ end
189
+ end
190
+ stream.next
191
+ Nodes::Function.create(name, args)
192
+ end
193
+
194
+ def led_or(stream, left)
195
+ stream.next
196
+ right = expr(stream, Token::BINDING_POWER[:or])
197
+ Nodes::Or.new(left, right)
198
+ end
199
+
200
+ def led_pipe(stream, left)
201
+ stream.next
202
+ right = expr(stream, Token::BINDING_POWER[:pipe])
203
+ Nodes::Pipe.new(left, right)
204
+ end
205
+
206
+ def parse_array_index_expression(stream)
207
+ pos = 0
208
+ parts = [nil, nil, nil]
209
+ begin
210
+ if stream.token.type == :colon
211
+ pos += 1
212
+ else
213
+ parts[pos] = stream.token.value
214
+ end
215
+ stream.next(match:Set.new([:number, :colon, :rbracket]))
216
+ end while stream.token.type != :rbracket
217
+ stream.next
218
+ if pos == 0
219
+ Nodes::Index.new(parts[0])
220
+ elsif pos > 2
221
+ raise Errors::SyntaxError, 'invalid array slice syntax: too many colons'
222
+ else
223
+ Nodes::Slice.new(*parts)
224
+ end
225
+ end
226
+
227
+ def parse_dot(stream, binding_power)
228
+ if stream.token.type == :lbracket
229
+ stream.next
230
+ parse_multi_select_list(stream)
231
+ else
232
+ expr(stream, binding_power)
233
+ end
234
+ end
235
+
236
+ def parse_key_value_pair(stream)
237
+ key = stream.token.value
238
+ stream.next(match:Set.new([:colon]))
239
+ stream.next
240
+ Nodes::MultiSelectHash::KeyValuePair.new(key, expr(stream))
241
+ end
242
+
243
+ def parse_multi_select_list(stream)
244
+ nodes = []
245
+ begin
246
+ nodes << expr(stream)
247
+ if stream.token.type == :comma
248
+ stream.next
249
+ if stream.token.type == :rbracket
250
+ raise Errors::SyntaxError, 'expression epxected, found rbracket'
251
+ end
252
+ end
253
+ end while stream.token.type != :rbracket
254
+ stream.next
255
+ Nodes::MultiSelectList.new(nodes)
256
+ end
257
+
258
+ def parse_projection(stream, binding_power)
259
+ type = stream.token.type
260
+ if stream.token.binding_power < 10
261
+ CURRENT_NODE
262
+ elsif type == :dot
263
+ stream.next(match:AFTER_DOT)
264
+ parse_dot(stream, binding_power)
265
+ elsif type == :lbracket || type == :filter
266
+ expr(stream, binding_power)
267
+ else
268
+ raise Errors::SyntaxError, 'syntax error after projection'
269
+ end
270
+ end
271
+
272
+ def parse_wildcard_array(stream, left = nil)
273
+ stream.next(match:Set.new([:rbracket]))
274
+ stream.next
275
+ left ||= CURRENT_NODE
276
+ right = parse_projection(stream, Token::BINDING_POWER[:star])
277
+ Nodes::ArrayProjection.new(left, right)
278
+ end
279
+
280
+ def parse_wildcard_object(stream, left = nil)
281
+ stream.next
282
+ left ||= CURRENT_NODE
283
+ right = parse_projection(stream, Token::BINDING_POWER[:star])
284
+ Nodes::ObjectProjection.new(left, right)
285
+ end
286
+
287
+ end
288
+ end
@@ -0,0 +1,82 @@
1
+ module JMESPath
2
+ # @api private
3
+ class Runtime
4
+
5
+ # Constructs a new runtime object for evaluating JMESPath expressions.
6
+ #
7
+ # runtime = JMESPath::Runtime.new
8
+ # runtime.search(expression, data)
9
+ # #=> ...
10
+ #
11
+ # ## Caching
12
+ #
13
+ # When constructing a {Runtime}, the default parser caches expressions.
14
+ # This significantly speeds up calls to {#search} multiple times
15
+ # with the same expression but different data. To disable caching, pass
16
+ # `:cache_expressions => false` to the constructor or pass a custom
17
+ # `:parser`.
18
+ #
19
+ # ## Optimizing
20
+ #
21
+ # By default the runtime will perform optimizations on the expression to
22
+ # try to make it run searches as fast as possible. If all your searches
23
+ # use different expressions this might not be worth the extra work, so you
24
+ # can disable the optimizer by passing `:optimize_expression => false`. If
25
+ # you disable caching it is also recommended that you disable optimizations,
26
+ # but you don't have to The optimizer will be disabled if you pass a custom
27
+ # parser with the `:parser` option.
28
+ #
29
+ # @example Re-use a Runtime, caching enabled by default
30
+ #
31
+ # runtime = JMESPath::Runtime.new
32
+ # runtime.parser
33
+ # #=> #<JMESPath::CachingParser ...>
34
+ #
35
+ # @example Disable caching
36
+ #
37
+ # runtime = JMESPath::Runtime.new(cache_expressions: false)
38
+ # runtime.parser
39
+ # #=> #<JMESPath::Parser ...>
40
+ #
41
+ # @option options [Boolean] :cache_expressions (true) When `false`, a non
42
+ # caching parser will be used. When `true`, a shared instance of
43
+ # {CachingParser} is used. Defaults to `true`.
44
+ #
45
+ # @option options [Boolean] :optimize_expressions (true) When `false`,
46
+ # no additional optimizations will be performed on the expression,
47
+ # when `true` the expression will be analyzed and optimized. This
48
+ # increases the time it takes to parse, but improves the speed of
49
+ # searches, so it's highly recommended if you're using the same expression
50
+ # multiple times and have not disabled caching. Defaults to `true`.
51
+ #
52
+ # @option options [Parser] :parser
53
+ #
54
+ def initialize(options = {})
55
+ @parser = options[:parser] || create_parser(options)
56
+ end
57
+
58
+ # @return [Parser]
59
+ attr_reader :parser
60
+
61
+ # @param [String<JMESPath>] expression
62
+ # @param [Hash] data
63
+ # @return [Mixed,nil]
64
+ def search(expression, data)
65
+ @parser.parse(expression).visit(data)
66
+ end
67
+
68
+ private
69
+
70
+ def create_parser(options)
71
+ parser = Parser.new
72
+ unless options[:optimize_expression] == false
73
+ parser = OptimizingParser.new(parser)
74
+ end
75
+ unless options[:cache_expressions] == false
76
+ parser = CachingParser.new(parser)
77
+ end
78
+ parser
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,41 @@
1
+ module JMESPath
2
+ # @api private
3
+ class Token < Struct.new(:type, :value, :position, :binding_power)
4
+
5
+ # @api private
6
+ NULL_TOKEN = Token.new(:eof, '', nil)
7
+
8
+ # binding power
9
+ # @api private
10
+ BINDING_POWER = {
11
+ :eof => 0,
12
+ :quoted_identifier => 0,
13
+ :identifier => 0,
14
+ :rbracket => 0,
15
+ :rparen => 0,
16
+ :comma => 0,
17
+ :rbrace => 0,
18
+ :number => 0,
19
+ :current => 0,
20
+ :expref => 0,
21
+ :pipe => 1,
22
+ :comparator => 2,
23
+ :or => 5,
24
+ :flatten => 6,
25
+ :star => 20,
26
+ :dot => 40,
27
+ :lbrace => 50,
28
+ :filter => 50,
29
+ :lbracket => 50,
30
+ :lparen => 60,
31
+ }
32
+
33
+ # @param [Symbol] type
34
+ # @param [Mixed] value
35
+ # @param [Integer] position
36
+ def initialize(type, value, position)
37
+ super(type, value, position, BINDING_POWER[type])
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,60 @@
1
+ module JMESPath
2
+ # @api private
3
+ class TokenStream
4
+
5
+ # @param [String<JMESPath>] expression
6
+ # @param [Array<Token>] tokens
7
+ def initialize(expression, tokens)
8
+ @expression = expression
9
+ @tokens = tokens
10
+ @token = nil
11
+ @position = -1
12
+ self.next
13
+ end
14
+
15
+ # @return [String<JMESPath>]
16
+ attr_reader :expression
17
+
18
+ # @return [Token]
19
+ attr_reader :token
20
+
21
+ # @return [Integer]
22
+ attr_reader :position
23
+
24
+ # @option options [Array<Symbol>] :match Requires the next token to be
25
+ # one of the given symbols or an error is raised.
26
+ def next(options = {})
27
+ validate_match(_next, options[:match])
28
+ end
29
+
30
+ def lookahead(count)
31
+ @tokens[@position + count] || Token::NULL_TOKEN
32
+ end
33
+
34
+ # @api private
35
+ def inspect
36
+ str = []
37
+ @tokens.each do |token|
38
+ str << "%3d %-15s %s" %
39
+ [token.position, token.type, token.value.inspect]
40
+ end
41
+ str.join("\n")
42
+ end
43
+
44
+ private
45
+
46
+ def _next
47
+ @position += 1
48
+ @token = @tokens[@position] || Token::NULL_TOKEN
49
+ end
50
+
51
+ def validate_match(token, match)
52
+ if match && !match.include?(token.type)
53
+ raise Errors::SyntaxError, "type missmatch"
54
+ else
55
+ token
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ module JMESPath
2
+ VERSION = '1.1.0'
3
+ end