json_logic 0.1 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +99 -2
- data/json_logic.gemspec +2 -2
- data/lib/core_ext/stringify_keys.rb +21 -0
- data/lib/json_logic.rb +12 -7
- data/lib/json_logic/operation.rb +89 -2
- data/lib/json_logic/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 00b8f19def918cb66b14892b45d3dff9c4c84045
|
4
|
+
data.tar.gz: f8fd51f4eb17fe1c1b6dec4dda24857c63b319a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e07d04d720757f412526c6b76611c6cbf9d68cd63406b8e26aff0f17159eefe7b957b9e86fd737d7962ee9b3e69b1ff522640096c17d9ded8b033462ff6a04a
|
7
|
+
data.tar.gz: fca9ad53de483794014db293c762eca8b246ba812a930fb2d2295e69b419ddf4959f5d4be3552f6e2af08775c8c3ecd6e58079f90a25d6a7f85189302dd96b64
|
data/README.md
CHANGED
@@ -1,5 +1,102 @@
|
|
1
|
-
# json-logic-ruby [![Build Status](https://travis-ci.org/
|
1
|
+
# json-logic-ruby [![Build Status](https://travis-ci.org/bhgames/json-logic-ruby.svg?branch=master)](https://travis-ci.org/bhgames/json-logic-ruby)
|
2
2
|
|
3
3
|
Build complex rules, serialize them as JSON, and execute them in ruby.
|
4
4
|
|
5
|
-
**json-logic-ruby** is a ruby parser for [JsonLogic](http://jsonlogic.com).
|
5
|
+
**json-logic-ruby** is a ruby parser for [JsonLogic](http://jsonlogic.com). Other libraries are available for parsing this logic for Python and JavaScript at that link!
|
6
|
+
|
7
|
+
## Why use JsonLogic?
|
8
|
+
|
9
|
+
If you're looking for a way to share logic between front-end and back-end code, and even store it in a database, JsonLogic might be a fit for you.
|
10
|
+
|
11
|
+
JsonLogic isn't a full programming language. It's a small, safe way to delegate one decision. You could store a rule in a database to decide later. You could send that rule from back-end to front-end so the decision is made immediately from user input. Because the rule is data, you can even build it dynamically from user actions or GUI input.
|
12
|
+
|
13
|
+
JsonLogic has no setters, no loops, no functions or gotos. One rule leads to one decision, with no side effects and deterministic computation time.
|
14
|
+
|
15
|
+
## Virtues
|
16
|
+
1. Terse.
|
17
|
+
2. Consistent. {"operator" : ["values" ... ]} Always.
|
18
|
+
3. Secure. We never eval(). Rules only have read access to data you provide, and no write access to anything.
|
19
|
+
4. Flexible. Easy to add new operators, easy to build complex structures.
|
20
|
+
|
21
|
+
## Examples
|
22
|
+
|
23
|
+
### simple
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
JSONLogic.apply({ "==" => [1, 1] }, {})
|
27
|
+
# => true
|
28
|
+
```
|
29
|
+
|
30
|
+
This is a simple rule, equivalent to 1 == 1. A few things about the format:
|
31
|
+
|
32
|
+
1. The operator is always in the 「key」 position. There is only one key per JsonLogic rule.
|
33
|
+
2. The values are typically an array.
|
34
|
+
3. Each value can be a string, number, boolean, array (non-associative), or null
|
35
|
+
|
36
|
+
### Compound
|
37
|
+
|
38
|
+
Here we're beginning to nest rules.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
JSONLogic.apply(
|
42
|
+
{ "and" => [
|
43
|
+
{ ">" => [3,1] },
|
44
|
+
{ "<" => [1,3] }]
|
45
|
+
}, {})
|
46
|
+
|
47
|
+
# => true
|
48
|
+
```
|
49
|
+
|
50
|
+
In an infix language (like JavaScript) this could be written as:
|
51
|
+
|
52
|
+
```
|
53
|
+
( (3 > 1) && (1 < 3) )
|
54
|
+
```
|
55
|
+
|
56
|
+
### Data-Driven
|
57
|
+
|
58
|
+
Obviously these rules aren't very interesting if they can only take static literal data. Typically jsonLogic will be called with a rule object and a data object. You can use the var operator to get attributes of the data object:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
JSONLogic.apply(
|
62
|
+
{ "var" => ["a"] }, # Rule
|
63
|
+
{ "a" => 1, "b" => 2 } # Data
|
64
|
+
)
|
65
|
+
# => 1
|
66
|
+
```
|
67
|
+
|
68
|
+
If you like, we support syntactic sugar on unary operators to skip the array around values:
|
69
|
+
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
JSONLogic.apply(
|
73
|
+
{ "var" => "a" },
|
74
|
+
{ "a" => 1, "b" => 2 }
|
75
|
+
)
|
76
|
+
# => 1
|
77
|
+
```
|
78
|
+
|
79
|
+
You can also use the `var` operator to access an array by numeric index:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
JSONLogic.apply(
|
83
|
+
{ "var" => 1 },
|
84
|
+
["apple", "banana", "carrot"]
|
85
|
+
)
|
86
|
+
# => "banana"
|
87
|
+
```
|
88
|
+
|
89
|
+
Here's a complex rule that mixes literals and data. The pie isn't ready to eat unless it's cooler than 110 degrees, and filled with apples.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
rules = JSON.parse(%Q|{ "and" : [
|
93
|
+
{"<" : [ { "var" : "temp" }, 110 ]},
|
94
|
+
{"==" : [ { "var" : "pie.filling" }, "apple" ] }
|
95
|
+
] }|)
|
96
|
+
|
97
|
+
data = JSON.parse(%Q|{ "temp" : 100, "pie" : { "filling" : "apple" } }|)
|
98
|
+
|
99
|
+
JSONLogic.apply(rules, data)
|
100
|
+
|
101
|
+
# => true
|
102
|
+
```
|
data/json_logic.gemspec
CHANGED
@@ -6,8 +6,8 @@ require 'json_logic/version'
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = 'json_logic'
|
8
8
|
spec.version = JSONLogic::VERSION
|
9
|
-
spec.authors = ['Kenneth Geerts']
|
10
|
-
spec.email = ['Kenneth.Geerts@gmail.com']
|
9
|
+
spec.authors = ['Kenneth Geerts', "Jordan Prince"]
|
10
|
+
spec.email = ['Kenneth.Geerts@gmail.com', "jordanmprince@gmail.com"]
|
11
11
|
spec.homepage = 'http://jsonlogic.com'
|
12
12
|
spec.summary = 'Build complex rules, serialize them as JSON, and execute them in ruby'
|
13
13
|
spec.description = 'Build complex rules, serialize them as JSON, and execute them in ruby. See http://jsonlogic.com'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Hash
|
2
|
+
# Stolen from ActiveSupport
|
3
|
+
def transform_keys
|
4
|
+
return enum_for(:transform_keys) { size } unless block_given?
|
5
|
+
result = {}
|
6
|
+
each_key do |key|
|
7
|
+
result[yield(key)] = self[key]
|
8
|
+
end
|
9
|
+
result
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns a new hash with all keys converted to strings.
|
13
|
+
#
|
14
|
+
# hash = { name: 'Rob', age: '28' }
|
15
|
+
#
|
16
|
+
# hash.stringify_keys
|
17
|
+
# # => {"name"=>"Rob", "age"=>"28"}
|
18
|
+
def stringify_keys
|
19
|
+
transform_keys(&:to_s)
|
20
|
+
end
|
21
|
+
end
|
data/lib/json_logic.rb
CHANGED
@@ -1,20 +1,25 @@
|
|
1
1
|
require 'core_ext/deep_fetch'
|
2
|
+
require 'core_ext/stringify_keys'
|
2
3
|
require 'json_logic/truthy'
|
3
4
|
require 'json_logic/operation'
|
4
|
-
|
5
5
|
module JSONLogic
|
6
6
|
def self.apply(logic, data)
|
7
|
-
return logic unless logic.is_a?(Hash)
|
8
|
-
|
9
|
-
values =
|
10
|
-
|
11
|
-
|
12
|
-
Operation.perform(operator, new_vals, data || {}) # perform operation
|
7
|
+
return logic unless logic.is_a?(Hash) # pass-thru
|
8
|
+
data = data.stringify_keys if data.is_a?(Hash) # Stringify keys to keep out problems with symbol/string mismatch
|
9
|
+
operator, values = logic.first # unwrap single-key hash
|
10
|
+
values = [values] unless values.is_a?(Array) # syntactic sugar
|
11
|
+
Operation.perform(operator, values, data || {})
|
13
12
|
end
|
14
13
|
|
15
14
|
def self.filter(logic, data)
|
16
15
|
data.select { |d| apply(logic, d) }
|
17
16
|
end
|
17
|
+
|
18
|
+
def self.add_operation(operator, function)
|
19
|
+
Operation.class.send(:define_method, operator) do |v, d|
|
20
|
+
function.call(v, d)
|
21
|
+
end
|
22
|
+
end
|
18
23
|
end
|
19
24
|
|
20
25
|
require 'json_logic/version'
|
data/lib/json_logic/operation.rb
CHANGED
@@ -1,12 +1,66 @@
|
|
1
1
|
module JSONLogic
|
2
2
|
class Operation
|
3
3
|
LAMBDAS = {
|
4
|
-
'var' => ->(v, d)
|
4
|
+
'var' => ->(v, d) do
|
5
|
+
return d unless d.is_a?(Hash) or d.is_a?(Array)
|
6
|
+
return v == [""] ? (d.is_a?(Array) ? d : d[""]) : d.deep_fetch(*v)
|
7
|
+
end,
|
5
8
|
'missing' => ->(v, d) { v.select { |val| d.deep_fetch(val).nil? } },
|
6
9
|
'missing_some' => ->(v, d) {
|
7
10
|
present = v[1] & d.keys
|
8
11
|
present.size >= v[0] ? [] : LAMBDAS['missing'].call(v[1], d)
|
9
12
|
},
|
13
|
+
'some' => -> (v,d) do
|
14
|
+
v[0].any? do |val|
|
15
|
+
interpolated_block(v[1], val).truthy?
|
16
|
+
end
|
17
|
+
end,
|
18
|
+
'filter' => -> (v,d) do
|
19
|
+
v[0].select do |val|
|
20
|
+
interpolated_block(v[1], val).truthy?
|
21
|
+
end
|
22
|
+
end,
|
23
|
+
'substr' => -> (v,d) do
|
24
|
+
limit = -1
|
25
|
+
if v[2]
|
26
|
+
if v[2] < 0
|
27
|
+
limit = v[2] - 1
|
28
|
+
else
|
29
|
+
limit = v[1] + v[2] - 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
v[0][v[1]..limit]
|
34
|
+
end,
|
35
|
+
'none' => -> (v,d) do
|
36
|
+
|
37
|
+
v[0].each do |val|
|
38
|
+
this_val_satisfies_condition = interpolated_block(v[1], val)
|
39
|
+
if this_val_satisfies_condition
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
return true
|
45
|
+
end,
|
46
|
+
'all' => -> (v,d) do
|
47
|
+
# Difference between Ruby and JSONLogic spec ruby all? with empty array is true
|
48
|
+
return false if v[0].empty?
|
49
|
+
|
50
|
+
v[0].all? do |val|
|
51
|
+
interpolated_block(v[1], val)
|
52
|
+
end
|
53
|
+
end,
|
54
|
+
'reduce' => -> (v,d) do
|
55
|
+
return v[2] unless v[0].is_a?(Array)
|
56
|
+
v[0].inject(v[2]) { |acc, val| interpolated_block(v[1], { "current": val, "accumulator": acc })}
|
57
|
+
end,
|
58
|
+
'map' => -> (v,d) do
|
59
|
+
return [] unless v[0].is_a?(Array)
|
60
|
+
v[0].map do |val|
|
61
|
+
interpolated_block(v[1], val)
|
62
|
+
end
|
63
|
+
end,
|
10
64
|
'if' => ->(v, d) {
|
11
65
|
v.each_slice(2) do |condition, value|
|
12
66
|
return condition if value.nil?
|
@@ -42,8 +96,41 @@ module JSONLogic
|
|
42
96
|
'log' => ->(v, d) { puts v }
|
43
97
|
}
|
44
98
|
|
99
|
+
def self.interpolated_block(block, data)
|
100
|
+
# Make sure the empty var is there to be used in iterator
|
101
|
+
JSONLogic.apply(block, data.is_a?(Hash) ? data.merge({"": data}) : { "": data })
|
102
|
+
end
|
103
|
+
|
45
104
|
def self.perform(operator, values, data)
|
46
|
-
|
105
|
+
# If iterable, we can only pre-fill the first element, the second one must be evaluated per element.
|
106
|
+
# If not, we can prefill all.
|
107
|
+
|
108
|
+
if is_iterable?(operator)
|
109
|
+
interpolated = [JSONLogic.apply(values[0], data), *values[1..-1]]
|
110
|
+
else
|
111
|
+
interpolated = values.map { |val| JSONLogic.apply(val, data) }
|
112
|
+
end
|
113
|
+
|
114
|
+
interpolated.flatten!(1) if interpolated.size == 1 # [['A']] => ['A']
|
115
|
+
|
116
|
+
return LAMBDAS[operator.to_s].call(interpolated, data) if is_standard?(operator)
|
117
|
+
send(operator, interpolated, data)
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.is_standard?(operator)
|
121
|
+
LAMBDAS.keys.include?(operator)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Determine if values associated with operator need to be re-interpreted for each iteration(ie some kind of iterator)
|
125
|
+
# or if values can just be evaluated before passing in.
|
126
|
+
def self.is_iterable?(operator)
|
127
|
+
['filter', 'some', 'all', 'none', 'in', 'map', 'reduce'].any? { |o| o == operator }
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.add_operation(operator, function)
|
131
|
+
self.class.send(:define_method, operator) do |v, d|
|
132
|
+
function.call(v, d)
|
133
|
+
end
|
47
134
|
end
|
48
135
|
end
|
49
136
|
end
|
data/lib/json_logic/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json_logic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.3'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kenneth Geerts
|
8
|
+
- Jordan Prince
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2017-12-05 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: bundler
|
@@ -56,6 +57,7 @@ description: Build complex rules, serialize them as JSON, and execute them in ru
|
|
56
57
|
See http://jsonlogic.com
|
57
58
|
email:
|
58
59
|
- Kenneth.Geerts@gmail.com
|
60
|
+
- jordanmprince@gmail.com
|
59
61
|
executables: []
|
60
62
|
extensions: []
|
61
63
|
extra_rdoc_files: []
|
@@ -71,6 +73,7 @@ files:
|
|
71
73
|
- bin/setup
|
72
74
|
- json_logic.gemspec
|
73
75
|
- lib/core_ext/deep_fetch.rb
|
76
|
+
- lib/core_ext/stringify_keys.rb
|
74
77
|
- lib/json_logic.rb
|
75
78
|
- lib/json_logic/operation.rb
|
76
79
|
- lib/json_logic/truthy.rb
|
@@ -95,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
98
|
version: '0'
|
96
99
|
requirements: []
|
97
100
|
rubyforge_project:
|
98
|
-
rubygems_version: 2.
|
101
|
+
rubygems_version: 2.5.1
|
99
102
|
signing_key:
|
100
103
|
specification_version: 4
|
101
104
|
summary: Build complex rules, serialize them as JSON, and execute them in ruby
|