dry-logic 0.3.0 → 0.4.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -5
  3. data/CHANGELOG.md +28 -0
  4. data/Gemfile +7 -4
  5. data/dry-logic.gemspec +1 -0
  6. data/lib/dry/logic.rb +2 -3
  7. data/lib/dry/logic/appliable.rb +33 -0
  8. data/lib/dry/logic/evaluator.rb +2 -0
  9. data/lib/dry/logic/operations.rb +13 -0
  10. data/lib/dry/logic/operations/abstract.rb +44 -0
  11. data/lib/dry/logic/operations/and.rb +35 -0
  12. data/lib/dry/logic/operations/attr.rb +17 -0
  13. data/lib/dry/logic/operations/binary.rb +26 -0
  14. data/lib/dry/logic/operations/check.rb +52 -0
  15. data/lib/dry/logic/operations/each.rb +32 -0
  16. data/lib/dry/logic/operations/implication.rb +37 -0
  17. data/lib/dry/logic/operations/key.rb +66 -0
  18. data/lib/dry/logic/operations/negation.rb +18 -0
  19. data/lib/dry/logic/operations/or.rb +35 -0
  20. data/lib/dry/logic/operations/set.rb +35 -0
  21. data/lib/dry/logic/operations/unary.rb +24 -0
  22. data/lib/dry/logic/operations/xor.rb +27 -0
  23. data/lib/dry/logic/operators.rb +25 -0
  24. data/lib/dry/logic/predicates.rb +143 -136
  25. data/lib/dry/logic/result.rb +76 -33
  26. data/lib/dry/logic/rule.rb +62 -46
  27. data/lib/dry/logic/rule/predicate.rb +28 -0
  28. data/lib/dry/logic/rule_compiler.rb +16 -17
  29. data/lib/dry/logic/version.rb +1 -1
  30. data/spec/integration/result_spec.rb +59 -0
  31. data/spec/integration/rule_spec.rb +53 -0
  32. data/spec/shared/predicates.rb +6 -0
  33. data/spec/shared/rule.rb +67 -0
  34. data/spec/spec_helper.rb +10 -3
  35. data/spec/support/mutant.rb +9 -0
  36. data/spec/unit/operations/and_spec.rb +64 -0
  37. data/spec/unit/operations/attr_spec.rb +27 -0
  38. data/spec/unit/operations/check_spec.rb +49 -0
  39. data/spec/unit/operations/each_spec.rb +47 -0
  40. data/spec/unit/operations/implication_spec.rb +30 -0
  41. data/spec/unit/operations/key_spec.rb +119 -0
  42. data/spec/unit/operations/negation_spec.rb +40 -0
  43. data/spec/unit/operations/or_spec.rb +73 -0
  44. data/spec/unit/operations/set_spec.rb +41 -0
  45. data/spec/unit/operations/xor_spec.rb +61 -0
  46. data/spec/unit/predicates_spec.rb +23 -0
  47. data/spec/unit/rule/predicate_spec.rb +53 -0
  48. data/spec/unit/rule_compiler_spec.rb +38 -38
  49. data/spec/unit/rule_spec.rb +94 -0
  50. metadata +67 -40
  51. data/lib/dry/logic/predicate.rb +0 -100
  52. data/lib/dry/logic/predicate_set.rb +0 -23
  53. data/lib/dry/logic/result/each.rb +0 -20
  54. data/lib/dry/logic/result/multi.rb +0 -14
  55. data/lib/dry/logic/result/named.rb +0 -17
  56. data/lib/dry/logic/result/set.rb +0 -10
  57. data/lib/dry/logic/result/value.rb +0 -17
  58. data/lib/dry/logic/rule/attr.rb +0 -13
  59. data/lib/dry/logic/rule/check.rb +0 -40
  60. data/lib/dry/logic/rule/composite.rb +0 -91
  61. data/lib/dry/logic/rule/each.rb +0 -13
  62. data/lib/dry/logic/rule/key.rb +0 -37
  63. data/lib/dry/logic/rule/negation.rb +0 -15
  64. data/lib/dry/logic/rule/set.rb +0 -31
  65. data/lib/dry/logic/rule/value.rb +0 -48
  66. data/spec/unit/predicate_spec.rb +0 -115
  67. data/spec/unit/rule/attr_spec.rb +0 -29
  68. data/spec/unit/rule/check_spec.rb +0 -44
  69. data/spec/unit/rule/conjunction_spec.rb +0 -30
  70. data/spec/unit/rule/disjunction_spec.rb +0 -38
  71. data/spec/unit/rule/each_spec.rb +0 -31
  72. data/spec/unit/rule/exclusive_disjunction_spec.rb +0 -19
  73. data/spec/unit/rule/implication_spec.rb +0 -16
  74. data/spec/unit/rule/key_spec.rb +0 -121
  75. data/spec/unit/rule/set_spec.rb +0 -30
  76. data/spec/unit/rule/value_spec.rb +0 -99
@@ -0,0 +1,40 @@
1
+ RSpec.describe Operations::Negation do
2
+ subject(:operation) { Operations::Negation.new(is_int) }
3
+
4
+ include_context 'predicates'
5
+
6
+ let(:is_int) { Rule::Predicate.new(int?) }
7
+
8
+ describe '#call' do
9
+ it 'negates its rule' do
10
+ expect(operation.('19')).to be_success
11
+ expect(operation.(17)).to be_failure
12
+ end
13
+ end
14
+
15
+ describe '#to_ast' do
16
+ it 'returns ast' do
17
+ expect(operation.to_ast).to eql(
18
+ [:not, [:predicate, [:int?, [[:input, Undefined]]]]]
19
+ )
20
+ end
21
+
22
+ it 'returns result ast' do
23
+ expect(operation.(17).to_ast).to eql(
24
+ [:not, [:predicate, [:int?, [[:input, 17]]]]]
25
+ )
26
+ end
27
+
28
+ it 'returns result ast with an :id' do
29
+ expect(operation.with(id: :age).(17).to_ast).to eql(
30
+ [:failure, [:age, [:not, [:predicate, [:int?, [[:input, 17]]]]]]]
31
+ )
32
+ end
33
+ end
34
+
35
+ describe '#to_s' do
36
+ it 'returns string representation' do
37
+ expect(operation.to_s).to eql('not(int?)')
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,73 @@
1
+ RSpec.describe Operations::Or do
2
+ subject(:operation) { Operations::Or.new(left, right) }
3
+
4
+ include_context 'predicates'
5
+
6
+ let(:left) { Rule::Predicate.new(none?) }
7
+ let(:right) { Rule::Predicate.new(gt?).curry(18) }
8
+
9
+ let(:other) do
10
+ Rule::Predicate.new(int?) & Rule::Predicate.new(lt?).curry(14)
11
+ end
12
+
13
+ describe '#call' do
14
+ it 'calls left and right' do
15
+ expect(operation.(nil)).to be_success
16
+ expect(operation.(19)).to be_success
17
+ expect(operation.(18)).to be_failure
18
+ end
19
+ end
20
+
21
+ describe '#to_ast' do
22
+ it 'returns ast' do
23
+ expect(operation.to_ast).to eql(
24
+ [:or, [
25
+ [:predicate, [:none?, [[:input, Undefined]]]],
26
+ [:predicate, [:gt?, [[:num, 18], [:input, Undefined]]]]]
27
+ ]
28
+ )
29
+ end
30
+
31
+ it 'returns result ast' do
32
+ expect(operation.(17).to_ast).to eql(
33
+ [:or, [
34
+ [:predicate, [:none?, [[:input, 17]]]],
35
+ [:predicate, [:gt?, [[:num, 18], [:input, 17]]]]]
36
+ ]
37
+ )
38
+ end
39
+
40
+ it 'returns failure result ast' do
41
+ expect(operation.with(id: :age).(17).to_ast).to eql(
42
+ [:failure, [:age, [:or, [
43
+ [:predicate, [:none?, [[:input, 17]]]],
44
+ [:predicate, [:gt?, [[:num, 18], [:input, 17]]]]]
45
+ ]]]
46
+ )
47
+ end
48
+ end
49
+
50
+ describe '#and' do
51
+ it 'creates and with the other' do
52
+ expect(operation.and(other).(nil)).to be_failure
53
+ expect(operation.and(other).(19)).to be_failure
54
+ expect(operation.and(other).(13)).to be_failure
55
+ expect(operation.and(other).(14)).to be_failure
56
+ end
57
+ end
58
+
59
+ describe '#or' do
60
+ it 'creates or with the other' do
61
+ expect(operation.or(other).(nil)).to be_success
62
+ expect(operation.or(other).(19)).to be_success
63
+ expect(operation.or(other).(13)).to be_success
64
+ expect(operation.or(other).(14)).to be_failure
65
+ end
66
+ end
67
+
68
+ describe '#to_s' do
69
+ it 'returns string representation' do
70
+ expect(operation.to_s).to eql('none? OR gt?(18)')
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,41 @@
1
+ RSpec.describe Operations::Set do
2
+ subject(:operation) { Operations::Set.new(is_int, gt_18) }
3
+
4
+ include_context 'predicates'
5
+
6
+ let(:is_int) { Rule::Predicate.new(int?) }
7
+ let(:gt_18) { Rule::Predicate.new(gt?, args: [18]) }
8
+
9
+ describe '#call' do
10
+ it 'applies all its rules to the input' do
11
+ expect(operation.(19)).to be_success
12
+ expect(operation.(17)).to be_failure
13
+ end
14
+ end
15
+
16
+ describe '#to_ast' do
17
+ it 'returns ast' do
18
+ expect(operation.to_ast).to eql(
19
+ [:set, [[:predicate, [:int?, [[:input, Undefined]]]], [:predicate, [:gt?, [[:num, 18], [:input, Undefined]]]]]]
20
+ )
21
+ end
22
+
23
+ it 'returns result ast' do
24
+ expect(operation.(17).to_ast).to eql(
25
+ [:set, [[:predicate, [:gt?, [[:num, 18], [:input, 17]]]]]]
26
+ )
27
+ end
28
+
29
+ it 'returns result ast with an :id' do
30
+ expect(operation.with(id: :age).(17).to_ast).to eql(
31
+ [:failure, [:age, [:set, [[:predicate, [:gt?, [[:num, 18], [:input, 17]]]]]]]]
32
+ )
33
+ end
34
+ end
35
+
36
+ describe '#to_s' do
37
+ it 'returns string representation' do
38
+ expect(operation.to_s).to eql('set(int?, gt?(18))')
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,61 @@
1
+ RSpec.describe Operations::Xor do
2
+ subject(:operation) { Operations::Xor.new(left, right) }
3
+
4
+ include_context 'predicates'
5
+
6
+ let(:left) { Rule::Predicate.new(array?) }
7
+ let(:right) { Rule::Predicate.new(empty?) }
8
+
9
+ let(:other) do
10
+ Rule::Predicate.new(str?)
11
+ end
12
+
13
+ describe '#call' do
14
+ it 'calls left and right' do
15
+ expect(operation.(nil)).to be_success
16
+ expect(operation.('')).to be_success
17
+ expect(operation.([])).to be_failure
18
+ end
19
+ end
20
+
21
+ describe '#to_ast' do
22
+ it 'returns ast' do
23
+ expect(operation.to_ast).to eql(
24
+ [:xor, [[:predicate, [:array?, [[:input, Undefined]]]], [:predicate, [:empty?, [[:input, Undefined]]]]]]
25
+ )
26
+ end
27
+
28
+ it 'returns result ast' do
29
+ expect(operation.([]).to_ast).to eql(
30
+ [:xor, [[:predicate, [:array?, [[:input, []]]]], [:predicate, [:empty?, [[:input, []]]]]]]
31
+ )
32
+ end
33
+
34
+ it 'returns failure result ast' do
35
+ expect(operation.with(id: :name).([]).to_ast).to eql(
36
+ [:failure, [:name, [:xor, [[:predicate, [:array?, [[:input, []]]]], [:predicate, [:empty?, [[:input, []]]]]]]]]
37
+ )
38
+ end
39
+ end
40
+
41
+ describe '#and' do
42
+ it 'creates conjunction with the other' do
43
+ expect(operation.and(other).(nil)).to be_failure
44
+ expect(operation.and(other).(19)).to be_failure
45
+ expect(operation.and(other).('')).to be_success
46
+ end
47
+ end
48
+
49
+ describe '#or' do
50
+ it 'creates disjunction with the other' do
51
+ expect(operation.or(other).([])).to be_failure
52
+ expect(operation.or(other).('')).to be_success
53
+ end
54
+ end
55
+
56
+ describe '#to_s' do
57
+ it 'returns string representation' do
58
+ expect(operation.to_s).to eql('array? XOR empty?')
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,23 @@
1
+ require 'dry/logic/predicates'
2
+
3
+ RSpec.describe Predicates do
4
+ it 'can be included in another module' do
5
+ mod = Module.new { include Predicates }
6
+
7
+ expect(mod[:key?]).to be_a(Method)
8
+ end
9
+
10
+ describe '.predicate' do
11
+ it 'defines a predicate method' do
12
+ mod = Module.new {
13
+ include Predicates
14
+
15
+ predicate(:test?) do |foo|
16
+ true
17
+ end
18
+ }
19
+
20
+ expect(mod.test?('arg')).to be(true)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,53 @@
1
+ RSpec.describe Rule::Predicate do
2
+ subject(:rule) { Rule::Predicate.new(predicate) }
3
+
4
+ let(:predicate) { str? }
5
+
6
+ include_context 'predicates'
7
+
8
+ it_behaves_like Rule
9
+
10
+ describe '#name' do
11
+ it 'returns predicate identifier' do
12
+ expect(rule.name).to be(:str?)
13
+ end
14
+ end
15
+
16
+ describe '#to_ast' do
17
+ context 'without a result' do
18
+ it 'returns rule ast' do
19
+ expect(rule.to_ast).to eql([:predicate, [:str?, [[:input, Undefined]]]])
20
+ end
21
+
22
+ it 'returns :failure with an id' do
23
+ email = rule.with(id: :email)
24
+
25
+ expect(email.(11).to_ast).to eql([:failure, [:email, [:predicate, [:str?, [[:input, 11]]]]]])
26
+ end
27
+ end
28
+
29
+ context 'with a result' do
30
+ it 'returns success ast' do
31
+ expect(rule.('foo').to_ast).to eql([:predicate, [:str?, [[:input, 'foo']]]])
32
+ end
33
+
34
+ it 'returns ast' do
35
+ expect(rule.(5).to_ast).to eql([:predicate, [:str?, [[:input, 5]]]])
36
+ end
37
+ end
38
+
39
+ context 'with a zero-arity predicate' do
40
+ let(:predicate) { Module.new { def self.test?; true; end }.method(:test?) }
41
+
42
+ it 'returns ast' do
43
+ expect(rule.to_ast).to eql([:predicate, [:test?, []]])
44
+ end
45
+ end
46
+ end
47
+
48
+ describe '#to_s' do
49
+ it 'returns string representation' do
50
+ expect(rule.curry('foo').to_s).to eql('str?("foo")')
51
+ end
52
+ end
53
+ end
@@ -11,117 +11,117 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
11
11
  one: predicate }
12
12
  }
13
13
 
14
- let(:predicate) { double(:predicate).as_null_object }
15
-
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]) }
21
- let(:and_rule) { key_rule & val_rule }
22
- let(:or_rule) { key_rule | val_rule }
23
- let(:xor_rule) { key_rule ^ val_rule }
24
- let(:set_rule) { Rule::Set.new([val_rule]) }
25
- let(:each_rule) { Rule::Each.new(val_rule) }
14
+ let(:predicate) { double(:predicate, name: :test?, arity: 2).as_null_object }
15
+
16
+ let(:rule) { Rule::Predicate.new(predicate) }
17
+ let(:key_op) { Operations::Key.new(rule, name: :email) }
18
+ let(:attr_op) { Operations::Attr.new(rule, name: :email) }
19
+ let(:check_op) { Operations::Check.new(rule, keys: [:email]) }
20
+ let(:not_key_op) { Operations::Negation.new(key_op) }
21
+ let(:and_op) { key_op.curry(:email) & rule }
22
+ let(:or_op) { key_op.curry(:email) | rule }
23
+ let(:xor_op) { key_op.curry(:email) ^ rule }
24
+ let(:set_op) { Operations::Set.new(rule) }
25
+ let(:each_op) { Operations::Each.new(rule) }
26
26
 
27
27
  it 'compiles key rules' do
28
- ast = [[:key, [:email, [:predicate, [:filled?, [[:input, nil]]]]]]]
28
+ ast = [[:key, [:email, [:predicate, [:filled?, [[:input, Undefined]]]]]]]
29
29
 
30
30
  rules = compiler.(ast)
31
31
 
32
- expect(rules).to eql([key_rule])
32
+ expect(rules).to eql([key_op])
33
33
  end
34
34
 
35
35
  it 'compiles attr rules' do
36
- ast = [[:attr, [:email, [:predicate, [:filled?, [[:input, nil]]]]]]]
36
+ ast = [[:attr, [:email, [:predicate, [:filled?, [[:input, Undefined]]]]]]]
37
37
 
38
38
  rules = compiler.(ast)
39
39
 
40
- expect(rules).to eql([attr_rule])
40
+ expect(rules).to eql([attr_op])
41
41
  end
42
42
 
43
43
  it 'compiles check rules' do
44
- ast = [[:check, [:email, [:predicate, [:filled?, [[:input, nil]]]]]]]
44
+ ast = [[:check, [[:email], [:predicate, [:filled?, [[:input, Undefined]]]]]]]
45
45
 
46
46
  rules = compiler.(ast)
47
47
 
48
- expect(rules).to eql([check_rule])
48
+ expect(rules).to eql([check_op])
49
49
  end
50
50
 
51
51
  it 'compiles attr rules' do
52
- ast = [[:attr, [:email, [:predicate, [:attr?, [[:name, :email]]]]]]]
52
+ ast = [[:attr, [:email, [:predicate, [:filled?, [[:input, Undefined]]]]]]]
53
53
 
54
54
  rules = compiler.(ast)
55
55
 
56
- expect(rules).to eql([attr_rule])
56
+ expect(rules).to eql([attr_op])
57
57
  end
58
58
 
59
59
  it 'compiles negated rules' do
60
- ast = [[:not, [:key, [:email, [:predicate, [:filled?, [[:input, nil]]]]]]]]
60
+ ast = [[:not, [:key, [:email, [:predicate, [:filled?, [[:input, Undefined]]]]]]]]
61
61
 
62
62
  rules = compiler.(ast)
63
63
 
64
- expect(rules).to eql([not_key_rule])
64
+ expect(rules).to eql([not_key_op])
65
65
  end
66
66
 
67
- it 'compiles conjunction rules' do
67
+ it 'compiles and rules' do
68
68
  ast = [
69
69
  [
70
70
  :and, [
71
- [:key, [:email, [:predicate, [:key?, [[:name, :email], [:input, nil]]]]]],
72
- [:val, [:predicate, [:filled?, [[:input, nil]]]]]
71
+ [:key, [:email, [:predicate, [:key?, [[:name, :email], [:input, Undefined]]]]]],
72
+ [:predicate, [:filled?, [[:input, Undefined]]]]
73
73
  ]
74
74
  ]
75
75
  ]
76
76
 
77
77
  rules = compiler.(ast)
78
78
 
79
- expect(rules).to eql([and_rule])
79
+ expect(rules).to eql([and_op])
80
80
  end
81
81
 
82
- it 'compiles disjunction rules' do
82
+ it 'compiles or rules' do
83
83
  ast = [
84
84
  [
85
85
  :or, [
86
- [:key, [:email, [:predicate, [:key?, [[:name, :email], [:input, nil]]]]]],
87
- [:val, [:predicate, [:filled?, [[:input, nil]]]]]
86
+ [:key, [:email, [:predicate, [:key?, [[:name, :email], [:input, Undefined]]]]]],
87
+ [:predicate, [:filled?, [[:input, Undefined]]]]
88
88
  ]
89
89
  ]
90
90
  ]
91
91
 
92
92
  rules = compiler.(ast)
93
93
 
94
- expect(rules).to eql([or_rule])
94
+ expect(rules).to eql([or_op])
95
95
  end
96
96
 
97
- it 'compiles exclusive disjunction rules' do
97
+ it 'compiles exclusive or rules' do
98
98
  ast = [
99
99
  [
100
100
  :xor, [
101
- [:key, [:email, [:predicate, [:key?, [[:name, :email], [:input, nil]]]]]],
102
- [:val, [:predicate, [:filled?, [[:input, nil]]]]]
101
+ [:key, [:email, [:predicate, [:key?, [[:name, :email], [:input, Undefined]]]]]],
102
+ [:predicate, [:filled?, [[:input, Undefined]]]]
103
103
  ]
104
104
  ]
105
105
  ]
106
106
 
107
107
  rules = compiler.(ast)
108
108
 
109
- expect(rules).to eql([xor_rule])
109
+ expect(rules).to eql([xor_op])
110
110
  end
111
111
 
112
112
  it 'compiles set rules' do
113
- ast = [[:set, [[:val, [:predicate, [:filled?, [[:input, nil]]]]]]]]
113
+ ast = [[:set, [[:predicate, [:filled?, [[:input, nil]]]]]]]
114
114
 
115
115
  rules = compiler.(ast)
116
116
 
117
- expect(rules).to eql([set_rule])
117
+ expect(rules).to eql([set_op])
118
118
  end
119
119
 
120
120
  it 'compiles each rules' do
121
- ast = [[:each, [:val, [:predicate, [:filled?, [[:input, nil]]]]]]]
121
+ ast = [[:each, [:predicate, [:filled?, [[:input, nil]]]]]]
122
122
 
123
123
  rules = compiler.(ast)
124
124
 
125
- expect(rules).to eql([each_rule])
125
+ expect(rules).to eql([each_op])
126
126
  end
127
127
  end