jsonpath 0.9.9 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/
|