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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: '090827e0ad9fbdb81bf8a03fbd783b3ec6791fe75f272f34a867d4230941acbc'
4
- data.tar.gz: 1590a5a7c869b86f6c8f9cd2f6fc356fffb7d1456012adbdcd1c39a2cad08ca3
2
+ SHA1:
3
+ metadata.gz: 86c7a2524bbfbf973b55437a09068d62ae594a05
4
+ data.tar.gz: 8653a33163faa160f9eef04155e3906ff6419343
5
5
  SHA512:
6
- metadata.gz: 46e38791ff5794763c762360b7721944f283c32f317e10e046171c6a57b40afb62b0f9cc5db924f52a95b2135b248a8e055a7da08341b95641dc38fec3e8b26f
7
- data.tar.gz: 52e95058cf0373fc98c3773585542960bd3a1cac2f13e3b038820cc6c91311f165441a2aa80644c20ad60be1c9c04cb17b5dfd79c8307e7a70627969aabe7c90
6
+ metadata.gz: ca8350260d52a9c24fc1782393975e74a14639c6152a6d4e450efde8e980882aefd0009ed1c6063cd24be2d36e5f64c16e81e361a42c075b35494c613b1c57d2
7
+ data.tar.gz: aa60fa92da9e3ba857f3914ebb902a473bada3f53a4486997d04a7d37530c942fbe93b13e3ac4cd4e39253ae2b57cdc8191afd4875ab1d9a34e678afb41ae2dc
@@ -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
- ret = parse_exp(exps.shift)
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
- case item
18
- when '&&'
19
- ret &&= parse_exp(exps[index + 1])
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class JsonPath
4
- VERSION = '0.9.9'.freeze
4
+ VERSION = '1.0.0'.freeze
5
5
  end
@@ -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.9.9
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-07 00:00:00.000000000 Z
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
- rubygems_version: 3.0.2
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/