jsonpath 0.9.9 → 1.0.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.
- checksums.yaml +5 -5
- data/lib/jsonpath/parser.rb +113 -8
- data/lib/jsonpath/version.rb +1 -1
- data/test/test_jsonpath.rb +11 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 86c7a2524bbfbf973b55437a09068d62ae594a05
|
4
|
+
data.tar.gz: 8653a33163faa160f9eef04155e3906ff6419343
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ca8350260d52a9c24fc1782393975e74a14639c6152a6d4e450efde8e980882aefd0009ed1c6063cd24be2d36e5f64c16e81e361a42c075b35494c613b1c57d2
|
7
|
+
data.tar.gz: aa60fa92da9e3ba857f3914ebb902a473bada3f53a4486997d04a7d37530c942fbe93b13e3ac4cd4e39253ae2b57cdc8191afd4875ab1d9a34e678afb41ae2dc
|
data/lib/jsonpath/parser.rb
CHANGED
@@ -8,22 +8,46 @@ class JsonPath
|
|
8
8
|
class Parser
|
9
9
|
def initialize(node)
|
10
10
|
@_current_node = node
|
11
|
+
@_expr_map = {}
|
11
12
|
end
|
12
13
|
|
14
|
+
# parse will parse an expression in the following way.
|
15
|
+
# Split the expression up into an array of legs for && and || operators.
|
16
|
+
# Parse this array into a map for which the keys are the parsed legs
|
17
|
+
# of the split. This map is then used to replace the expression with their
|
18
|
+
# corresponding boolean or numeric value. This might look something like this:
|
19
|
+
# ((false || false) && (false || true))
|
20
|
+
# Once this string is assembled... we proceed to evaluate from left to right.
|
21
|
+
# The above string is broken down like this:
|
22
|
+
# (false && (false || true))
|
23
|
+
# (false && true)
|
24
|
+
# false
|
13
25
|
def parse(exp)
|
14
26
|
exps = exp.split(/(&&)|(\|\|)/)
|
15
|
-
|
27
|
+
construct_expression_map(exps)
|
28
|
+
@_expr_map.each {|k, v| exp.sub!(k, "#{v}")}
|
29
|
+
raise ArgumentError, "unmatched parenthesis in expression: #{exp}" unless check_parenthesis_count(exp)
|
30
|
+
while (exp.include?("("))
|
31
|
+
exp = parse_parentheses(exp)
|
32
|
+
end
|
33
|
+
bool_or_exp(exp)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Construct a map for which the keys are the expressions
|
37
|
+
# and the values are the corresponding parsed results.
|
38
|
+
# Exp.:
|
39
|
+
# {"(@['author'] =~ /herman|lukyanenko/i)"=>0}
|
40
|
+
# {"@['isTrue']"=>true}
|
41
|
+
def construct_expression_map(exps)
|
16
42
|
exps.each_with_index do |item, index|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
when '||'
|
21
|
-
ret ||= parse_exp(exps[index + 1])
|
22
|
-
end
|
43
|
+
next if item == '&&' || item == '||'
|
44
|
+
item = item.strip.gsub(/\)*$/, '').gsub(/^\(*/, '')
|
45
|
+
@_expr_map[item] = parse_exp(item)
|
23
46
|
end
|
24
|
-
ret
|
25
47
|
end
|
26
48
|
|
49
|
+
# using a scanner break down the individual expressions and determine if
|
50
|
+
# there is a match in the JSON for it or not.
|
27
51
|
def parse_exp(exp)
|
28
52
|
exp = exp.sub(/@/, '').gsub(/^\(/, '').gsub(/\)$/, '').tr('"', '\'').strip
|
29
53
|
scanner = StringScanner.new(exp)
|
@@ -79,5 +103,86 @@ class JsonPath
|
|
79
103
|
prev = keys.shift
|
80
104
|
dig(keys, hash.fetch(prev))
|
81
105
|
end
|
106
|
+
|
107
|
+
# This will break down a parenthesis from the left to the right
|
108
|
+
# and replace the given expression with it's returned value.
|
109
|
+
# It does this in order to make it easy to eliminate groups
|
110
|
+
# one-by-one.
|
111
|
+
def parse_parentheses(str)
|
112
|
+
opening_index = 0
|
113
|
+
closing_index = 0
|
114
|
+
|
115
|
+
(0..str.length-1).step(1) do |i|
|
116
|
+
if str[i] == '('
|
117
|
+
opening_index = i
|
118
|
+
end
|
119
|
+
if str[i] == ')'
|
120
|
+
closing_index = i
|
121
|
+
break
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
to_parse = str[opening_index+1..closing_index-1]
|
126
|
+
|
127
|
+
# handle cases like (true && true || false && true) in
|
128
|
+
# one giant parenthesis.
|
129
|
+
top = to_parse.split(/(&&)|(\|\|)/)
|
130
|
+
top = top.map{|t| t.strip}
|
131
|
+
res = bool_or_exp(top.shift)
|
132
|
+
top.each_with_index do |item, index|
|
133
|
+
case item
|
134
|
+
when '&&'
|
135
|
+
res &&= top[index + 1]
|
136
|
+
when '||'
|
137
|
+
res ||= top[index + 1]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# if we are at the last item, the opening index will be 0
|
142
|
+
# and the closing index will be the last index. To avoid
|
143
|
+
# off-by-one errors we simply return the result at that point.
|
144
|
+
if closing_index+1 >= str.length && opening_index == 0
|
145
|
+
return "#{res}"
|
146
|
+
else
|
147
|
+
return "#{str[0..opening_index-1]}#{res}#{str[closing_index+1..str.length]}"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# This is convoluted and I should probably refactor it somehow.
|
152
|
+
# The map that is created will contain strings since essentially I'm
|
153
|
+
# constructing a string like `true || true && false`.
|
154
|
+
# With eval the need for this would disappear but never the less, here
|
155
|
+
# it is. The fact is that the results can be either boolean, or a number
|
156
|
+
# in case there is only indexing happening like give me the 3rd item... or
|
157
|
+
# it also can be nil in case of regexes or things that aren't found.
|
158
|
+
# Hence, I have to be clever here to see what kind of variable I need to
|
159
|
+
# provide back.
|
160
|
+
def bool_or_exp(b)
|
161
|
+
if "#{b}" == 'true'
|
162
|
+
return true
|
163
|
+
elsif "#{b}" == 'false'
|
164
|
+
return false
|
165
|
+
elsif "#{b}" == ""
|
166
|
+
return nil
|
167
|
+
end
|
168
|
+
b = Float(b) rescue b
|
169
|
+
b
|
170
|
+
end
|
171
|
+
|
172
|
+
# this simply makes sure that we aren't getting into the whole
|
173
|
+
# parenthesis parsing business without knowing that every parenthesis
|
174
|
+
# has its pair.
|
175
|
+
def check_parenthesis_count(exp)
|
176
|
+
return true unless exp.include?("(")
|
177
|
+
depth = 0
|
178
|
+
exp.chars.each do |c|
|
179
|
+
if c == '('
|
180
|
+
depth += 1
|
181
|
+
elsif c == ')'
|
182
|
+
depth -= 1
|
183
|
+
end
|
184
|
+
end
|
185
|
+
depth == 0
|
186
|
+
end
|
82
187
|
end
|
83
188
|
end
|
data/lib/jsonpath/version.rb
CHANGED
data/test/test_jsonpath.rb
CHANGED
@@ -717,6 +717,17 @@ class TestJsonpath < MiniTest::Unit::TestCase
|
|
717
717
|
assert_equal [true], JsonPath.on(json, broken_path)
|
718
718
|
end
|
719
719
|
|
720
|
+
def test_complex_nested_grouping
|
721
|
+
path = "$..book[?((@['author'] == 'Evelyn Waugh' || @['author'] == 'Herman Melville') && (@['price'] == 33 || @['price'] == 9))]"
|
722
|
+
assert_equal [@object['store']['book'][2]], JsonPath.new(path).on(@object)
|
723
|
+
end
|
724
|
+
|
725
|
+
def test_complex_nested_grouping_unmatched_parent
|
726
|
+
path = "$..book[?((@['author'] == 'Evelyn Waugh' || @['author'] == 'Herman Melville' && (@['price'] == 33 || @['price'] == 9))]"
|
727
|
+
err = assert_raises(ArgumentError, "should have raised an exception") { JsonPath.new(path).on(@object)}
|
728
|
+
assert_match(/unmatched parenthesis in expression: \(\(false \|\| false && \(false \|\| true\)\)/, err.message)
|
729
|
+
end
|
730
|
+
|
720
731
|
def test_delete_more_items
|
721
732
|
a = { 'itemList' =>
|
722
733
|
[{ 'alfa' => 'beta1' },
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonpath
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joshua Hull
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2019-01-
|
12
|
+
date: 2019-01-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: multi_json
|
@@ -158,7 +158,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
158
|
- !ruby/object:Gem::Version
|
159
159
|
version: '0'
|
160
160
|
requirements: []
|
161
|
-
|
161
|
+
rubyforge_project: jsonpath
|
162
|
+
rubygems_version: 2.6.13
|
162
163
|
signing_key:
|
163
164
|
specification_version: 4
|
164
165
|
summary: Ruby implementation of http://goessner.net/articles/JsonPath/
|