dry-logic 0.1.4 → 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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -1
  3. data/README.md +6 -2
  4. data/lib/dry/logic/evaluator.rb +46 -0
  5. data/lib/dry/logic/predicate.rb +3 -3
  6. data/lib/dry/logic/result.rb +26 -126
  7. data/lib/dry/logic/result/each.rb +10 -0
  8. data/lib/dry/logic/result/multi.rb +14 -0
  9. data/lib/dry/logic/result/named.rb +17 -0
  10. data/lib/dry/logic/result/set.rb +10 -0
  11. data/lib/dry/logic/result/value.rb +13 -0
  12. data/lib/dry/logic/rule.rb +14 -36
  13. data/lib/dry/logic/rule/attr.rb +3 -11
  14. data/lib/dry/logic/rule/check.rb +23 -22
  15. data/lib/dry/logic/rule/composite.rb +32 -12
  16. data/lib/dry/logic/rule/each.rb +3 -3
  17. data/lib/dry/logic/rule/key.rb +24 -5
  18. data/lib/dry/logic/rule/negation.rb +15 -0
  19. data/lib/dry/logic/rule/set.rb +9 -8
  20. data/lib/dry/logic/rule/value.rb +15 -3
  21. data/lib/dry/logic/rule_compiler.rb +8 -40
  22. data/lib/dry/logic/version.rb +1 -1
  23. data/spec/shared/predicates.rb +2 -0
  24. data/spec/spec_helper.rb +1 -0
  25. data/spec/unit/rule/attr_spec.rb +5 -5
  26. data/spec/unit/rule/check_spec.rb +26 -39
  27. data/spec/unit/rule/conjunction_spec.rb +4 -4
  28. data/spec/unit/rule/disjunction_spec.rb +3 -3
  29. data/spec/unit/rule/each_spec.rb +2 -2
  30. data/spec/unit/rule/exclusive_disjunction_spec.rb +19 -0
  31. data/spec/unit/rule/implication_spec.rb +2 -2
  32. data/spec/unit/rule/key_spec.rb +103 -9
  33. data/spec/unit/rule/set_spec.rb +7 -9
  34. data/spec/unit/rule/value_spec.rb +29 -3
  35. data/spec/unit/rule_compiler_spec.rb +21 -49
  36. metadata +12 -9
  37. data/lib/dry/logic/rule/group.rb +0 -21
  38. data/lib/dry/logic/rule/result.rb +0 -33
  39. data/rakelib/rubocop.rake +0 -18
  40. data/spec/unit/rule/group_spec.rb +0 -12
  41. data/spec/unit/rule/result_spec.rb +0 -102
@@ -1,56 +1,43 @@
1
1
  RSpec.describe Rule::Check do
2
2
  include_context 'predicates'
3
3
 
4
- let(:other) do
5
- Rule::Value.new(:name, none?).or(Rule::Value.new(:name, filled?))
6
- end
7
-
8
4
  describe '#call' do
9
- subject(:rule) do
10
- Rule::Check::Unary.new(:name, other.(input).curry(predicate), [:name])
11
- end
5
+ context 'with 1-level nesting' do
6
+ subject(:rule) do
7
+ Rule::Check.new(eql?.curry(1), name: :compare, keys: [:num])
8
+ end
12
9
 
13
- context 'when the given predicate passed' do
14
- let(:input) { 'Jane' }
15
- let(:predicate) { :filled? }
10
+ it 'applies predicate to args extracted from the input' do
11
+ expect(rule.(num: 1)).to be_success
12
+ expect(rule.(num: 2)).to be_failure
16
13
 
17
- it 'returns a success' do
18
- expect(rule.('Jane')).to be_success
14
+ expect(rule.(num: 1).to_ast).to eql(
15
+ [:input, [:compare, [
16
+ :result, [1, [:check, [:compare, [:predicate, [:eql?, [1]]]]]]]]
17
+ ]
18
+ )
19
19
  end
20
20
  end
21
21
 
22
- context 'when the given predicate did not pass' do
23
- let(:input) { nil }
24
- let(:predicate) { :filled? }
25
-
26
- it 'returns a failure' do
27
- expect(rule.(nil)).to be_failure
22
+ context 'with 2-levels nesting' do
23
+ subject(:rule) do
24
+ Rule::Check.new(eql?, name: :compare, keys: [[:nums, :left], [:nums, :right]])
28
25
  end
29
- end
30
- end
31
26
 
32
- describe '#call with a nested result' do
33
- subject(:rule) do
34
- Rule::Check::Binary.new(:address, result, [:user, { user: :address }])
35
- end
27
+ it 'applies predicate to args extracted from the input' do
28
+ expect(rule.(nums: { left: 1, right: 1 })).to be_success
29
+ expect(rule.(nums: { left: 1, right: 2 })).to be_failure
30
+ end
36
31
 
37
- let(:other) { Rule::Value.new(:user, hash?) }
38
- let(:result) { other.(input).curry(:hash?) }
39
- let(:input) { { address: 'Earth' } }
32
+ it 'curries args properly' do
33
+ result = rule.(nums: { left: 1, right: 2 })
40
34
 
41
- it 'evaluates the input' do
42
- expect(rule.(user: result).to_ary).to eql([
43
- :input, [
44
- :address, { address: 'Earth' },
45
- [
46
- [:check, [
47
- :address, [
48
- :input, [:user, { address: 'Earth' }, [
49
- [:val, [:user, [:predicate, [:hash?, []]]]]]]]
50
- ]]
35
+ expect(result.to_ast).to eql([
36
+ :input, [:compare, [
37
+ :result, [1, [:check, [:compare, [:predicate, [:eql?, [2]]]]]]]
51
38
  ]
52
- ]
53
- ])
39
+ ])
40
+ end
54
41
  end
55
42
  end
56
43
  end
@@ -3,8 +3,8 @@ RSpec.describe Rule::Composite::Conjunction do
3
3
 
4
4
  subject(:rule) { Rule::Composite::Conjunction.new(left, right) }
5
5
 
6
- let(:left) { Rule::Value.new(:age, int?) }
7
- let(:right) { Rule::Value.new(:age, gt?.curry(18)) }
6
+ let(:left) { Rule::Value.new(int?) }
7
+ let(:right) { Rule::Value.new(gt?.curry(18)) }
8
8
 
9
9
  describe '#call' do
10
10
  it 'calls left and right' do
@@ -13,7 +13,7 @@ RSpec.describe Rule::Composite::Conjunction do
13
13
  end
14
14
 
15
15
  describe '#and' do
16
- let(:other) { Rule::Value.new(:age, lt?.curry(30)) }
16
+ let(:other) { Rule::Value.new(lt?.curry(30)) }
17
17
 
18
18
  it 'creates conjunction with the other' do
19
19
  expect(rule.and(other).(31)).to be_failure
@@ -21,7 +21,7 @@ RSpec.describe Rule::Composite::Conjunction do
21
21
  end
22
22
 
23
23
  describe '#or' do
24
- let(:other) { Rule::Value.new(:age, lt?.curry(14)) }
24
+ let(:other) { Rule::Value.new(lt?.curry(14)) }
25
25
 
26
26
  it 'creates disjunction with the other' do
27
27
  expect(rule.or(other).(13)).to be_success
@@ -3,11 +3,11 @@ RSpec.describe Rule::Composite::Disjunction do
3
3
 
4
4
  subject(:rule) { Rule::Composite::Disjunction.new(left, right) }
5
5
 
6
- let(:left) { Rule::Value.new(:age, none?) }
7
- let(:right) { Rule::Value.new(:age, gt?.curry(18)) }
6
+ let(:left) { Rule::Value.new(none?) }
7
+ let(:right) { Rule::Value.new(gt?.curry(18)) }
8
8
 
9
9
  let(:other) do
10
- Rule::Value.new(:age, int?) & Rule::Value.new(:age, lt?.curry(14))
10
+ Rule::Value.new(int?) & Rule::Value.new(lt?.curry(14))
11
11
  end
12
12
 
13
13
  describe '#call' do
@@ -4,10 +4,10 @@ RSpec.describe Dry::Logic::Rule::Each do
4
4
  include_context 'predicates'
5
5
 
6
6
  subject(:address_rule) do
7
- Dry::Logic::Rule::Each.new(:name, is_string)
7
+ Dry::Logic::Rule::Each.new(is_string)
8
8
  end
9
9
 
10
- let(:is_string) { Dry::Logic::Rule::Value.new(:name, str?) }
10
+ let(:is_string) { Dry::Logic::Rule::Value.new(str?) }
11
11
 
12
12
  describe '#call' do
13
13
  it 'applies its rules to all elements in the input' do
@@ -0,0 +1,19 @@
1
+ RSpec.describe Rule::ExclusiveDisjunction do
2
+ include_context 'predicates'
3
+
4
+ subject(:rule) do
5
+ Rule::ExclusiveDisjunction.new(left, right)
6
+ end
7
+
8
+ let(:left) { Rule::Key.new(true?, name: :eat_cake) }
9
+ let(:right) { Rule::Key.new(true?, name: :have_cake) }
10
+
11
+ describe '#call' do
12
+ it 'calls left and right' do
13
+ expect(rule.(eat_cake: true, have_cake: false)).to be_success
14
+ expect(rule.(eat_cake: false, have_cake: true)).to be_success
15
+ expect(rule.(eat_cake: false, have_cake: false)).to be_failure
16
+ expect(rule.(eat_cake: true, have_cake: true)).to be_failure
17
+ end
18
+ end
19
+ end
@@ -3,8 +3,8 @@ RSpec.describe Rule::Composite::Implication do
3
3
 
4
4
  subject(:rule) { Rule::Composite::Implication.new(left, right) }
5
5
 
6
- let(:left) { Rule::Value.new(:age, int?) }
7
- let(:right) { Rule::Value.new(:age, gt?.curry(18)) }
6
+ let(:left) { Rule::Value.new(int?) }
7
+ let(:right) { Rule::Value.new(gt?.curry(18)) }
8
8
 
9
9
  describe '#call' do
10
10
  it 'calls left and right' do
@@ -1,27 +1,121 @@
1
1
  require 'dry/logic/rule'
2
2
 
3
- RSpec.describe Dry::Logic::Rule::Key do
3
+ RSpec.describe Rule::Key do
4
4
  include_context 'predicates'
5
5
 
6
- subject(:rule) { Dry::Logic::Rule::Key.new(:name, key?) }
6
+ subject(:rule) do
7
+ Rule::Key.new(predicate, name: :user)
8
+ end
9
+
10
+ let(:predicate) do
11
+ key?.curry(:name)
12
+ end
7
13
 
8
14
  describe '#call' do
9
- it 'applies predicate to the value' do
10
- expect(rule.(name: 'Jane')).to be_success
11
- expect(rule.({})).to be_failure
15
+ context 'with a plain predicate' do
16
+ it 'applies predicate to the value' do
17
+ expect(rule.(user: { name: 'Jane' })).to be_success
18
+ expect(rule.(user: {})).to be_failure
19
+ end
20
+
21
+ context 'with a custom predicate' do
22
+ let(:predicate) { -> input { double(success?: true, to_ast: [:foo]) } }
23
+
24
+ let(:result) { rule.(test: true) }
25
+
26
+ it 'delegates to_ast to response' do
27
+ expect(result.to_ast).to eql([:foo])
28
+ end
29
+ end
30
+ end
31
+
32
+ context 'with a set rule as predicate' do
33
+ subject(:rule) do
34
+ Rule::Key.new(predicate, name: :address)
35
+ end
36
+
37
+ let(:predicate) do
38
+ Rule::Set.new(
39
+ [Rule::Value.new(key?.curry(:city)), Rule::Value.new(key?.curry(:zipcode))]
40
+ )
41
+ end
42
+
43
+ it 'applies set rule to the value that passes' do
44
+ result = rule.(address: { city: 'NYC', zipcode: '123' })
45
+
46
+ expect(result).to be_success
47
+ end
48
+
49
+ it 'applies set rule to the value that fails' do
50
+ result = rule.(address: { city: 'NYC' })
51
+
52
+ expect(result).to be_failure
53
+
54
+ expect(result.to_ast).to eql([
55
+ :input, [
56
+ :address,
57
+ [:result, [
58
+ { city: "NYC" },
59
+ [:set, [[:result, [{ city: 'NYC' }, [:val, [:predicate, [:key?, [:zipcode]]]]]]]]
60
+ ]]
61
+ ]
62
+ ])
63
+ end
64
+ end
65
+
66
+ context 'with an each rule as predicate' do
67
+ subject(:rule) do
68
+ Rule::Key.new(predicate, name: :nums)
69
+ end
70
+
71
+ let(:predicate) do
72
+ Rule::Each.new(Rule::Value.new(str?))
73
+ end
74
+
75
+ it 'applies each rule to the value that passses' do
76
+ result = rule.(nums: %w(1 2 3))
77
+
78
+ expect(result).to be_success
79
+
80
+ expect(result.to_ast).to eql([
81
+ :input, [:nums, [:result, [%w(1 2 3), [:each, []]]]]
82
+ ])
83
+ end
84
+
85
+ it 'applies each rule to the value that fails' do
86
+ failure = rule.(nums: [1, '3', 3])
87
+
88
+ expect(failure).to be_failure
89
+
90
+ expect(failure.to_ast).to eql([
91
+ :input, [
92
+ :nums, [
93
+ :result, [
94
+ [1, '3', 3],
95
+ [:each, [
96
+ [:el, [0, [:result, [1, [:val, [:predicate, [:str?, []]]]]]]],
97
+ [:el, [2, [:result, [3, [:val, [:predicate, [:str?, []]]]]]]]
98
+ ]]
99
+ ]
100
+ ]
101
+ ]
102
+ ])
103
+ end
12
104
  end
13
105
  end
14
106
 
15
107
  describe '#and' do
16
- let(:other) { Dry::Logic::Rule::Value.new(:name, str?) }
108
+ let(:other) do
109
+ Rule::Key.new(str?, name: [:user, :name])
110
+ end
17
111
 
18
112
  it 'returns conjunction rule where value is passed to the right' do
19
113
  present_and_string = rule.and(other)
20
114
 
21
- expect(present_and_string.(name: 'Jane')).to be_success
115
+ expect(present_and_string.(user: { name: 'Jane' })).to be_success
22
116
 
23
- expect(present_and_string.({})).to be_failure
24
- expect(present_and_string.(name: 1)).to be_failure
117
+ expect(present_and_string.(user: {})).to be_failure
118
+ expect(present_and_string.(user: { name: 1 })).to be_failure
25
119
  end
26
120
  end
27
121
  end
@@ -4,11 +4,11 @@ RSpec.describe Dry::Logic::Rule::Set do
4
4
  include_context 'predicates'
5
5
 
6
6
  subject(:rule) do
7
- Dry::Logic::Rule::Set.new(:address, [is_string, min_size.curry(6)])
7
+ Dry::Logic::Rule::Set.new([is_string, min_size.curry(6)])
8
8
  end
9
9
 
10
- let(:is_string) { Dry::Logic::Rule::Value.new(:name, str?) }
11
- let(:min_size) { Dry::Logic::Rule::Value.new(:name, min_size?) }
10
+ let(:is_string) { Dry::Logic::Rule::Value.new(str?) }
11
+ let(:min_size) { Dry::Logic::Rule::Value.new(min_size?) }
12
12
 
13
13
  describe '#call' do
14
14
  it 'applies its rules to the input' do
@@ -17,14 +17,12 @@ RSpec.describe Dry::Logic::Rule::Set do
17
17
  end
18
18
  end
19
19
 
20
- describe '#to_ary' do
20
+ describe '#to_ast' do
21
21
  it 'returns an array representation' do
22
- expect(rule).to match_array([
22
+ expect(rule.to_ast).to eql([
23
23
  :set, [
24
- :address, [
25
- [:val, [:name, [:predicate, [:str?, []]]]],
26
- [:val, [:name, [:predicate, [:min_size?, [6]]]]]
27
- ]
24
+ [:val, [:predicate, [:str?, []]]],
25
+ [:val, [:predicate, [:min_size?, [6]]]]
28
26
  ]
29
27
  ])
30
28
  end
@@ -3,17 +3,43 @@ require 'dry/logic/rule'
3
3
  RSpec.describe Dry::Logic::Rule::Value do
4
4
  include_context 'predicates'
5
5
 
6
- let(:is_nil) { Dry::Logic::Rule::Value.new(:name, none?) }
6
+ let(:is_nil) { Dry::Logic::Rule::Value.new(none?) }
7
7
 
8
- let(:is_string) { Dry::Logic::Rule::Value.new(:name, str?) }
8
+ let(:is_string) { Dry::Logic::Rule::Value.new(str?) }
9
9
 
10
- let(:min_size) { Dry::Logic::Rule::Value.new(:name, min_size?) }
10
+ let(:min_size) { Dry::Logic::Rule::Value.new(min_size?) }
11
11
 
12
12
  describe '#call' do
13
13
  it 'returns result of a predicate' do
14
14
  expect(is_string.(1)).to be_failure
15
15
  expect(is_string.('1')).to be_success
16
16
  end
17
+
18
+ context 'with a custom predicate' do
19
+ subject(:rule) { Dry::Logic::Rule::Value.new(predicate) }
20
+
21
+ let(:response) { double(success?: true) }
22
+ let(:predicate) { -> input { Result.new(response, double, input) } }
23
+
24
+ let(:result) { rule.(test: true) }
25
+
26
+ it 'calls its predicate returning custom result' do
27
+ expect(result).to be_success
28
+ end
29
+
30
+ it 'exposes access to nested result' do
31
+ expect(response).to receive(:[]).with(:foo).and_return(:bar)
32
+ expect(result[:foo]).to be(:bar)
33
+ end
34
+
35
+ it 'returns nil from [] when response does not respond to it' do
36
+ expect(result[:foo]).to be(nil)
37
+ end
38
+
39
+ it 'has no name by default' do
40
+ expect(result.name).to be(nil)
41
+ end
42
+ end
17
43
  end
18
44
 
19
45
  describe '#and' do
@@ -8,58 +8,44 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
8
8
  attr?: predicate,
9
9
  filled?: predicate,
10
10
  gt?: predicate,
11
- email: val_rule.('email').curry(:filled?),
12
- left: res_left_rule,
13
- right: double(input: 312) }
11
+ one: predicate }
14
12
  }
15
13
 
16
14
  let(:predicate) { double(:predicate).as_null_object }
17
15
 
18
- let(:key_rule) { Rule::Key.new(:email, predicate) }
19
- let(:not_key_rule) { Rule::Key.new(:email, predicate).negation }
20
- let(:attr_rule) { Rule::Attr.new(:email, predicate) }
21
- let(:val_rule) { Rule::Value.new(:email, predicate) }
22
- let(:check_rule) { Rule::Check::Unary.new(:email, predicates[:email], [:email]) }
23
- let(:res_rule) { Rule::Result.new(:email, predicates[:email]) }
24
- let(:res_left_rule) { Rule::Result.new(:left, predicate) }
16
+ let(:val_rule) { Rule::Value.new(predicate) }
17
+ let(:key_rule) { Rule::Key.new(predicate, name: :email) }
18
+ let(:attr_rule) { Rule::Attr.new(predicate, name: :email) }
19
+ let(:not_key_rule) { Rule::Key.new(predicate, name: :email).negation }
20
+ let(:check_rule) { Rule::Check.new(predicate, name: :email, keys: [:email]) }
25
21
  let(:and_rule) { key_rule & val_rule }
26
22
  let(:or_rule) { key_rule | val_rule }
27
23
  let(:xor_rule) { key_rule ^ val_rule }
28
- let(:set_rule) { Rule::Set.new(:email, [val_rule]) }
29
- let(:each_rule) { Rule::Each.new(:email, val_rule) }
24
+ let(:set_rule) { Rule::Set.new([val_rule]) }
25
+ let(:each_rule) { Rule::Each.new(val_rule) }
30
26
 
31
27
  it 'compiles key rules' do
32
- ast = [[:key, [:email, [:predicate, [:key?, predicate]]]]]
28
+ ast = [[:key, [:email, [:predicate, [:filled?, []]]]]]
33
29
 
34
30
  rules = compiler.(ast)
35
31
 
36
32
  expect(rules).to eql([key_rule])
37
33
  end
38
34
 
39
- it 'compiles check rules' do
40
- ast = [[:check, [:email, [:predicate, [:email, [:filled?]]]]]]
41
-
42
- rules = compiler.(ast)
43
-
44
- expect(rules).to eql([check_rule])
45
- end
46
-
47
- it 'compiles result rules' do
48
- ast = [[:res, [:email, [:predicate, [:email, [:filled?]]]]]]
35
+ it 'compiles attr rules' do
36
+ ast = [[:attr, [:email, [:predicate, [:filled?, []]]]]]
49
37
 
50
38
  rules = compiler.(ast)
51
39
 
52
- expect(rules).to eql([res_rule])
40
+ expect(rules).to eql([attr_rule])
53
41
  end
54
42
 
55
- it 'compiles result rules with res args' do
56
- ast = [[:res, [:left, [:predicate, [:gt?, [:args, [[:res_arg, :right]]]]]]]]
57
-
58
- expect(predicate).to receive(:curry).with(312)
43
+ it 'compiles check rules' do
44
+ ast = [[:check, [:email, [:predicate, [:filled?, []]]]]]
59
45
 
60
46
  rules = compiler.(ast)
61
47
 
62
- expect(rules).to eql([res_left_rule])
48
+ expect(rules).to eql([check_rule])
63
49
  end
64
50
 
65
51
  it 'compiles attr rules' do
@@ -71,7 +57,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
71
57
  end
72
58
 
73
59
  it 'compiles negated rules' do
74
- ast = [[:not, [:key, [:email, [:predicate, [:key?, predicate]]]]]]
60
+ ast = [[:not, [:key, [:email, [:predicate, [:filled?, []]]]]]]
75
61
 
76
62
  rules = compiler.(ast)
77
63
 
@@ -83,7 +69,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
83
69
  [
84
70
  :and, [
85
71
  [:key, [:email, [:predicate, [:key?, []]]]],
86
- [:val, [:email, [:predicate, [:filled?, []]]]]
72
+ [:val, [:predicate, [:filled?, []]]]
87
73
  ]
88
74
  ]
89
75
  ]
@@ -98,7 +84,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
98
84
  [
99
85
  :or, [
100
86
  [:key, [:email, [:predicate, [:key?, []]]]],
101
- [:val, [:email, [:predicate, [:filled?, []]]]]
87
+ [:val, [:predicate, [:filled?, []]]]
102
88
  ]
103
89
  ]
104
90
  ]
@@ -113,7 +99,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
113
99
  [
114
100
  :xor, [
115
101
  [:key, [:email, [:predicate, [:key?, []]]]],
116
- [:val, [:email, [:predicate, [:filled?, []]]]]
102
+ [:val, [:predicate, [:filled?, []]]]
117
103
  ]
118
104
  ]
119
105
  ]
@@ -124,15 +110,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
124
110
  end
125
111
 
126
112
  it 'compiles set rules' do
127
- ast = [
128
- [
129
- :set, [
130
- :email, [
131
- [:val, [:email, [:predicate, [:filled?, []]]]]
132
- ]
133
- ]
134
- ]
135
- ]
113
+ ast = [[:set, [[:val, [:predicate, [:filled?, []]]]]]]
136
114
 
137
115
  rules = compiler.(ast)
138
116
 
@@ -140,13 +118,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
140
118
  end
141
119
 
142
120
  it 'compiles each rules' do
143
- ast = [
144
- [
145
- :each, [
146
- :email, [:val, [:email, [:predicate, [:filled?, []]]]]
147
- ]
148
- ]
149
- ]
121
+ ast = [[:each, [:val, [:predicate, [:filled?, []]]]]]
150
122
 
151
123
  rules = compiler.(ast)
152
124