json_logic 0.1 → 0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d703a82274651845fd543ed444107612bbc93cf5
4
- data.tar.gz: 434db11c0888e5680d00a2b90fb5171a2ad10927
3
+ metadata.gz: 00b8f19def918cb66b14892b45d3dff9c4c84045
4
+ data.tar.gz: f8fd51f4eb17fe1c1b6dec4dda24857c63b319a3
5
5
  SHA512:
6
- metadata.gz: 51b5633809a496a26546fe5407b4fa919e591dd66ee153ffcfe9989ff39e2732f4eb0c508c497575203a4a7e2f1809cb672cde0943e91713d2f3e5aab21ddfa4
7
- data.tar.gz: be43a7e24f84fb74226d401ccd33bd27a4fee29e6414906d11ab286801da5d2dec5c3ad333e3a281185d8a5ae19d5c0084c56b161ebd2e581abdcfb828a275cf
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/kennethgeerts/json-logic-ruby.svg?branch=master)](https://travis-ci.org/kennethgeerts/json-logic-ruby)
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
+ ```
@@ -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
@@ -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) # pass-thru
8
- operator, values = logic.first # unwrap single-key hash
9
- values = [values] unless values.is_a?(Array) # syntactic sugar
10
- new_vals = values.map { |value| apply(value, data) } # recursion step
11
- new_vals.flatten!(1) if new_vals.size == 1 # [['A']] => ['A']
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'
@@ -1,12 +1,66 @@
1
1
  module JSONLogic
2
2
  class Operation
3
3
  LAMBDAS = {
4
- 'var' => ->(v, d) { d.deep_fetch(*v) },
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
- LAMBDAS[operator].call(values, data)
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
@@ -1,3 +1,3 @@
1
1
  module JSONLogic
2
- VERSION = '0.1'
2
+ VERSION = '0.3'
3
3
  end
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.1'
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: 2016-11-08 00:00:00.000000000 Z
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.6.7
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