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 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