1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
|
# frozen_string_literal: true
class JsonPath
class Enumerable
include ::Enumerable
include Dig
def initialize(path, object, mode, options = {})
@path = path.path
@object = object
@mode = mode
@options = options
end
def each(context = @object, key = nil, pos = 0, &blk)
node = key ? dig_one(context, key) : context
@_current_node = node
return yield_value(blk, context, key) if pos == @path.size
case expr = @path[pos]
when '*', '..', '@'
each(context, key, pos + 1, &blk)
when '$'
if node == @object
each(context, key, pos + 1, &blk)
else
handle_wildcard(node, "['#{expr}']", context, key, pos, &blk)
end
when /^\[(.*)\]$/
handle_wildcard(node, expr, context, key, pos, &blk)
when /\(.*\)/
keys = expr.gsub(/[()]/, '').split(',').map(&:strip)
new_context = filter_context(context, keys)
yield_value(blk, new_context, key)
end
if pos > 0 && @path[pos - 1] == '..' || (@path[pos - 1] == '*' && @path[pos] != '..')
case node
when Hash then node.each { |k, _| each(node, k, pos, &blk) }
when Array then node.each_with_index { |_, i| each(node, i, pos, &blk) }
end
end
end
private
def filter_context(context, keys)
case context
when Hash
dig_as_hash(context, keys)
when Array
context.each_with_object([]) do |c, memo|
memo << dig_as_hash(c, keys)
end
end
end
def handle_wildcard(node, expr, _context, _key, pos, &blk)
expr[1, expr.size - 2].split(',').each do |sub_path|
case sub_path[0]
when '\'', '"'
k = sub_path[1, sub_path.size - 2]
yield_if_diggable(node, k) do
each(node, k, pos + 1, &blk)
end
when '?'
handle_question_mark(sub_path, node, pos, &blk)
else
next if node.is_a?(Array) && node.empty?
next if node.nil? # when default_path_leaf_to_null is true
next if node.size.zero?
array_args = sub_path.split(':')
if array_args[0] == '*'
start_idx = 0
end_idx = node.size - 1
elsif sub_path.count(':') == 0
start_idx = end_idx = process_function_or_literal(array_args[0], 0)
next unless start_idx
next if start_idx >= node.size
else
start_idx = process_function_or_literal(array_args[0], 0)
next unless start_idx
end_idx = array_args[1] && ensure_exclusive_end_index(process_function_or_literal(array_args[1], -1)) || -1
next unless end_idx
next if start_idx == end_idx && start_idx >= node.size
end
start_idx %= node.size
end_idx %= node.size
step = process_function_or_literal(array_args[2], 1)
next unless step
if @mode == :delete
(start_idx..end_idx).step(step) { |i| node[i] = nil }
node.compact!
else
(start_idx..end_idx).step(step) { |i| each(node, i, pos + 1, &blk) }
end
end
end
end
def ensure_exclusive_end_index(value)
return value unless value.is_a?(Integer) && value > 0
value - 1
end
def handle_question_mark(sub_path, node, pos, &blk)
case node
when Array
node.size.times do |index|
@_current_node = node[index]
if process_function_or_literal(sub_path[1, sub_path.size - 1])
each(@_current_node, nil, pos + 1, &blk)
end
end
when Hash
if process_function_or_literal(sub_path[1, sub_path.size - 1])
each(@_current_node, nil, pos + 1, &blk)
end
else
yield node if process_function_or_literal(sub_path[1, sub_path.size - 1])
end
end
def yield_value(blk, context, key)
case @mode
when nil
blk.call(key ? dig_one(context, key) : context)
when :compact
if key && context[key].nil?
key.is_a?(Integer) ? context.delete_at(key) : context.delete(key)
end
when :delete
if key
key.is_a?(Integer) ? context.delete_at(key) : context.delete(key)
else
context.replace({})
end
when :substitute
if key
context[key] = blk.call(context[key])
else
context.replace(blk.call(context[key]))
end
end
end
def process_function_or_literal(exp, default = nil)
return default if exp.nil? || exp.empty?
return Integer(exp) if exp[0] != '('
return nil unless @_current_node
identifiers = /@?(((?<!\d)\.(?!\d)(\w+))|\['(.*?)'\])+/.match(exp)
# to filter arrays with known/unknown name.
if (!identifiers.nil? && !(@_current_node.methods.include?(identifiers[2]&.to_sym) || @_current_node.methods.include?(identifiers[4]&.to_sym)))
exp_to_eval = exp.dup
begin
return JsonPath::Parser.new(@_current_node, @options).parse(exp_to_eval)
rescue StandardError
return default
end
end
JsonPath::Parser.new(@_current_node, @options).parse(exp)
end
end
end
|