json-logic-rb 0.1.0.beta1

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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +315 -0
  4. data/lib/json_logic/engine.rb +54 -0
  5. data/lib/json_logic/enumerable_operation.rb +9 -0
  6. data/lib/json_logic/lazy_operation.rb +3 -0
  7. data/lib/json_logic/operation.rb +14 -0
  8. data/lib/json_logic/operations/add.rb +6 -0
  9. data/lib/json_logic/operations/all.rb +16 -0
  10. data/lib/json_logic/operations/and.rb +14 -0
  11. data/lib/json_logic/operations/bool_cast.rb +9 -0
  12. data/lib/json_logic/operations/cat.rb +6 -0
  13. data/lib/json_logic/operations/div.rb +6 -0
  14. data/lib/json_logic/operations/equal.rb +6 -0
  15. data/lib/json_logic/operations/filter.rb +15 -0
  16. data/lib/json_logic/operations/gt.rb +6 -0
  17. data/lib/json_logic/operations/gte.rb +6 -0
  18. data/lib/json_logic/operations/if.rb +15 -0
  19. data/lib/json_logic/operations/in_op.rb +6 -0
  20. data/lib/json_logic/operations/lt.rb +10 -0
  21. data/lib/json_logic/operations/lte.rb +10 -0
  22. data/lib/json_logic/operations/map.rb +10 -0
  23. data/lib/json_logic/operations/max.rb +6 -0
  24. data/lib/json_logic/operations/merge.rb +8 -0
  25. data/lib/json_logic/operations/min.rb +6 -0
  26. data/lib/json_logic/operations/missing.rb +33 -0
  27. data/lib/json_logic/operations/missing_some.rb +27 -0
  28. data/lib/json_logic/operations/mod.rb +6 -0
  29. data/lib/json_logic/operations/mul.rb +6 -0
  30. data/lib/json_logic/operations/none.rb +14 -0
  31. data/lib/json_logic/operations/not.rb +6 -0
  32. data/lib/json_logic/operations/not_equal.rb +6 -0
  33. data/lib/json_logic/operations/or.rb +12 -0
  34. data/lib/json_logic/operations/reduce.rb +19 -0
  35. data/lib/json_logic/operations/some.rb +16 -0
  36. data/lib/json_logic/operations/strict_equal.rb +6 -0
  37. data/lib/json_logic/operations/strict_not_equal.rb +6 -0
  38. data/lib/json_logic/operations/sub.rb +6 -0
  39. data/lib/json_logic/operations/substr.rb +30 -0
  40. data/lib/json_logic/operations/ternary.rb +13 -0
  41. data/lib/json_logic/operations/var.rb +27 -0
  42. data/lib/json_logic/registry.rb +19 -0
  43. data/lib/json_logic/semantics.rb +41 -0
  44. data/lib/json_logic/version.rb +3 -0
  45. data/lib/json_logic.rb +42 -0
  46. data/script/compliance.rb +50 -0
  47. data/spec/tmp/tests.js +0 -0
  48. data/spec/tmp/tests.json +532 -0
  49. data/test/selftest.rb +15 -0
  50. metadata +96 -0
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::MissingSome < JsonLogic::Operation
4
+ def self.op_name = "missing_some"
5
+
6
+ def call((min_ok, list), data)
7
+ arr = list.is_a?(Array) ? list : Array(list)
8
+ missing = arr.select { |k| dig(data, k).nil? }
9
+ (arr.size - missing.size) >= min_ok ? [] : missing
10
+ end
11
+
12
+ private
13
+ def dig(obj, path)
14
+ return nil if obj.nil?
15
+ cur = obj
16
+ path.to_s.split(".").each do |k|
17
+ if cur.is_a?(Array) && k =~ /\A\d+\z/
18
+ cur = cur[k.to_i]
19
+ elsif cur.is_a?(Hash)
20
+ cur = cur[k] || cur[k.to_s] || cur[k.to_sym]
21
+ else
22
+ return nil
23
+ end
24
+ end
25
+ cur
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::Mod < JsonLogic::Operation
4
+ def self.op_name = "%"
5
+ def call((a,b), _data) = a.to_f % b.to_f
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::Mul < JsonLogic::Operation
4
+ def self.op_name = "*"
5
+ def call(values, _data) = values.map!(&:to_f).inject(1){|m,v| m * v }
6
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::None < JsonLogic::EnumerableOperation
4
+ def self.op_name = "none"
5
+
6
+ def call(args, data)
7
+ items, rule_applied_to_each_item = resolve_items_and_per_item_rule(args, data)
8
+ items.none? do |item|
9
+ JsonLogic::Semantics.truthy?(
10
+ JsonLogic.apply(rule_applied_to_each_item, item)
11
+ )
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::Not < JsonLogic::Operation
4
+ def self.op_name = "!";
5
+ def call((a), _data) = !JsonLogic::Semantics.truthy?(a)
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::NotEqual < JsonLogic::Operation
4
+ def self.op_name = "!="
5
+ def call((a,b), _data) = !JsonLogic::Semantics.loose_equal(a,b)
6
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ class JsonLogic::Operations::Or < JsonLogic::LazyOperation
3
+ def self.op_name = "or"
4
+
5
+ def call(args, data)
6
+ args.each do |a|
7
+ v = JsonLogic.apply(a, data)
8
+ return v if JsonLogic::Semantics.truthy?(v)
9
+ end
10
+ args.empty? ? nil : JsonLogic.apply(args.last, data)
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::Reduce < JsonLogic::EnumerableOperation
4
+ def self.op_name = "reduce"
5
+
6
+ def call(args, data)
7
+ rule_that_returns_items, step_rule_applied_per_item, rule_that_returns_initial_accumulator = args
8
+
9
+ items = Array(JsonLogic.apply(rule_that_returns_items, data))
10
+ acc = JsonLogic.apply(rule_that_returns_initial_accumulator, data)
11
+
12
+ items.reduce(acc) do |memo, item|
13
+ JsonLogic.apply(
14
+ step_rule_applied_per_item,
15
+ (data || {}).merge("" => item, "current" => item, "accumulator" => memo)
16
+ )
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: tru
2
+
3
+ class JsonLogic::Operations::Some < JsonLogic::EnumerableOperation
4
+ def self.op_name = "some"
5
+
6
+ def call(args, data)
7
+ items, rule_applied_to_each_item = resolve_items_and_per_item_rule(args, data)
8
+ return false if items.empty?
9
+
10
+ items.any? do |item|
11
+ JsonLogic::Semantics.truthy?(
12
+ JsonLogic.apply(rule_applied_to_each_item, item)
13
+ )
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::StrictEqual < JsonLogic::Operation
4
+ def self.op_name = "==="
5
+ def call((a,b), _data) = JsonLogic::Semantics.strict_equal(a,b)
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::StrictNotEqual < JsonLogic::Operation
4
+ def self.op_name = "!=="
5
+ def call((a,b), _data) = !JsonLogic::Semantics.strict_equal(a,b)
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::Sub < JsonLogic::Operation
4
+ def self.op_name = "-"
5
+ def call(values, _data) = (values.size == 1 ? -values[0].to_f : values[0].to_f - values[1].to_f)
6
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::Substr < JsonLogic::Operation
4
+ def self.op_name = "substr"
5
+
6
+ def call(values, _data)
7
+ s, i, len = values
8
+ str = s.to_s
9
+ start = i.to_i
10
+
11
+ start += str.length if start < 0
12
+ start = 0 if start < 0
13
+ start = str.length if start > str.length
14
+
15
+ return (str[start..-1] || "") if len.nil?
16
+
17
+ l = len.to_i
18
+ if l >= 0
19
+ slice = str[start, l]
20
+ slice.nil? ? "" : slice
21
+ else
22
+ end_excl = str.length + l
23
+ end_excl = start if end_excl < start
24
+ end_excl = str.length if end_excl > str.length
25
+ length = end_excl - start
26
+ length = 0 if length < 0
27
+ str[start, length] || ""
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::Ternary < JsonLogic::LazyOperation
4
+ def self.op_name = "?:"
5
+
6
+ def call((cond_rule, then_rule, else_rule), data)
7
+ if JsonLogic::Semantics.truthy?(JsonLogic.apply(cond_rule, data))
8
+ JsonLogic.apply(then_rule, data)
9
+ else
10
+ JsonLogic.apply(else_rule, data)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonLogic::Operations::Var < JsonLogic::Operation
4
+ def self.op_name = "var"; def self.values_only? = false
5
+ def call((path_rule, fallback_rule), data)
6
+ path = JsonLogic.apply(path_rule, data)
7
+ return data if path == ""
8
+ val = dig(data, path)
9
+ return val unless val.nil?
10
+ return nil if fallback_rule.nil?
11
+ JsonLogic.apply(fallback_rule, data)
12
+ end
13
+ def dig(obj, path)
14
+ return nil if obj.nil?
15
+ cur = obj
16
+ path.to_s.split(".").each do |k|
17
+ if cur.is_a?(Array) && k =~ /\A\d+\z/
18
+ cur = cur[k.to_i]
19
+ elsif cur.is_a?(Hash)
20
+ cur = cur[k] || cur[k.to_s] || cur[k.to_sym]
21
+ else
22
+ return nil
23
+ end
24
+ end
25
+ cur
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ # lib/json_logic/registry.rb
2
+
3
+ module JsonLogic
4
+ class Registry
5
+ def initialize(map = {})
6
+ @map = map.dup
7
+ end
8
+
9
+ def register(op_class)
10
+ name = op_class.op_name or raise ArgumentError, 'op_name missing'
11
+ @map[name.to_s] = op_class
12
+ self
13
+ end
14
+
15
+ def fetch(name)
16
+ @map[name.to_s]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonLogic
4
+ module Semantics
5
+ module_function
6
+
7
+ def truthy?(v)
8
+ case v
9
+ when nil then false
10
+ when TrueClass, FalseClass then v
11
+ when Numeric then !v.zero?
12
+ when String then !v.empty?
13
+ when Array then !v.empty?
14
+ else true
15
+ end
16
+ end
17
+
18
+ def falsy?(v) = !truthy?(v)
19
+
20
+ def to_number(v)
21
+ return v.to_f if v.is_a?(Numeric) || v.is_a?(String)
22
+ v.to_s.to_f
23
+ end
24
+
25
+ def strict_equal(a,b)
26
+ if a.is_a?(Numeric) && b.is_a?(Numeric)
27
+ a.to_f == b.to_f
28
+ else
29
+ a.class == b.class && a == b
30
+ end
31
+ end
32
+
33
+ def loose_equal(a,b)
34
+ if (a.is_a?(Numeric)||a.is_a?(String)) && (b.is_a?(Numeric)||b.is_a?(String))
35
+ to_number(a) == to_number(b)
36
+ else
37
+ a == b
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonLogic; VERSION = '0.1.0.beta1'; end
data/lib/json_logic.rb ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'json_logic/version'
4
+ require_relative 'json_logic/semantics'
5
+ require_relative 'json_logic/operation'
6
+ require_relative 'json_logic/lazy_operation'
7
+ require_relative 'json_logic/enumerable_operation'
8
+ require_relative 'json_logic/registry'
9
+ require_relative 'json_logic/engine'
10
+
11
+ module JsonLogic
12
+ module Operations
13
+ end
14
+ end
15
+
16
+
17
+ # Load operation classes (each file defines one class with .op_name)
18
+ Dir[File.join(__dir__, 'json_logic', 'operations', '*.rb')].sort.each { |f| require f }
19
+
20
+ # Auto-register all operation classes with .op_name
21
+ module JsonLogic
22
+ module Loader
23
+ module_function
24
+
25
+ def register_all!(registry)
26
+ ObjectSpace.each_object(Class) do |klass|
27
+ next unless klass < JsonLogic::Operation
28
+ next unless klass.respond_to?(:op_name) && klass.op_name && !klass.op_name.to_s.empty?
29
+
30
+ registry.register(klass)
31
+ end
32
+ end
33
+ end
34
+
35
+ class << self
36
+ def apply(rule, data = nil)
37
+ Engine.default.evaluate(rule, data)
38
+ end
39
+ end
40
+ end
41
+
42
+ JsonLogic::Loader.register_all!(JsonLogic::Engine.default.registry)
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/json_logic'
4
+ require 'json'
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']]
18
+ end
19
+ end
20
+
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 }
30
+ end
31
+ 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}"
46
+ end
47
+ end
48
+
49
+ puts "Compliance: #{total - fail}/#{total} passed"
50
+ exit(fail.zero? ? 0 : 1)
data/spec/tmp/tests.js ADDED
File without changes