burtpath 1.1.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.
@@ -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