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