json_logic_ruby 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.
- checksums.yaml +7 -0
- data/lib/json_logic/concerns/trackable.rb +26 -0
- data/lib/json_logic/evaluator.rb +167 -0
- data/lib/json_logic/operations.rb +77 -0
- data/lib/json_logic/rule.rb +54 -0
- data/lib/json_logic/validator.rb +51 -0
- data/lib/json_logic/version.rb +5 -0
- data/lib/json_logic.rb +14 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ca5319a17275ae6b07caa25ed7fb14f8e3a507ac83d19a44c6b8bbc61783ced0
|
4
|
+
data.tar.gz: 1d728aecb2b1901441567e343c433ed45419b8da21dc6f070bdfb420a7a01609
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7d728d93db6bbda69b82bdb6915c1ff3cfe10f05ed5c0648d264b1b8ba287d115397abb3422beb0b852593ba7b476ac2f56c881a595f4b8a191d5f817b1a209e
|
7
|
+
data.tar.gz: e5245e733f1d727a58467f2d23a301273934eed585bdfc92f87df8cabc4b102ebcaa5efc1cc4a785380c86198a545dd9669b1af9024a5791c387994d6e304a9b
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JsonLogic
|
4
|
+
module Trackable
|
5
|
+
attr_reader :tracker
|
6
|
+
|
7
|
+
def init_tracker(operator)
|
8
|
+
if @tracker.nil?
|
9
|
+
@tracker = Rule.new(operator)
|
10
|
+
elsif COMPLEX_OPERATORS.include?(operator)
|
11
|
+
@tracker = Rule.new(operator, @tracker)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def commit_rule_result!(var_name, operator, data, rules, result)
|
16
|
+
if COMPLEX_OPERATORS.include?(operator)
|
17
|
+
# change operand to parent & save result
|
18
|
+
@tracker.result = result
|
19
|
+
@tracker = @tracker.parent unless @tracker.parent.nil?
|
20
|
+
return result
|
21
|
+
end
|
22
|
+
@tracker.add_data_point(var_name, operator, rules, data, result)
|
23
|
+
result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JsonLogic
|
4
|
+
class Evaluator
|
5
|
+
include Trackable
|
6
|
+
|
7
|
+
def apply(rules, data = {})
|
8
|
+
return rules unless rules.is_a?(Hash)
|
9
|
+
|
10
|
+
operator = rules.keys[0]
|
11
|
+
init_tracker(operator)
|
12
|
+
|
13
|
+
values = operator == 'map' ? [] : Array(rules[operator]).map { |rule| apply(rule, data) }
|
14
|
+
|
15
|
+
operators(operator, values, rules, data)
|
16
|
+
end
|
17
|
+
|
18
|
+
def operators(operator, values, rules, data)
|
19
|
+
case operator
|
20
|
+
when 'var' then get_var_value(data, *values)
|
21
|
+
when 'missing' then missing(data, *values)
|
22
|
+
when 'missing_some' then missing_some(data, *values)
|
23
|
+
when 'map' then json_logic_map(data, *rules[operator])
|
24
|
+
else
|
25
|
+
raise("Unrecognized operation #{operator}") unless OPERATIONS.key?(operator)
|
26
|
+
|
27
|
+
execute_operation(operator, rules, data, *values)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# This method traverses `rules` (expected as a nested data structure
|
32
|
+
# composed of Hash and Array objects) to extract values associated
|
33
|
+
# with the key 'var'.
|
34
|
+
#
|
35
|
+
# It's a recursive method that digs into the structure to find 'var'.
|
36
|
+
#
|
37
|
+
# If `rules` is a Hash, it iterates through each key-value pair:
|
38
|
+
# - If the key is 'var', it appends the value to `vars`.
|
39
|
+
# - If the key is not 'var', it recurses into the value (if it's an Array or Hash).
|
40
|
+
#
|
41
|
+
# If `rules` is an Array, it iterates through each element,
|
42
|
+
# which should be a Hash or Array, and recurses through it.
|
43
|
+
#
|
44
|
+
# `vars`, is an optional accumulator Array to hold collected variables.
|
45
|
+
# It starts as an empty array if not provided.
|
46
|
+
#
|
47
|
+
# @param rules [Hash, Array] a data structure composed of nested Hashes
|
48
|
+
# and arrays that we want to extract values from.
|
49
|
+
#
|
50
|
+
# @param vars [Array] an optional accumulator array to hold collected variables.
|
51
|
+
#
|
52
|
+
# @return [Array] an array of extracted variable values.
|
53
|
+
|
54
|
+
def extract_vars(rules, vars = [])
|
55
|
+
if rules.is_a?(Hash)
|
56
|
+
rules.each do |key, value|
|
57
|
+
key == 'var' ? vars << value : extract_vars(value, vars)
|
58
|
+
end
|
59
|
+
return vars
|
60
|
+
end
|
61
|
+
|
62
|
+
rules.each { |rule| extract_vars(rule, vars) } and return vars if rules.is_a?(Array)
|
63
|
+
|
64
|
+
vars
|
65
|
+
end
|
66
|
+
|
67
|
+
# This method recursively searches through the `rules` data structure
|
68
|
+
# (composed of nested Hashes and Arrays) to find all values associated
|
69
|
+
# with a specified variable name.
|
70
|
+
#
|
71
|
+
# If `rules` is an Array:
|
72
|
+
# - It checks if the first element of the Array has the specified variable.
|
73
|
+
# If so, it adds the second element of the Array (presumed to be the value
|
74
|
+
# of the variable) to `values`.
|
75
|
+
# - If not, it recurses through each element of the Array.
|
76
|
+
#
|
77
|
+
# If `rules` is a Hash, it recurses through each value in the Hash.
|
78
|
+
#
|
79
|
+
# `values` is an optional accumulator Array used to collect the found variable values.
|
80
|
+
#
|
81
|
+
# @param rules [Hash, Array] a data structure composed of nested Hashes and
|
82
|
+
# Arrays to be searched through.
|
83
|
+
#
|
84
|
+
# @param var_name [String, Symbol] the name of the variable we want to find the values for.
|
85
|
+
#
|
86
|
+
# @param values [Array] an optional accumulator array to collect found values.
|
87
|
+
#
|
88
|
+
# @return [Array] an array of all values associated with the specified variable in `rules`.
|
89
|
+
|
90
|
+
def fetch_var_values(rules, var_name, values = [])
|
91
|
+
if rules.is_a?(Array)
|
92
|
+
return values << rules[1] if rule_has_var?(rules.first, var_name)
|
93
|
+
|
94
|
+
rules.each { |rule| fetch_var_values(rule, var_name, values) }
|
95
|
+
return values
|
96
|
+
end
|
97
|
+
|
98
|
+
if rules.is_a?(Hash)
|
99
|
+
rules.each { |_, rule| fetch_var_values(rule, var_name, values) }
|
100
|
+
return values
|
101
|
+
end
|
102
|
+
|
103
|
+
values
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def rule_has_var?(rule, var_name)
|
109
|
+
rule.is_a?(Hash) && rule.key?('var') && rule['var'] == var_name
|
110
|
+
end
|
111
|
+
|
112
|
+
def json_logic_map(data, items_rule, map_rule)
|
113
|
+
items = apply(items_rule, data)
|
114
|
+
|
115
|
+
Array(items).map { |item| apply(map_rule, item) }
|
116
|
+
end
|
117
|
+
|
118
|
+
def execute_operation(operator, rules, data, *)
|
119
|
+
result = OPERATIONS[operator].call(*)
|
120
|
+
var_name = get_var_name(operator, rules)
|
121
|
+
|
122
|
+
commit_rule_result!(var_name, operator, data, rules, result)
|
123
|
+
end
|
124
|
+
|
125
|
+
# This method retrieves the value of a variable with a given name from the data structure.
|
126
|
+
#
|
127
|
+
# @param data [Hash] The data structure to search for the variable value.
|
128
|
+
# @param var_name [String] The name of the variable to retrieve.
|
129
|
+
# @return [String, Numeric, nil] The value of the variable if found, otherwise nil.
|
130
|
+
|
131
|
+
def get_var_value(data, var_name, default_value = nil)
|
132
|
+
var_name.to_s.split('.').each do |key|
|
133
|
+
data = data[key]
|
134
|
+
rescue TypeError
|
135
|
+
data = data[key.to_i]
|
136
|
+
end
|
137
|
+
data || default_value
|
138
|
+
end
|
139
|
+
|
140
|
+
# This method retrieves the variable name from a hash of rules based on the given operator.
|
141
|
+
# { "<=" : [ 25, { "var": "age" }, 75] }
|
142
|
+
# { "<=" : [ { "var" : "age" }, 20 ] }
|
143
|
+
#
|
144
|
+
# @param operator [String] The operator for which to retrieve the variable name.
|
145
|
+
# @param rules [Hash] A hash containing rule data.
|
146
|
+
# @return [String, nil] The variable name if found, otherwise nil.
|
147
|
+
|
148
|
+
def get_var_name(operator, rules)
|
149
|
+
args = rules[operator]
|
150
|
+
index = COMPLEX_OPERATORS.exclude?(operator) && args.length == 3 ? 1 : 0
|
151
|
+
args.dig(index, 'var')
|
152
|
+
rescue TypeError
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
|
156
|
+
def missing(data, *args)
|
157
|
+
args.select { |arg| get_var_value(data, arg).nil? }
|
158
|
+
end
|
159
|
+
|
160
|
+
def missing_some(data, min_required, args)
|
161
|
+
return [] if min_required < 1
|
162
|
+
|
163
|
+
missed_args, present_args = args.partition { |arg| get_var_value(data, arg).nil? }
|
164
|
+
present_args.length >= min_required ? [] : missed_args
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JsonLogic
|
4
|
+
COMPLEX_OPERATORS = %w[and or if].freeze
|
5
|
+
|
6
|
+
OPERATIONS = {
|
7
|
+
'==' => ->(a, b) { a == b },
|
8
|
+
'!=' => ->(a, b) { a != b },
|
9
|
+
'>' => ->(a, b) { to_comparable(a) > to_comparable(b) },
|
10
|
+
'>=' => ->(a, b) { to_comparable(a) >= to_comparable(b) },
|
11
|
+
'<' => ->(a, b) { to_comparable(a) < to_comparable(b) },
|
12
|
+
'<=' => lambda do |a, b, c = nil|
|
13
|
+
return to_comparable(a) <= to_comparable(b) if c.nil?
|
14
|
+
|
15
|
+
to_comparable(b).between?(to_comparable(a), to_comparable(c))
|
16
|
+
end,
|
17
|
+
'!' => ->(a) { json_logic_falsey(a) },
|
18
|
+
'!!' => ->(a) { json_logic_truthy(a) },
|
19
|
+
'%' => ->(a, b) { a % b },
|
20
|
+
'and' => ->(*args) { args.reduce(true) { |total, arg| total && arg } },
|
21
|
+
'or' => ->(*args) { args.reduce(false) { |total, arg| total || arg } },
|
22
|
+
'?:' => ->(a, b, c) { json_logic_truthy(a) ? b : c },
|
23
|
+
'if' => lambda do |*args|
|
24
|
+
(0...args.length - 1).step(2) do |i|
|
25
|
+
return args[i + 1] if json_logic_truthy(args[i])
|
26
|
+
end
|
27
|
+
|
28
|
+
args.length.odd? ? args[-1] : nil
|
29
|
+
end,
|
30
|
+
'log' => ->(a) { puts a },
|
31
|
+
'in' => ->(a, b) { b.respond_to?(:include?) ? b.include?(a) : false },
|
32
|
+
'cat' => ->(*args) { args.map(&:to_s).join },
|
33
|
+
'+' => ->(*args) { args.sum(&:to_f) },
|
34
|
+
'*' => ->(*args) { args.reduce(1) { |total, arg| total * arg.to_f } },
|
35
|
+
'-' => ->(*args) { args.length == 1 ? -args[0].to_f : args[0].to_f - args[1].to_f },
|
36
|
+
'/' => ->(a, b) { a.to_f / b },
|
37
|
+
'min' => ->(*args) { args.map { |arg| to_comparable(arg) }.min },
|
38
|
+
'max' => ->(*args) { args.map { |arg| to_comparable(arg) }.max },
|
39
|
+
'merge' => ->(*args) { args.flat_map { |arg| arg.is_a?(Array) ? arg.to_a : arg } },
|
40
|
+
'count' => ->(*args) { args.count { |a| a } }
|
41
|
+
}.freeze
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
COMPARATORS = {
|
46
|
+
NilClass => ->(_) { 0 },
|
47
|
+
FalseClass => ->(_) { 0 },
|
48
|
+
TrueClass => ->(_) { 1 },
|
49
|
+
Array => ->(arr) { arr.map { |item| to_comparable(item) } },
|
50
|
+
Hash => ->(hash) { hash.transform_values { |item| to_comparable(item) } },
|
51
|
+
Numeric => ->(num) { num.to_f }
|
52
|
+
}.freeze
|
53
|
+
|
54
|
+
def to_comparable(value)
|
55
|
+
comparator = COMPARATORS[value.class] || ->(val) { val }
|
56
|
+
comparator.call(value)
|
57
|
+
end
|
58
|
+
|
59
|
+
def json_logic_falsey(value)
|
60
|
+
case value
|
61
|
+
when NilClass, FalseClass, TrueClass
|
62
|
+
!value
|
63
|
+
when Numeric
|
64
|
+
value.zero?
|
65
|
+
when String, Array, Hash
|
66
|
+
value.empty?
|
67
|
+
else
|
68
|
+
false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def json_logic_truthy(value)
|
73
|
+
!json_logic_falsey(value)
|
74
|
+
end
|
75
|
+
|
76
|
+
module_function :to_comparable, :json_logic_falsey, :json_logic_truthy
|
77
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JsonLogic
|
4
|
+
class DataPoint
|
5
|
+
attr_accessor :name, :operation, :expected, :current, :result
|
6
|
+
|
7
|
+
def initialize(name, operation, expected, current, result)
|
8
|
+
@name = name
|
9
|
+
@operation = operation
|
10
|
+
@expected = expected
|
11
|
+
@current = current
|
12
|
+
@result = result
|
13
|
+
end
|
14
|
+
|
15
|
+
def report
|
16
|
+
"DATA: '#{name}' data:#{current || 'None'} #{operation} expected:#{expected_args}, RESULT = #{result}"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def expected_args
|
22
|
+
@expected.length == 3 ? [@expected.first, @expected.last] : @expected.last
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Rule
|
27
|
+
attr_accessor :name, :reasons, :result, :parent, :deep_level
|
28
|
+
|
29
|
+
def initialize(name, parent = nil)
|
30
|
+
@name = name
|
31
|
+
@result = false
|
32
|
+
@parent = parent
|
33
|
+
@reasons = []
|
34
|
+
@deep_level = 0
|
35
|
+
return if @parent.nil?
|
36
|
+
|
37
|
+
@deep_level = @parent.deep_level + 1
|
38
|
+
@parent.reasons << self
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_data_point(var_name, operator, rules, data, result)
|
42
|
+
@result = result
|
43
|
+
current_data = data.is_a?(Hash) ? data[var_name] : data
|
44
|
+
@reasons << DataPoint.new(var_name, operator, rules[operator], current_data, result)
|
45
|
+
end
|
46
|
+
|
47
|
+
def report
|
48
|
+
report = "LOGIC: '#{name}', RESULT = #{result}\n"
|
49
|
+
report + reasons.map do |rule|
|
50
|
+
"#{Array.new(deep_level + 1, "\t").join} #{rule.report}"
|
51
|
+
end.join("\n")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JsonLogic
|
4
|
+
class Validator
|
5
|
+
def json_logic_valid?(rules)
|
6
|
+
return validate_hash(rules) if rules.is_a?(Hash)
|
7
|
+
return validate_array(rules) if rules.is_a?(Array)
|
8
|
+
|
9
|
+
primitive?(rules)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def operator?(operator)
|
15
|
+
operators = JsonLogic::OPERATIONS.keys
|
16
|
+
operators.include?(operator)
|
17
|
+
end
|
18
|
+
|
19
|
+
def variable?(value)
|
20
|
+
return false unless value.is_a?(Hash)
|
21
|
+
|
22
|
+
var = value['var']
|
23
|
+
return false unless var
|
24
|
+
|
25
|
+
var.is_a?(String) || var.is_a?(Numeric) || var.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def primitive?(value)
|
29
|
+
value.is_a?(String) || value.is_a?(Numeric) ||
|
30
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass) || value.is_a?(NilClass)
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_hash(hash)
|
34
|
+
hash.all? do |operator, value|
|
35
|
+
operator?(operator) && json_logic_valid?(value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_array(array)
|
40
|
+
array.all? do |value|
|
41
|
+
json_logic_valid?(value) || variable?(value) || primitive?(value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate_var(value)
|
46
|
+
return false unless value.is_a?(Hash)
|
47
|
+
|
48
|
+
value.key?('var') && value.keys.count == 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/json_logic.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/all'
|
4
|
+
|
5
|
+
require_relative 'json_logic/version'
|
6
|
+
require_relative 'json_logic/operations'
|
7
|
+
require_relative 'json_logic/rule'
|
8
|
+
require_relative 'json_logic/concerns/trackable'
|
9
|
+
require_relative 'json_logic/evaluator'
|
10
|
+
require_relative 'json_logic/validator'
|
11
|
+
|
12
|
+
module JsonLogic
|
13
|
+
class Error < StandardError; end
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: json_logic_ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Volodymyr Stashchenko
|
8
|
+
- Andriy Savka
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2023-11-23 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '7.0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '7.0'
|
28
|
+
description: Build complex rules, serialize them as JSON, and execute them in ruby.
|
29
|
+
See https://jsonlogic.com
|
30
|
+
email:
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- lib/json_logic.rb
|
36
|
+
- lib/json_logic/concerns/trackable.rb
|
37
|
+
- lib/json_logic/evaluator.rb
|
38
|
+
- lib/json_logic/operations.rb
|
39
|
+
- lib/json_logic/rule.rb
|
40
|
+
- lib/json_logic/validator.rb
|
41
|
+
- lib/json_logic/version.rb
|
42
|
+
homepage: https://github.com/useful-libs/json_logic
|
43
|
+
licenses:
|
44
|
+
- MIT
|
45
|
+
metadata:
|
46
|
+
rubygems_mfa_required: 'true'
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 3.2.0
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubygems_version: 3.4.10
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Build complex rules, serialize them as JSON, and execute them in ruby.
|
66
|
+
test_files: []
|