dry-logic 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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,53 @@
1
+ require 'dry-logic'
2
+
3
+ RSpec.describe 'Rules' do
4
+ specify 'defining an anonymous rule with an arbitrary predicate' do
5
+ rule = Dry::Logic.Rule { |value| value.is_a?(Integer) }
6
+
7
+ expect(rule.(1)).to be_success
8
+ expect(rule[1]).to be(true)
9
+ end
10
+
11
+ specify 'defining a conjunction' do
12
+ rule = Dry::Logic.Rule(&:even?) & Dry::Logic.Rule { |v| v > 4 }
13
+
14
+ expect(rule.(3)).to be_failure
15
+ expect(rule.(4)).to be_failure
16
+ expect(rule.(5)).to be_failure
17
+ expect(rule.(6)).to be_success
18
+ end
19
+
20
+ specify 'defining a disjunction' do
21
+ rule = Dry::Logic.Rule { |v| v < 4 } | Dry::Logic.Rule { |v| v > 6 }
22
+
23
+ expect(rule.(5)).to be_failure
24
+ expect(rule.(3)).to be_success
25
+ expect(rule.(7)).to be_success
26
+ end
27
+
28
+ specify 'defining an implication' do
29
+ rule = Dry::Logic.Rule(&:empty?) > Dry::Logic.Rule { |v| v.is_a?(Array) }
30
+
31
+ expect(rule.('foo')).to be_success
32
+ expect(rule.([1, 2])).to be_success
33
+ expect(rule.([])).to be_success
34
+ expect(rule.('')).to be_failure
35
+ end
36
+
37
+ specify 'defining an exclusive disjunction' do
38
+ rule = Dry::Logic.Rule(&:empty?) ^ Dry::Logic.Rule { |v| v.is_a?(Array) }
39
+
40
+ expect(rule.('foo')).to be_failure
41
+ expect(rule.([])).to be_failure
42
+ expect(rule.([1, 2])).to be_success
43
+ expect(rule.('')).to be_success
44
+ end
45
+
46
+ specify 'defining a rule with options' do
47
+ rule = Dry::Logic::Rule(id: :empty?) { |value| value.empty? }
48
+
49
+ expect(rule.('foo')).to be_failure
50
+ expect(rule.('')).to be_success
51
+ expect(rule.ast('foo')).to eql([:predicate, [:empty?, [[:value, 'foo']]]])
52
+ end
53
+ end
@@ -3,6 +3,10 @@ require 'dry/logic/predicates'
3
3
  RSpec.shared_examples 'predicates' do
4
4
  let(:none?) { Dry::Logic::Predicates[:none?] }
5
5
 
6
+ let(:array?) { Dry::Logic::Predicates[:array?] }
7
+
8
+ let(:empty?) { Dry::Logic::Predicates[:empty?] }
9
+
6
10
  let(:str?) { Dry::Logic::Predicates[:str?] }
7
11
 
8
12
  let(:true?) { Dry::Logic::Predicates[:true?] }
@@ -24,6 +28,8 @@ RSpec.shared_examples 'predicates' do
24
28
  let(:attr?) { Dry::Logic::Predicates[:attr?] }
25
29
 
26
30
  let(:eql?) { Dry::Logic::Predicates[:eql?] }
31
+
32
+ let(:size?) { Dry::Logic::Predicates[:size?] }
27
33
  end
28
34
 
29
35
  RSpec.shared_examples 'a passing predicate' do
@@ -0,0 +1,67 @@
1
+ shared_examples_for Dry::Logic::Rule do
2
+ let(:predicate) { double(:predicate, arity: 2, name: predicate_name) }
3
+ let(:rule_type) { described_class }
4
+ let(:predicate_name) { :good? }
5
+
6
+ describe '#arity' do
7
+ it 'returns its predicate arity' do
8
+ rule = rule_type.new(predicate)
9
+
10
+ expect(rule.arity).to be(2)
11
+ end
12
+ end
13
+
14
+ describe '#parameters' do
15
+ it 'returns a list of args with their names' do
16
+ rule = rule_type.new(-> foo, bar { true }, args: [312])
17
+
18
+ expect(rule.parameters).to eql([[:req, :foo], [:req, :bar]])
19
+ end
20
+ end
21
+
22
+ describe '#call' do
23
+ it 'returns success for valid input' do
24
+ rule = rule_type.new(predicate)
25
+
26
+ expect(predicate).to receive(:[]).with(2).and_return(true)
27
+
28
+ expect(rule.(2)).to be_success
29
+ end
30
+
31
+ it 'returns failure for invalid input' do
32
+ rule = rule_type.new(predicate)
33
+
34
+ expect(predicate).to receive(:[]).with(2).and_return(false)
35
+
36
+ expect(rule.(2)).to be_failure
37
+ end
38
+ end
39
+
40
+ describe '#[]' do
41
+ it 'delegates to its predicate' do
42
+ rule = rule_type.new(predicate)
43
+
44
+ expect(predicate).to receive(:[]).with(2).and_return(true)
45
+ expect(rule[2]).to be(true)
46
+ end
47
+ end
48
+
49
+ describe '#curry' do
50
+ it 'returns a curried rule' do
51
+ rule = rule_type.new(predicate).curry(3)
52
+
53
+ expect(predicate).to receive(:[]).with(3, 2).and_return(true)
54
+ expect(rule.args).to eql([3])
55
+
56
+ expect(rule.(2)).to be_success
57
+ end
58
+
59
+ it 'raises argument error when arity does not match' do
60
+ expect(predicate).to receive(:arity).and_return(2)
61
+
62
+ expect { rule_type.new(predicate).curry(3, 2, 1) }.to raise_error(
63
+ ArgumentError, 'wrong number of arguments (3 for 2)'
64
+ )
65
+ end
66
+ end
67
+ end
@@ -1,6 +1,11 @@
1
- if RUBY_ENGINE == "rbx"
2
- require "codeclimate-test-reporter"
3
- CodeClimate::TestReporter.start
1
+ if RUBY_ENGINE == 'ruby' && RUBY_VERSION >= '2.3.1'
2
+ require "codeclimate-test-reporter"
3
+ CodeClimate::TestReporter.start
4
+ end
5
+
6
+ if ENV['COVERAGE']
7
+ require 'simplecov'
8
+ SimpleCov.start
4
9
  end
5
10
 
6
11
  begin
@@ -8,6 +13,7 @@ begin
8
13
  rescue LoadError; end
9
14
 
10
15
  require 'dry-logic'
16
+ require 'dry/core/constants'
11
17
  require 'pathname'
12
18
 
13
19
  SPEC_ROOT = Pathname(__dir__)
@@ -16,6 +22,7 @@ Dir[SPEC_ROOT.join('shared/**/*.rb')].each(&method(:require))
16
22
  Dir[SPEC_ROOT.join('support/**/*.rb')].each(&method(:require))
17
23
 
18
24
  include Dry::Logic
25
+ include Dry::Core::Constants
19
26
 
20
27
  RSpec.configure do |config|
21
28
  config.disable_monkey_patching!
@@ -0,0 +1,9 @@
1
+ module Mutant
2
+ class Selector
3
+ class Expression < self
4
+ def call(_subject)
5
+ integration.all_tests
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,64 @@
1
+ RSpec.describe Operations::And do
2
+ subject(:operation) { Operations::And.new(left, right) }
3
+
4
+ include_context 'predicates'
5
+
6
+ let(:left) { Rule::Predicate.new(int?) }
7
+ let(:right) { Rule::Predicate.new(gt?).curry(18) }
8
+
9
+ describe '#call' do
10
+ it 'calls left and right' do
11
+ expect(operation.(18)).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
+ [:and, [[:predicate, [:int?, [[:input, Undefined]]]], [:predicate, [:gt?, [[:num, 18], [:input, Undefined]]]]]]
19
+ )
20
+ end
21
+
22
+ it 'returns result ast' do
23
+ expect(operation.('18').to_ast).to eql(
24
+ [:and, [[:predicate, [:int?, [[:input, '18']]]], [:hint, [:predicate, [:gt?, [[:num, 18], [:input, '18']]]]]]]
25
+ )
26
+
27
+ expect(operation.(18).to_ast).to eql(
28
+ [:predicate, [:gt?, [[:num, 18], [:input, 18]]]]
29
+ )
30
+ end
31
+
32
+ it 'returns failure result ast' do
33
+ expect(operation.with(id: :age).('18').to_ast).to eql(
34
+ [:failure, [:age, [:and, [[:predicate, [:int?, [[:input, '18']]]], [:hint, [:predicate, [:gt?, [[:num, 18], [:input, '18']]]]]]]]]
35
+ )
36
+
37
+ expect(operation.with(id: :age).(18).to_ast).to eql(
38
+ [:failure, [:age, [:predicate, [:gt?, [[:num, 18], [:input, 18]]]]]]
39
+ )
40
+ end
41
+ end
42
+
43
+ describe '#and' do
44
+ let(:other) { Rule::Predicate.new(lt?).curry(30) }
45
+
46
+ it 'creates and with the other' do
47
+ expect(operation.and(other).(31)).to be_failure
48
+ end
49
+ end
50
+
51
+ describe '#or' do
52
+ let(:other) { Rule::Predicate.new(lt?).curry(14) }
53
+
54
+ it 'creates or with the other' do
55
+ expect(operation.or(other).(13)).to be_success
56
+ end
57
+ end
58
+
59
+ describe '#to_s' do
60
+ it 'returns string representation' do
61
+ expect(operation.to_s).to eql('int? AND gt?(18)')
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,27 @@
1
+ RSpec.describe Operations::Attr do
2
+ subject(:operation) { Operations::Attr.new(Rule::Predicate.new(str?), name: :name) }
3
+
4
+ include_context 'predicates'
5
+
6
+ let(:model) { Struct.new(:name) }
7
+
8
+ describe '#call' do
9
+ it 'applies predicate to the value' do
10
+ expect(operation.(model.new('Jane'))).to be_success
11
+ expect(operation.(model.new(nil))).to be_failure
12
+ end
13
+ end
14
+
15
+ describe '#and' do
16
+ let(:other) { Operations::Attr.new(Rule::Predicate.new(min_size?).curry(3), name: :name) }
17
+
18
+ it 'returns and where value is passed to the right' do
19
+ present_and_string = operation.and(other)
20
+
21
+ expect(present_and_string.(model.new('Jane'))).to be_success
22
+
23
+ expect(present_and_string.(model.new('Ja'))).to be_failure
24
+ expect(present_and_string.(model.new(1))).to be_failure
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ RSpec.describe Operations::Check do
2
+ include_context 'predicates'
3
+
4
+ describe '#call' do
5
+ context 'with 1-level nesting' do
6
+ subject(:operation) do
7
+ Operations::Check.new(Rule::Predicate.new(eql?).curry(1), id: :compare, keys: [:num])
8
+ end
9
+
10
+ it 'applies predicate to args extracted from the input' do
11
+ expect(operation.(num: 1)).to be_success
12
+ expect(operation.(num: 2)).to be_failure
13
+ end
14
+ end
15
+
16
+ context 'with 2-levels nesting' do
17
+ subject(:operation) do
18
+ Operations::Check.new(Rule::Predicate.new(eql?), id: :compare, keys: [[:nums, :left], [:nums, :right]])
19
+ end
20
+
21
+ it 'applies predicate to args extracted from the input' do
22
+ expect(operation.(nums: { left: 1, right: 1 })).to be_success
23
+ expect(operation.(nums: { left: 1, right: 2 })).to be_failure
24
+ end
25
+
26
+ it 'curries args properly' do
27
+ result = operation.(nums: { left: 1, right: 2 })
28
+
29
+ expect(result.to_ast).to eql(
30
+ [:failure, [:compare, [:check, [
31
+ [[:nums, :left], [:nums, :right]], [:predicate, [:eql?, [[:left, 1], [:right, 2]]]]]
32
+ ]]]
33
+ )
34
+ end
35
+ end
36
+ end
37
+
38
+ describe '#to_ast' do
39
+ subject(:operation) do
40
+ Operations::Check.new(Rule::Predicate.new(str?), keys: [:email])
41
+ end
42
+
43
+ it 'returns ast' do
44
+ expect(operation.to_ast).to eql(
45
+ [:check, [[:email], [:predicate, [:str?, [[:input, Undefined]]]]]]
46
+ )
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,47 @@
1
+ RSpec.describe Operations::Each do
2
+ subject(:operation) { Operations::Each.new(is_string) }
3
+
4
+ include_context 'predicates'
5
+
6
+ let(:is_string) { Rule::Predicate.new(str?) }
7
+
8
+ describe '#call' do
9
+ it 'applies its rules to all elements in the input' do
10
+ expect(operation.(['Address'])).to be_success
11
+
12
+ expect(operation.([nil, 'Address'])).to be_failure
13
+ expect(operation.([:Address, 'Address'])).to be_failure
14
+ end
15
+ end
16
+
17
+ describe '#to_ast' do
18
+ it 'returns ast' do
19
+ expect(operation.to_ast).to eql([:each, [:predicate, [:str?, [[:input, Undefined]]]]])
20
+ end
21
+
22
+ it 'returns result ast' do
23
+ expect(operation.([nil, 12, nil]).to_ast).to eql(
24
+ [:set, [
25
+ [:key, [0, [:predicate, [:str?, [[:input, nil]]]]]],
26
+ [:key, [1, [:predicate, [:str?, [[:input, 12]]]]]],
27
+ [:key, [2, [:predicate, [:str?, [[:input, nil]]]]]]
28
+ ]]
29
+ )
30
+ end
31
+
32
+ it 'returns failure result ast' do
33
+ expect(operation.with(id: :tags).([nil, 'red', 12]).to_ast).to eql(
34
+ [:failure, [:tags, [:set, [
35
+ [:key, [0, [:predicate, [:str?, [[:input, nil]]]]]],
36
+ [:key, [2, [:predicate, [:str?, [[:input, 12]]]]]]
37
+ ]]]]
38
+ )
39
+ end
40
+ end
41
+
42
+ describe '#to_s' do
43
+ it 'returns string representation' do
44
+ expect(operation.to_s).to eql('each(str?)')
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,30 @@
1
+ RSpec.describe Operations::Implication do
2
+ subject(:operation) { Operations::Implication.new(left, right) }
3
+
4
+ include_context 'predicates'
5
+
6
+ let(:left) { Rule::Predicate.new(int?) }
7
+ let(:right) { Rule::Predicate.new(gt?).curry(18) }
8
+
9
+ describe '#call' do
10
+ it 'calls left and right' do
11
+ expect(operation.('19')).to be_success
12
+ expect(operation.(19)).to be_success
13
+ expect(operation.(18)).to be_failure
14
+ end
15
+ end
16
+
17
+ describe '#to_ast' do
18
+ it 'returns ast' do
19
+ expect(operation.to_ast).to eql(
20
+ [:implication, [[:predicate, [:int?, [[:input, Undefined]]]], [:predicate, [:gt?, [[:num, 18], [:input, Undefined]]]]]]
21
+ )
22
+ end
23
+ end
24
+
25
+ describe '#to_s' do
26
+ it 'returns string representation' do
27
+ expect(operation.to_s).to eql('int? THEN gt?(18)')
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,119 @@
1
+ RSpec.describe Operations::Key do
2
+ subject(:operation) { Operations::Key.new(predicate, name: :user) }
3
+
4
+ include_context 'predicates'
5
+
6
+ let(:predicate) do
7
+ Rule::Predicate.new(key?).curry(:age)
8
+ end
9
+
10
+ describe '#call' do
11
+ context 'with a plain predicate' do
12
+ it 'returns a success for valid input' do
13
+ expect(operation.(user: { age: 18 })).to be_success
14
+ end
15
+
16
+ it 'returns a failure for invalid input' do
17
+ result = operation.(user: {})
18
+
19
+ expect(result).to be_failure
20
+
21
+ expect(result.to_ast).to eql(
22
+ [:failure, [:user, [:key, [:user,
23
+ [:predicate, [:key?, [[:name, :age], [:input, {}]]]]
24
+ ]]]]
25
+ )
26
+ end
27
+ end
28
+
29
+ context 'with a set rule as predicate' do
30
+ subject(:operation) do
31
+ Operations::Key.new(predicate, name: :address)
32
+ end
33
+
34
+ let(:predicate) do
35
+ Operations::Set.new(Rule::Predicate.new(key?).curry(:city), Rule::Predicate.new(key?).curry(:zipcode))
36
+ end
37
+
38
+ it 'applies set rule to the value that passes' do
39
+ result = operation.(address: { city: 'NYC', zipcode: '123' })
40
+
41
+ expect(result).to be_success
42
+ end
43
+
44
+ it 'applies set rule to the value that fails' do
45
+ result = operation.(address: { city: 'NYC' })
46
+
47
+ expect(result).to be_failure
48
+
49
+ expect(result.to_ast).to eql(
50
+ [:failure, [:address, [:key, [:address, [:set, [
51
+ [:predicate, [:key?, [[:name, :zipcode], [:input, { city: 'NYC' }]]]]
52
+ ]]]]]]
53
+ )
54
+ end
55
+ end
56
+
57
+ context 'with an each rule as predicate' do
58
+ subject(:operation) do
59
+ Operations::Key.new(predicate, name: :nums)
60
+ end
61
+
62
+ let(:predicate) do
63
+ Operations::Each.new(Rule::Predicate.new(str?))
64
+ end
65
+
66
+ it 'applies each rule to the value that passses' do
67
+ result = operation.(nums: %w(1 2 3))
68
+
69
+ expect(result).to be_success
70
+ end
71
+
72
+ it 'applies each rule to the value that fails' do
73
+ failure = operation.(nums: [1, '3', 3])
74
+
75
+ expect(failure).to be_failure
76
+
77
+ expect(failure.to_ast).to eql(
78
+ [:failure, [:nums, [:key, [:nums, [:set, [
79
+ [:key, [0, [:predicate, [:str?, [[:input, 1]]]]]],
80
+ [:key, [2, [:predicate, [:str?, [[:input, 3]]]]]]
81
+ ]]]]]]
82
+ )
83
+ end
84
+ end
85
+ end
86
+
87
+ describe '#to_ast' do
88
+ it 'returns ast' do
89
+ expect(operation.to_ast).to eql(
90
+ [:key, [:user, [:predicate, [:key?, [[:name, :age], [:input, Undefined]]]]]]
91
+ )
92
+ end
93
+ end
94
+
95
+ describe '#and' do
96
+ subject(:operation) do
97
+ Operations::Key.new(Rule::Predicate.new(str?), name: [:user, :name])
98
+ end
99
+
100
+ let(:other) do
101
+ Operations::Key.new(Rule::Predicate.new(filled?), name: [:user, :name])
102
+ end
103
+
104
+ it 'returns and rule where value is passed to the right' do
105
+ present_and_string = operation.and(other)
106
+
107
+ expect(present_and_string.(user: { name: 'Jane' })).to be_success
108
+
109
+ expect(present_and_string.(user: {})).to be_failure
110
+ expect(present_and_string.(user: { name: 1 })).to be_failure
111
+ end
112
+ end
113
+
114
+ describe '#to_s' do
115
+ it 'returns string representation' do
116
+ expect(operation.to_s).to eql('key[user](key?(:age))')
117
+ end
118
+ end
119
+ end