json_logic_ruby 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|