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 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/