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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +315 -0
- data/lib/json_logic/engine.rb +54 -0
- data/lib/json_logic/enumerable_operation.rb +9 -0
- data/lib/json_logic/lazy_operation.rb +3 -0
- data/lib/json_logic/operation.rb +14 -0
- data/lib/json_logic/operations/add.rb +6 -0
- data/lib/json_logic/operations/all.rb +16 -0
- data/lib/json_logic/operations/and.rb +14 -0
- data/lib/json_logic/operations/bool_cast.rb +9 -0
- data/lib/json_logic/operations/cat.rb +6 -0
- data/lib/json_logic/operations/div.rb +6 -0
- data/lib/json_logic/operations/equal.rb +6 -0
- data/lib/json_logic/operations/filter.rb +15 -0
- data/lib/json_logic/operations/gt.rb +6 -0
- data/lib/json_logic/operations/gte.rb +6 -0
- data/lib/json_logic/operations/if.rb +15 -0
- data/lib/json_logic/operations/in_op.rb +6 -0
- data/lib/json_logic/operations/lt.rb +10 -0
- data/lib/json_logic/operations/lte.rb +10 -0
- data/lib/json_logic/operations/map.rb +10 -0
- data/lib/json_logic/operations/max.rb +6 -0
- data/lib/json_logic/operations/merge.rb +8 -0
- data/lib/json_logic/operations/min.rb +6 -0
- data/lib/json_logic/operations/missing.rb +33 -0
- data/lib/json_logic/operations/missing_some.rb +27 -0
- data/lib/json_logic/operations/mod.rb +6 -0
- data/lib/json_logic/operations/mul.rb +6 -0
- data/lib/json_logic/operations/none.rb +14 -0
- data/lib/json_logic/operations/not.rb +6 -0
- data/lib/json_logic/operations/not_equal.rb +6 -0
- data/lib/json_logic/operations/or.rb +12 -0
- data/lib/json_logic/operations/reduce.rb +19 -0
- data/lib/json_logic/operations/some.rb +16 -0
- data/lib/json_logic/operations/strict_equal.rb +6 -0
- data/lib/json_logic/operations/strict_not_equal.rb +6 -0
- data/lib/json_logic/operations/sub.rb +6 -0
- data/lib/json_logic/operations/substr.rb +30 -0
- data/lib/json_logic/operations/ternary.rb +13 -0
- data/lib/json_logic/operations/var.rb +27 -0
- data/lib/json_logic/registry.rb +19 -0
- data/lib/json_logic/semantics.rb +41 -0
- data/lib/json_logic/version.rb +3 -0
- data/lib/json_logic.rb +42 -0
- data/script/compliance.rb +50 -0
- data/spec/tmp/tests.js +0 -0
- data/spec/tmp/tests.json +532 -0
- data/test/selftest.rb +15 -0
- 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,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,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,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
|
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
|