iso-jsonpath 1.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonPath
4
+ class Enumerable
5
+ include ::Enumerable
6
+ include Dig
7
+
8
+ def initialize(path, object, mode, options = {})
9
+ @path = path.path
10
+ @object = object
11
+ @mode = mode
12
+ @options = options
13
+ end
14
+
15
+ def each(context = @object, key = nil, pos = 0, &blk)
16
+ node = key ? dig_one(context, key) : context
17
+ @_current_node = node
18
+ return yield_value(blk, context, key) if pos == @path.size
19
+
20
+ case expr = @path[pos]
21
+ when '*', '..', '@'
22
+ each(context, key, pos + 1, &blk)
23
+ when '$'
24
+ if node == @object
25
+ each(context, key, pos + 1, &blk)
26
+ else
27
+ handle_wildcard(node, "['#{expr}']", context, key, pos, &blk)
28
+ end
29
+ when /^\[(.*)\]$/
30
+ handle_wildcard(node, expr, context, key, pos, &blk)
31
+ when /\(.*\)/
32
+ keys = expr.gsub(/[()]/, '').split(',').map(&:strip)
33
+ new_context = filter_context(context, keys)
34
+ yield_value(blk, new_context, key)
35
+ end
36
+
37
+ if pos > 0 && @path[pos - 1] == '..' || (@path[pos - 1] == '*' && @path[pos] != '..')
38
+ case node
39
+ when Hash then node.each { |k, _| each(node, k, pos, &blk) }
40
+ when Array then node.each_with_index { |_, i| each(node, i, pos, &blk) }
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def filter_context(context, keys)
48
+ case context
49
+ when Hash
50
+ dig_as_hash(context, keys)
51
+ when Array
52
+ context.each_with_object([]) do |c, memo|
53
+ memo << dig_as_hash(c, keys)
54
+ end
55
+ end
56
+ end
57
+
58
+ def handle_wildcard(node, expr, _context, _key, pos, &blk)
59
+ expr[1, expr.size - 2].split(',').each do |sub_path|
60
+ case sub_path[0]
61
+ when '\'', '"'
62
+ k = sub_path[1, sub_path.size - 2]
63
+ yield_if_diggable(node, k) do
64
+ each(node, k, pos + 1, &blk)
65
+ end
66
+ when '?'
67
+ handle_question_mark(sub_path, node, pos, &blk)
68
+ else
69
+ next if node.is_a?(Array) && node.empty?
70
+ next if node.nil? # when default_path_leaf_to_null is true
71
+ next if node.size.zero?
72
+
73
+ array_args = sub_path.split(':')
74
+ if array_args[0] == '*'
75
+ start_idx = 0
76
+ end_idx = node.size - 1
77
+ elsif sub_path.count(':') == 0
78
+ start_idx = end_idx = process_function_or_literal(array_args[0], 0)
79
+ next unless start_idx
80
+ next if start_idx >= node.size
81
+ else
82
+ start_idx = process_function_or_literal(array_args[0], 0)
83
+ next unless start_idx
84
+
85
+ end_idx = array_args[1] && ensure_exclusive_end_index(process_function_or_literal(array_args[1], -1)) || -1
86
+ next unless end_idx
87
+ next if start_idx == end_idx && start_idx >= node.size
88
+ end
89
+
90
+ start_idx %= node.size
91
+ end_idx %= node.size
92
+ step = process_function_or_literal(array_args[2], 1)
93
+ next unless step
94
+
95
+ if @mode == :delete
96
+ (start_idx..end_idx).step(step) { |i| node[i] = nil }
97
+ node.compact!
98
+ else
99
+ (start_idx..end_idx).step(step) { |i| each(node, i, pos + 1, &blk) }
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ def ensure_exclusive_end_index(value)
106
+ return value unless value.is_a?(Integer) && value > 0
107
+
108
+ value - 1
109
+ end
110
+
111
+ def handle_question_mark(sub_path, node, pos, &blk)
112
+ case node
113
+ when Array
114
+ node.size.times do |index|
115
+ @_current_node = node[index]
116
+ if process_function_or_literal(sub_path[1, sub_path.size - 1])
117
+ each(@_current_node, nil, pos + 1, &blk)
118
+ end
119
+ end
120
+ when Hash
121
+ if process_function_or_literal(sub_path[1, sub_path.size - 1])
122
+ each(@_current_node, nil, pos + 1, &blk)
123
+ end
124
+ else
125
+ yield node if process_function_or_literal(sub_path[1, sub_path.size - 1])
126
+ end
127
+ end
128
+
129
+ def yield_value(blk, context, key)
130
+ case @mode
131
+ when nil
132
+ blk.call(key ? dig_one(context, key) : context)
133
+ when :compact
134
+ if key && context[key].nil?
135
+ key.is_a?(Integer) ? context.delete_at(key) : context.delete(key)
136
+ end
137
+ when :delete
138
+ if key
139
+ key.is_a?(Integer) ? context.delete_at(key) : context.delete(key)
140
+ else
141
+ context.replace({})
142
+ end
143
+ when :substitute
144
+ if key
145
+ context[key] = blk.call(context[key])
146
+ else
147
+ context.replace(blk.call(context[key]))
148
+ end
149
+ end
150
+ end
151
+
152
+ def process_function_or_literal(exp, default = nil)
153
+ return default if exp.nil? || exp.empty?
154
+ return Integer(exp) if exp[0] != '('
155
+ return nil unless @_current_node
156
+
157
+ identifiers = /@?(((?<!\d)\.(?!\d)(\w+))|\['(.*?)'\])+/.match(exp)
158
+ # to filter arrays with known/unknown name.
159
+ if (!identifiers.nil? && !(@_current_node.methods.include?(identifiers[2]&.to_sym) || @_current_node.methods.include?(identifiers[4]&.to_sym)))
160
+ exp_to_eval = exp.dup
161
+ begin
162
+ return JsonPath::Parser.new(@_current_node, @options).parse(exp_to_eval)
163
+ rescue StandardError
164
+ return default
165
+ end
166
+ end
167
+ JsonPath::Parser.new(@_current_node, @options).parse(exp)
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+
5
+ class JsonPath
6
+ # Parser parses and evaluates an expression passed to @_current_node.
7
+ class Parser
8
+ include Dig
9
+
10
+ REGEX = /\A\/(.+)\/([imxnesu]*)\z|\A%r{(.+)}([imxnesu]*)\z/
11
+
12
+ def initialize(node, options)
13
+ @_current_node = node
14
+ @_expr_map = {}
15
+ @options = options
16
+ end
17
+
18
+ # parse will parse an expression in the following way.
19
+ # Split the expression up into an array of legs for && and || operators.
20
+ # Parse this array into a map for which the keys are the parsed legs
21
+ #  of the split. This map is then used to replace the expression with their
22
+ # corresponding boolean or numeric value. This might look something like this:
23
+ # ((false || false) && (false || true))
24
+ #  Once this string is assembled... we proceed to evaluate from left to right.
25
+ #  The above string is broken down like this:
26
+ # (false && (false || true))
27
+ # (false && true)
28
+ #  false
29
+ def parse(exp)
30
+ exps = exp.split(/(&&)|(\|\|)/)
31
+ construct_expression_map(exps)
32
+ @_expr_map.each { |k, v| exp.sub!(k, v.to_s) }
33
+ raise ArgumentError, "unmatched parenthesis in expression: #{exp}" unless check_parenthesis_count(exp)
34
+
35
+ exp = parse_parentheses(exp) while exp.include?('(')
36
+ bool_or_exp(exp)
37
+ end
38
+
39
+ # Construct a map for which the keys are the expressions
40
+ #  and the values are the corresponding parsed results.
41
+ # Exp.:
42
+ # {"(@['author'] =~ /herman|lukyanenko/i)"=>0}
43
+ # {"@['isTrue']"=>true}
44
+ def construct_expression_map(exps)
45
+ exps.each_with_index do |item, _index|
46
+ next if item == '&&' || item == '||'
47
+
48
+ item = item.strip.gsub(/\)*$/, '').gsub(/^\(*/, '')
49
+ @_expr_map[item] = parse_exp(item)
50
+ end
51
+ end
52
+
53
+ # Using a scanner break down the individual expressions and determine if
54
+ # there is a match in the JSON for it or not.
55
+ def parse_exp(exp)
56
+ exp = exp.sub(/@/, '').gsub(/^\(/, '').gsub(/\)$/, '').tr('"', '\'').strip
57
+ exp.scan(/^\[(\d+)\]/) do |i|
58
+ next if i.empty?
59
+
60
+ index = Integer(i[0])
61
+ raise ArgumentError, 'Node does not appear to be an array.' unless @_current_node.is_a?(Array)
62
+ raise ArgumentError, "Index out of bounds for nested array. Index: #{index}" if @_current_node.size < index
63
+
64
+ @_current_node = @_current_node[index]
65
+ # Remove the extra '' and the index.
66
+ exp = exp.gsub(/^\[\d+\]|\[''\]/, '')
67
+ end
68
+ scanner = StringScanner.new(exp)
69
+ elements = []
70
+ until scanner.eos?
71
+ if (t = scanner.scan(/\['[a-zA-Z@&*\/$%^?_ ]+'\]/))
72
+ elements << t.gsub(/^\s*\[\s*'(.*)'\s*\]\s*$/, '\1')
73
+ elsif (t = scanner.scan(/\.[a-zA-Z0-9_]+[?]?/))
74
+ elements << t.gsub(/[\[\]'.]|\s+/, '')
75
+ elsif (t = scanner.scan(/(\s+)?[<>=!\-+][=~]?(\s+)?/))
76
+ operator = t
77
+ elsif (t = scanner.scan(/(\s+)?'?.*'?(\s+)?/))
78
+ # If we encounter a node which does not contain `'` it means
79
+ #  that we are dealing with a boolean type.
80
+ operand =
81
+ if t == 'true'
82
+ true
83
+ elsif t == 'false'
84
+ false
85
+ elsif operator.to_s.strip == '=~'
86
+ parse_regex(t)
87
+ else
88
+ t.gsub(%r{^'|'$}, '').strip
89
+ end
90
+ elsif (t = scanner.scan(/\/\w+\//))
91
+ elsif (t = scanner.scan(/.*/))
92
+ raise "Could not process symbol: #{t}"
93
+ end
94
+ end
95
+
96
+ el = if elements.empty?
97
+ @_current_node
98
+ elsif @_current_node.is_a?(Hash)
99
+ dig(@_current_node, *elements)
100
+ else
101
+ elements.inject(@_current_node, &:__send__)
102
+ end
103
+
104
+ return (el ? true : false) if el.nil? || operator.nil?
105
+
106
+ el = Float(el) rescue el
107
+ operand = Float(operand) rescue operand
108
+
109
+ el.__send__(operator.strip, operand)
110
+ end
111
+
112
+ private
113
+
114
+ # /foo/i -> Regex.new("foo", Regexp::IGNORECASE) without using eval
115
+ # also supports %r{foo}i
116
+ # following https://github.com/seamusabshere/to_regexp/blob/master/lib/to_regexp.rb
117
+ def parse_regex(t)
118
+ t =~ REGEX
119
+ content = $1 || $3
120
+ options = $2 || $4
121
+
122
+ raise ArgumentError, "unsupported regex #{t} use /foo/ style" if !content || !options
123
+
124
+ content = content.gsub '\\/', '/'
125
+
126
+ flags = 0
127
+ flags |= Regexp::IGNORECASE if options.include?('i')
128
+ flags |= Regexp::MULTILINE if options.include?('m')
129
+ flags |= Regexp::EXTENDED if options.include?('x')
130
+
131
+ # 'n' = none, 'e' = EUC, 's' = SJIS, 'u' = UTF-8
132
+ lang = options.scan(/[nes]/).join.downcase # ignores u since that is default and causes a warning
133
+
134
+ args = [content, flags]
135
+ args << lang unless lang.empty? # avoid warning
136
+ Regexp.new(*args)
137
+ end
138
+
139
+ #  This will break down a parenthesis from the left to the right
140
+ #  and replace the given expression with it's returned value.
141
+ # It does this in order to make it easy to eliminate groups
142
+ # one-by-one.
143
+ def parse_parentheses(str)
144
+ opening_index = 0
145
+ closing_index = 0
146
+
147
+ (0..str.length - 1).step(1) do |i|
148
+ opening_index = i if str[i] == '('
149
+ if str[i] == ')'
150
+ closing_index = i
151
+ break
152
+ end
153
+ end
154
+
155
+ to_parse = str[opening_index + 1..closing_index - 1]
156
+
157
+ #  handle cases like (true && true || false && true) in
158
+ # one giant parenthesis.
159
+ top = to_parse.split(/(&&)|(\|\|)/)
160
+ top = top.map(&:strip)
161
+ res = bool_or_exp(top.shift)
162
+ top.each_with_index do |item, index|
163
+ if item == '&&'
164
+ next_value = bool_or_exp(top[index + 1])
165
+ res &&= next_value
166
+ elsif item == '||'
167
+ next_value = bool_or_exp(top[index + 1])
168
+ res ||= next_value
169
+ end
170
+ end
171
+
172
+ #  if we are at the last item, the opening index will be 0
173
+ # and the closing index will be the last index. To avoid
174
+ # off-by-one errors we simply return the result at that point.
175
+ if closing_index + 1 >= str.length && opening_index == 0
176
+ res.to_s
177
+ else
178
+ "#{str[0..opening_index - 1]}#{res}#{str[closing_index + 1..str.length]}"
179
+ end
180
+ end
181
+
182
+ #  This is convoluted and I should probably refactor it somehow.
183
+ #  The map that is created will contain strings since essentially I'm
184
+ # constructing a string like `true || true && false`.
185
+ # With eval the need for this would disappear but never the less, here
186
+ #  it is. The fact is that the results can be either boolean, or a number
187
+ # in case there is only indexing happening like give me the 3rd item... or
188
+ # it also can be nil in case of regexes or things that aren't found.
189
+ # Hence, I have to be clever here to see what kind of variable I need to
190
+ # provide back.
191
+ def bool_or_exp(b)
192
+ if b.to_s == 'true'
193
+ return true
194
+ elsif b.to_s == 'false'
195
+ return false
196
+ elsif b.to_s == ''
197
+ return nil
198
+ end
199
+
200
+ b = Float(b) rescue b
201
+ b
202
+ end
203
+
204
+ # this simply makes sure that we aren't getting into the whole
205
+ #  parenthesis parsing business without knowing that every parenthesis
206
+ # has its pair.
207
+ def check_parenthesis_count(exp)
208
+ return true unless exp.include?('(')
209
+
210
+ depth = 0
211
+ exp.chars.each do |c|
212
+ if c == '('
213
+ depth += 1
214
+ elsif c == ')'
215
+ depth -= 1
216
+ end
217
+ end
218
+ depth == 0
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonPath
4
+ class Proxy
5
+ attr_reader :obj
6
+ alias to_hash obj
7
+
8
+ def initialize(obj)
9
+ @obj = obj
10
+ end
11
+
12
+ def gsub(path, replacement = nil, &replacement_block)
13
+ _gsub(_deep_copy, path, replacement ? proc(&method(:replacement)) : replacement_block)
14
+ end
15
+
16
+ def gsub!(path, replacement = nil, &replacement_block)
17
+ _gsub(@obj, path, replacement ? proc(&method(:replacement)) : replacement_block)
18
+ end
19
+
20
+ def delete(path = JsonPath::PATH_ALL)
21
+ _delete(_deep_copy, path)
22
+ end
23
+
24
+ def delete!(path = JsonPath::PATH_ALL)
25
+ _delete(@obj, path)
26
+ end
27
+
28
+ def compact(path = JsonPath::PATH_ALL)
29
+ _compact(_deep_copy, path)
30
+ end
31
+
32
+ def compact!(path = JsonPath::PATH_ALL)
33
+ _compact(@obj, path)
34
+ end
35
+
36
+ private
37
+
38
+ def _deep_copy
39
+ Marshal.load(Marshal.dump(@obj))
40
+ end
41
+
42
+ def _gsub(obj, path, replacement)
43
+ JsonPath.new(path)[obj, :substitute].each(&replacement)
44
+ Proxy.new(obj)
45
+ end
46
+
47
+ def _delete(obj, path)
48
+ JsonPath.new(path)[obj, :delete].each
49
+ obj = _remove(obj)
50
+ Proxy.new(obj)
51
+ end
52
+
53
+ def _remove(obj)
54
+ obj.each do |o|
55
+ if o.is_a?(Hash) || o.is_a?(Array)
56
+ _remove(o)
57
+ o.delete({})
58
+ end
59
+ end
60
+ end
61
+
62
+ def _compact(obj, path)
63
+ JsonPath.new(path)[obj, :compact].each
64
+ Proxy.new(obj)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonPath
4
+ VERSION = '1.1.6'
5
+ end
data/lib/jsonpath.rb ADDED
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+ require 'multi_json'
5
+ require 'jsonpath/proxy'
6
+ require 'jsonpath/dig'
7
+ require 'jsonpath/enumerable'
8
+ require 'jsonpath/version'
9
+ require 'jsonpath/parser'
10
+
11
+ # JsonPath: initializes the class with a given JsonPath and parses that path
12
+ # into a token array.
13
+ class JsonPath
14
+ PATH_ALL = '$..*'
15
+ MAX_NESTING_ALLOWED = 100
16
+
17
+ DEFAULT_OPTIONS = {
18
+ :default_path_leaf_to_null => false,
19
+ :symbolize_keys => false,
20
+ :use_symbols => false,
21
+ :allow_send => true,
22
+ :max_nesting => MAX_NESTING_ALLOWED
23
+ }
24
+
25
+ attr_accessor :path
26
+
27
+ def initialize(path, opts = {})
28
+ @opts = DEFAULT_OPTIONS.merge(opts)
29
+ set_max_nesting
30
+ scanner = StringScanner.new(path.strip)
31
+ @path = []
32
+ until scanner.eos?
33
+ if (token = scanner.scan(/\$\B|@\B|\*|\.\./))
34
+ @path << token
35
+ elsif (token = scanner.scan(/[$@\p{Alnum}:{}_ -]+/))
36
+ @path << "['#{token}']"
37
+ elsif (token = scanner.scan(/'(.*?)'/))
38
+ @path << "[#{token}]"
39
+ elsif (token = scanner.scan(/\[/))
40
+ @path << find_matching_brackets(token, scanner)
41
+ elsif (token = scanner.scan(/\]/))
42
+ raise ArgumentError, 'unmatched closing bracket'
43
+ elsif (token = scanner.scan(/\(.*\)/))
44
+ @path << token
45
+ elsif scanner.scan(/\./)
46
+ nil
47
+ elsif (token = scanner.scan(/[><=] \d+/))
48
+ @path.last << token
49
+ elsif (token = scanner.scan(/./))
50
+ @path.last << token
51
+ else
52
+ raise ArgumentError, "character '#{scanner.peek(1)}' not supported in query"
53
+ end
54
+ end
55
+ end
56
+
57
+ def find_matching_brackets(token, scanner)
58
+ count = 1
59
+ until count.zero?
60
+ if (t = scanner.scan(/\[/))
61
+ token << t
62
+ count += 1
63
+ elsif (t = scanner.scan(/\]/))
64
+ token << t
65
+ count -= 1
66
+ elsif (t = scanner.scan(/[^\[\]]+/))
67
+ token << t
68
+ elsif scanner.eos?
69
+ raise ArgumentError, 'unclosed bracket'
70
+ end
71
+ end
72
+ token
73
+ end
74
+
75
+ def join(join_path)
76
+ res = deep_clone
77
+ res.path += JsonPath.new(join_path).path
78
+ res
79
+ end
80
+
81
+ def on(obj_or_str, opts = {})
82
+ a = enum_on(obj_or_str).to_a
83
+ if symbolize_keys?(opts)
84
+ a.map! do |e|
85
+ e.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v; }
86
+ end
87
+ end
88
+ a
89
+ end
90
+
91
+ def self.fetch_all_path(obj)
92
+ all_paths = ['$']
93
+ find_path(obj, '$', all_paths, obj.class == Array)
94
+ return all_paths
95
+ end
96
+
97
+ def self.find_path(obj, root_key, all_paths, is_array = false)
98
+ obj.each do |key, value|
99
+ table_params = { key: key, root_key: root_key}
100
+ is_loop = value.class == Array || value.class == Hash
101
+ if is_loop
102
+ path_exp = construct_path(table_params)
103
+ all_paths << path_exp
104
+ find_path(value, path_exp, all_paths, value.class == Array)
105
+ elsif is_array
106
+ table_params[:index] = obj.find_index(key)
107
+ path_exp = construct_path(table_params)
108
+ find_path(key, path_exp, all_paths, key.class == Array) if key.class == Hash || key.class == Array
109
+ all_paths << path_exp
110
+ else
111
+ all_paths << construct_path(table_params)
112
+ end
113
+ end
114
+ end
115
+
116
+ def self.construct_path(table_row)
117
+ if table_row[:index]
118
+ return table_row[:root_key] + '['+ table_row[:index].to_s + ']'
119
+ else
120
+ return table_row[:root_key] + '.'+ table_row[:key]
121
+ end
122
+ end
123
+
124
+ def first(obj_or_str, *args)
125
+ enum_on(obj_or_str).first(*args)
126
+ end
127
+
128
+ def enum_on(obj_or_str, mode = nil)
129
+ JsonPath::Enumerable.new(self, self.class.process_object(obj_or_str, @opts), mode,
130
+ @opts)
131
+ end
132
+ alias [] enum_on
133
+
134
+ def self.on(obj_or_str, path, opts = {})
135
+ new(path, opts).on(process_object(obj_or_str))
136
+ end
137
+
138
+ def self.for(obj_or_str)
139
+ Proxy.new(process_object(obj_or_str))
140
+ end
141
+
142
+ private
143
+
144
+ def self.process_object(obj_or_str, opts = {})
145
+ obj_or_str.is_a?(String) ? MultiJson.decode(obj_or_str, max_nesting: opts[:max_nesting]) : obj_or_str
146
+ end
147
+
148
+ def deep_clone
149
+ Marshal.load Marshal.dump(self)
150
+ end
151
+
152
+ def set_max_nesting
153
+ return unless @opts[:max_nesting].is_a?(Integer) && @opts[:max_nesting] > MAX_NESTING_ALLOWED
154
+ @opts[:max_nesting] = false
155
+ end
156
+
157
+ def symbolize_keys?(opts)
158
+ opts.fetch(:symbolize_keys, @opts&.dig(:symbolize_keys))
159
+ end
160
+ end