dry-logic 0.1.4 → 0.2.0

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