json-logic-rb 0.1.5 → 0.2.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +197 -197
  3. data/lib/json_logic/engine.rb +22 -21
  4. data/lib/json_logic/enumerable_operation.rb +27 -5
  5. data/lib/json_logic/errors/error.rb +29 -0
  6. data/lib/json_logic/errors/invalid_arguments_error.rb +7 -0
  7. data/lib/json_logic/errors/logic_error.rb +7 -0
  8. data/lib/json_logic/errors/nan_error.rb +7 -0
  9. data/lib/json_logic/ext/array.rb +5 -0
  10. data/lib/json_logic/operations/add.rb +3 -3
  11. data/lib/json_logic/operations/all.rb +3 -6
  12. data/lib/json_logic/operations/and.rb +6 -5
  13. data/lib/json_logic/operations/bool_cast.rb +2 -3
  14. data/lib/json_logic/operations/cat.rb +3 -1
  15. data/lib/json_logic/operations/coalesce.rb +9 -0
  16. data/lib/json_logic/operations/div.rb +7 -1
  17. data/lib/json_logic/operations/equal.rb +12 -3
  18. data/lib/json_logic/operations/exists.rb +14 -0
  19. data/lib/json_logic/operations/filter.rb +11 -3
  20. data/lib/json_logic/operations/gt.rb +12 -4
  21. data/lib/json_logic/operations/gte.rb +12 -4
  22. data/lib/json_logic/operations/if.rb +2 -0
  23. data/lib/json_logic/operations/in.rb +2 -0
  24. data/lib/json_logic/operations/lt.rb +10 -4
  25. data/lib/json_logic/operations/lte.rb +12 -4
  26. data/lib/json_logic/operations/map.rb +14 -2
  27. data/lib/json_logic/operations/max.rb +3 -1
  28. data/lib/json_logic/operations/merge.rb +4 -3
  29. data/lib/json_logic/operations/min.rb +3 -1
  30. data/lib/json_logic/operations/missing.rb +4 -26
  31. data/lib/json_logic/operations/missing_some.rb +6 -20
  32. data/lib/json_logic/operations/mod.rb +9 -1
  33. data/lib/json_logic/operations/mul.rb +4 -1
  34. data/lib/json_logic/operations/none.rb +4 -5
  35. data/lib/json_logic/operations/not_equal.rb +12 -3
  36. data/lib/json_logic/operations/or.rb +5 -1
  37. data/lib/json_logic/operations/preserve.rb +9 -0
  38. data/lib/json_logic/operations/reduce.rb +21 -5
  39. data/lib/json_logic/operations/some.rb +4 -7
  40. data/lib/json_logic/operations/strict_equal.rb +26 -3
  41. data/lib/json_logic/operations/strict_not_equal.rb +24 -3
  42. data/lib/json_logic/operations/sub.rb +8 -1
  43. data/lib/json_logic/operations/substr.rb +12 -20
  44. data/lib/json_logic/operations/ternary.rb +1 -7
  45. data/lib/json_logic/operations/throw.rb +12 -0
  46. data/lib/json_logic/operations/try.rb +35 -0
  47. data/lib/json_logic/operations/val.rb +79 -0
  48. data/lib/json_logic/operations/var.rb +22 -20
  49. data/lib/json_logic/scope.rb +67 -0
  50. data/lib/json_logic/semantics.rb +107 -38
  51. data/lib/json_logic/tree.rb +97 -0
  52. data/lib/json_logic/version.rb +1 -1
  53. data/lib/json_logic.rb +12 -0
  54. data/script/build_tests_json.rb +26 -0
  55. data/script/compliance.rb +160 -37
  56. data/spec/tmp/v2/tests.json +16981 -0
  57. metadata +24 -13
  58. /data/spec/tmp/{tests.json → v1/tests.json} +0 -0
data/script/compliance.rb CHANGED
@@ -3,48 +3,171 @@
3
3
  require_relative '../lib/json_logic'
4
4
  require 'json'
5
5
 
6
- path = ARGV[0] || File.expand_path('../spec/tmp/tests.json', __dir__)
7
- abort("tests.json not found at #{path}") unless File.exist?(path)
8
-
9
- payload = JSON.parse(File.read(path))
10
-
11
- extract = lambda do |x|
12
- if x.is_a?(Array) && [2, 3].include?(x.size) && (x[0].is_a?(Hash) || x[0].is_a?(Array))
13
- rule, a2, a3 = x
14
- data, exp = (x.size == 2 ? [nil, a2] : [a2, a3])
15
- [rule, data, exp]
16
- elsif x.is_a?(Hash) && x.key?('rule')
17
- [x['rule'], x['data'], x['result'] || x['expected']]
6
+ SUITES = {
7
+ "compliance_v1" => File.expand_path("../spec/tmp/v1/tests.json", __dir__),
8
+ "compliance_v2" => File.expand_path("../spec/tmp/v2/tests.json", __dir__),
9
+ "original" => File.expand_path("../spec/tmp/v1/tests.json", __dir__),
10
+ "new" => File.expand_path("../spec/tmp/v2/tests.json", __dir__)
11
+ }.freeze
12
+
13
+ def usage!
14
+ puts <<~USAGE
15
+ Usage:
16
+ ruby script/compliance.rb -v [1|2]
17
+ ruby script/compliance.rb -f PATH
18
+ ruby script/compliance.rb [compliance_v1|compliance_v2|PATH]
19
+
20
+ Examples:
21
+ ruby script/compliance.rb -v 1
22
+ ruby script/compliance.rb -v 2
23
+ ruby script/compliance.rb -f spec/tmp/v2/tests.json
24
+ ruby script/compliance.rb compliance_v1
25
+ ruby script/compliance.rb compliance_v2
26
+ ruby script/compliance.rb spec/tmp/v2/tests.json
27
+ USAGE
28
+ exit 1
29
+ end
30
+
31
+ def extract_case(node)
32
+ if node.is_a?(Array) && [2, 3].include?(node.size) && (node[0].is_a?(Hash) || node[0].is_a?(Array))
33
+ rule, a2, a3 = node
34
+ data, expected = (node.size == 2 ? [nil, a2] : [a2, a3])
35
+ return { rule: rule, data: data, expected: expected, expected_error: nil, description: nil }
18
36
  end
37
+
38
+ return unless node.is_a?(Hash) && node.key?("rule")
39
+
40
+ expected = if node.key?("result")
41
+ node["result"]
42
+ elsif node.key?("expected")
43
+ node["expected"]
44
+ end
45
+
46
+ {
47
+ rule: node["rule"],
48
+ data: node["data"],
49
+ expected: expected,
50
+ expected_error: node["error"],
51
+ description: node["description"]
52
+ }
19
53
  end
20
54
 
21
- cases = []
22
- stack = [payload]
23
- while (n = stack.pop)
24
- if (c = extract.call(n))
25
- cases << c
26
- elsif n.is_a?(Array)
27
- n.size == 2 && n[0].is_a?(String) && n[1].is_a?(Array) ? stack << n[1] : n.each { |e| stack << e }
28
- elsif n.is_a?(Hash)
29
- n.each_value { |v| stack << v }
55
+ def collect_cases(payload)
56
+ cases = []
57
+ stack = [payload]
58
+
59
+ while (node = stack.pop)
60
+ if (item = extract_case(node))
61
+ cases << item
62
+ elsif node.is_a?(Array)
63
+ node.size == 2 && node[0].is_a?(String) && node[1].is_a?(Array) ? stack << node[1] : node.each { |e| stack << e }
64
+ elsif node.is_a?(Hash)
65
+ node.each_value { |v| stack << v }
66
+ end
67
+ end
68
+
69
+ cases.reverse
70
+ end
71
+
72
+ def run_suite(label, path)
73
+ abort("#{label}: tests file not found at #{path}") unless File.exist?(path)
74
+
75
+ payload = JSON.parse(File.read(path))
76
+ cases = collect_cases(payload)
77
+ abort("#{label}: no tests found in #{path}") if cases.empty?
78
+
79
+ total = 0
80
+ failed = 0
81
+
82
+ cases.each_with_index do |c, i|
83
+ rule = c[:rule]
84
+ data = c[:data]
85
+ expected = c[:expected]
86
+ expected_error = c[:expected_error]
87
+ description = c[:description]
88
+ total += 1
89
+ case_label = description ? " #{description}" : ""
90
+
91
+ if expected_error
92
+ begin
93
+ got = JsonLogic.apply(rule, data)
94
+ next if expected_error["type"] == "NaN" && got.is_a?(Float) && got.nan?
95
+
96
+ failed += 1
97
+ puts "[FAIL ##{i + 1}]#{case_label} expected_error=#{expected_error.inspect} got=#{got.inspect} rule=#{rule.inspect} data=#{data.inspect}"
98
+ rescue StandardError => e
99
+ got_type =
100
+ if e.respond_to?(:payload) && e.payload.is_a?(Hash) && e.payload["type"]
101
+ e.payload["type"]
102
+ else
103
+ e.message.to_s
104
+ end
105
+
106
+ next if got_type == expected_error["type"]
107
+
108
+ failed += 1
109
+ puts "[ERROR ##{i + 1}]#{case_label} expected_error=#{expected_error.inspect} got_error=#{got_type.inspect} rule=#{rule.inspect} data=#{data.inspect}"
110
+ end
111
+ else
112
+ begin
113
+ got = JsonLogic.apply(rule, data)
114
+ next if got == expected
115
+ next if expected.is_a?(Float) && expected.nan? && got.is_a?(Float) && got.nan?
116
+
117
+ failed += 1
118
+ puts "[FAIL ##{i + 1}]#{case_label} exp=#{expected.inspect} got=#{got.inspect} rule=#{rule.inspect} data=#{data.inspect}"
119
+ rescue StandardError => e
120
+ failed += 1
121
+ puts "[ERROR ##{i + 1}]#{case_label} #{e.class}: #{e.message} rule=#{rule.inspect} data=#{data.inspect}"
122
+ end
123
+ end
30
124
  end
125
+
126
+ passed = total - failed
127
+ percent = total.zero? ? 100.0 : (passed * 100.0 / total)
128
+ puts "#{label}: #{passed}/#{total} passed (#{format('%.2f', percent)}%)"
129
+ { label: label, passed: passed, total: total, failed: failed }
31
130
  end
32
- abort("No tests found in #{path}") if cases.empty?
33
-
34
- total = fail = 0
35
- cases.each_with_index do |(rule, data, expected), i|
36
- total += 1
37
- begin
38
- got = JsonLogic.apply(rule, data)
39
- next if got == expected
40
-
41
- fail += 1
42
- puts "[FAIL ##{i + 1}] exp=#{expected.inspect} got=#{got.inspect} rule=#{rule.inspect} data=#{data.inspect}"
43
- rescue StandardError => e
44
- fail += 1
45
- puts "[ERROR ##{i + 1}] #{e.class}: #{e.message} rule=#{rule.inspect} data=#{data.inspect}"
131
+
132
+ def parse_targets(argv)
133
+ return ["compliance_v2"] if argv.empty?
134
+
135
+ arg = argv[0]
136
+ usage! if %w[-h --help].include?(arg)
137
+ usage! if arg == "all"
138
+
139
+ if %w[-v --suite].include?(arg)
140
+ usage! unless argv.size == 2
141
+ suite = argv[1]
142
+ usage! unless %w[1 2].include?(suite)
143
+ return ["compliance_v#{suite}"]
144
+ end
145
+
146
+ if %w[-f --file].include?(arg)
147
+ usage! unless argv.size == 2
148
+ return [argv[1]]
46
149
  end
150
+
151
+ if (match = arg.match(/\A-v([12])\z/))
152
+ usage! unless argv.size == 1
153
+ return ["compliance_v#{match[1]}"]
154
+ end
155
+
156
+ [arg]
157
+ end
158
+
159
+ targets = parse_targets(ARGV)
160
+
161
+ results = targets.map do |target|
162
+ path = SUITES[target] || target
163
+ run_suite(target, path)
164
+ end
165
+
166
+ if results.size > 1
167
+ passed = results.sum { |r| r[:passed] }
168
+ total = results.sum { |r| r[:total] }
169
+ percent = total.zero? ? 100.0 : (passed * 100.0 / total)
170
+ puts "total: #{passed}/#{total} passed (#{format('%.2f', percent)}%)"
47
171
  end
48
172
 
49
- puts "Compliance: #{total - fail}/#{total} passed"
50
- exit(fail.zero? ? 0 : 1)
173
+ exit(results.any? { |r| r[:failed].positive? } ? 1 : 0)