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
@@ -1,13 +0,0 @@
1
- module Dry
2
- module Logic
3
- class Rule::Attr < Rule::Key
4
- def self.evaluator(name)
5
- Evaluator::Attr.new(name)
6
- end
7
-
8
- def type
9
- :attr
10
- end
11
- end
12
- end
13
- end
@@ -1,40 +0,0 @@
1
- require 'dry/logic/evaluator'
2
-
3
- module Dry
4
- module Logic
5
- class Rule::Check < Rule::Value
6
- attr_reader :name, :evaluator
7
-
8
- def self.new(predicate, options)
9
- keys = options.fetch(:keys)
10
- evaluator = Evaluator::Set.new(keys)
11
-
12
- super(predicate, options.merge(evaluator: evaluator))
13
- end
14
-
15
- def initialize(predicate, options)
16
- super
17
- @name = options.fetch(:name)
18
- @evaluator = options[:evaluator]
19
- end
20
-
21
- def call(input)
22
- args = evaluator[input].reverse
23
- *head, tail = args
24
- Logic.Result(predicate.curry(*head).(tail), curry(*args), input)
25
- end
26
-
27
- def evaluate(input)
28
- evaluator[input].first
29
- end
30
-
31
- def type
32
- :check
33
- end
34
-
35
- def to_ast
36
- [type, [name, predicate.to_ast]]
37
- end
38
- end
39
- end
40
- end
@@ -1,91 +0,0 @@
1
- module Dry
2
- module Logic
3
- class Rule::Composite < Rule
4
- include Dry::Equalizer(:left, :right)
5
-
6
- attr_reader :left, :right
7
-
8
- def initialize(left, right)
9
- @left = left
10
- @right = right
11
- end
12
-
13
- def arity
14
- -1
15
- end
16
-
17
- def input
18
- Predicate::Undefined
19
- end
20
-
21
- def curry(*args)
22
- self.class.new(left.curry(*args), right.curry(*args))
23
- end
24
-
25
- def name
26
- :"#{left.name}_#{type}_#{right.name}"
27
- end
28
-
29
- def to_ast
30
- [type, [left.to_ast, right.to_ast]]
31
- end
32
- alias_method :to_a, :to_ast
33
- end
34
-
35
- class Rule::Implication < Rule::Composite
36
- def call(input)
37
- if left.(input).success?
38
- right.(input)
39
- else
40
- Logic.Result(true, left, input)
41
- end
42
- end
43
-
44
- def type
45
- :implication
46
- end
47
- end
48
-
49
- class Rule::Conjunction < Rule::Composite
50
- def call(input)
51
- result = left.(input)
52
-
53
- if result.success?
54
- right.(input)
55
- else
56
- result
57
- end
58
- end
59
-
60
- def type
61
- :and
62
- end
63
- end
64
-
65
- class Rule::Disjunction < Rule::Composite
66
- def call(input)
67
- result = left.(input)
68
-
69
- if result.success?
70
- result
71
- else
72
- right.(input)
73
- end
74
- end
75
-
76
- def type
77
- :or
78
- end
79
- end
80
-
81
- class Rule::ExclusiveDisjunction < Rule::Composite
82
- def call(input)
83
- Logic.Result(left.(input).success? ^ right.(input).success?, self, input)
84
- end
85
-
86
- def type
87
- :xor
88
- end
89
- end
90
- end
91
- end
@@ -1,13 +0,0 @@
1
- module Dry
2
- module Logic
3
- class Rule::Each < Rule::Value
4
- def apply(input)
5
- Hash[input.map.with_index { |element, index| [index, predicate.(element)] }]
6
- end
7
-
8
- def type
9
- :each
10
- end
11
- end
12
- end
13
- end
@@ -1,37 +0,0 @@
1
- require 'dry/logic/evaluator'
2
-
3
- module Dry
4
- module Logic
5
- class Rule::Key < Rule::Value
6
- attr_reader :name, :evaluator
7
-
8
- def self.new(predicate, options)
9
- name = options.fetch(:name)
10
- eval = options.fetch(:evaluator, evaluator(name))
11
- super(predicate, evaluator: eval, name: name)
12
- end
13
-
14
- def self.evaluator(name)
15
- Evaluator::Key.new(name)
16
- end
17
-
18
- def initialize(predicate, options)
19
- super
20
- @name = options[:name]
21
- @evaluator = options[:evaluator]
22
- end
23
-
24
- def evaluate(input)
25
- evaluator[input]
26
- end
27
-
28
- def type
29
- :key
30
- end
31
-
32
- def to_ast
33
- [type, [name, predicate.to_ast]]
34
- end
35
- end
36
- end
37
- end
@@ -1,15 +0,0 @@
1
- module Dry
2
- module Logic
3
- class Rule
4
- class Negation < Rule::Value
5
- def type
6
- :not
7
- end
8
-
9
- def call(input)
10
- predicate.(input).negated
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,31 +0,0 @@
1
- module Dry
2
- module Logic
3
- class Rule::Set < Rule::Value
4
- alias_method :rules, :predicate
5
-
6
- def type
7
- :set
8
- end
9
-
10
- def arity
11
- -1
12
- end
13
-
14
- def apply(input)
15
- rules.map { |rule| rule.(input) }
16
- end
17
-
18
- def curry(*args)
19
- new(rules.map { |r| r.curry(*args) })
20
- end
21
-
22
- def at(*args)
23
- new(rules.values_at(*args))
24
- end
25
-
26
- def to_ast
27
- [type, rules.map { |rule| rule.to_ast }]
28
- end
29
- end
30
- end
31
- end
@@ -1,48 +0,0 @@
1
- module Dry
2
- module Logic
3
- class Rule::Value < Rule
4
- def type
5
- :val
6
- end
7
-
8
- def nulary?
9
- arity == 0
10
- end
11
-
12
- def arity
13
- @arity ||= predicate.arity
14
- end
15
-
16
- def args
17
- @args ||= predicate.args
18
- end
19
-
20
- def input
21
- predicate.args.last
22
- end
23
-
24
- def call(input)
25
- if nulary?
26
- Logic.Result(predicate.(), self, input)
27
- else
28
- evaled = evaluate(input)
29
- result = apply(evaled)
30
- rule = result == true ? self : curry(evaled)
31
- Logic.Result(result, rule, input)
32
- end
33
- end
34
-
35
- def apply(input)
36
- predicate.(input)
37
- end
38
-
39
- def evaluate(input)
40
- input
41
- end
42
-
43
- def to_ast
44
- [type, predicate.to_ast]
45
- end
46
- end
47
- end
48
- end
@@ -1,115 +0,0 @@
1
- require 'dry/logic/predicate'
2
-
3
- RSpec.describe Predicate do
4
- describe '.new' do
5
- it 'can be initialized with empty args' do
6
- predicate = Predicate.new(:id) { |v| v.is_a?(Integer) }
7
-
8
- expect(predicate.to_ast).to eql([:predicate, [:id, [[:v, Predicate::Undefined]]]])
9
- end
10
-
11
- it 'can be initialized with args' do
12
- predicate = Predicate.new(:id, args: [1]) { |v| v.is_a?(Integer) }
13
-
14
- expect(predicate.to_ast).to eql([:predicate, [:id, [[:v, 1]]]])
15
- end
16
-
17
- it 'can be initialized with fn as the last arg' do
18
- predicate = Predicate.new(:id, args: [1], fn: -> v { v.is_a?(Integer) })
19
-
20
- expect(predicate.to_ast).to eql([:predicate, [:id, [[:v, 1]]]])
21
- end
22
- end
23
-
24
- describe '#bind' do
25
- it 'returns a predicate bound to a specific object' do
26
- fn = String.instance_method(:empty?)
27
-
28
- expect(Dry::Logic.Predicate(fn).bind("").()).to be(true)
29
- expect(Dry::Logic.Predicate(fn).bind("foo").()).to be(false)
30
- end
31
- end
32
-
33
- describe '#call' do
34
- it 'returns result of the predicate function' do
35
- is_empty = Dry::Logic::Predicate.new(:is_empty) { |str| str.empty? }
36
-
37
- expect(is_empty.('')).to be(true)
38
-
39
- expect(is_empty.('filled')).to be(false)
40
- end
41
-
42
- it "raises argument error when incorrect number of args provided" do
43
- min_age = Dry::Logic::Predicate.new(:min_age) { |age, input| input >= age }
44
-
45
- expect { min_age.curry(10, 12, 14) }.to raise_error(ArgumentError)
46
- expect { min_age.(18, 19, 20, 30) }.to raise_error(ArgumentError)
47
- expect { min_age.curry(18).(19, 20) }.to raise_error(ArgumentError)
48
- end
49
-
50
- it "predicates should work without any args" do
51
- is_empty = Dry::Logic::Predicate.new(:is_empty) { true }
52
-
53
- expect(is_empty.()).to be(true)
54
- end
55
- end
56
-
57
- describe '#arity' do
58
- it 'returns arity of the predicate function' do
59
- is_equal = Dry::Logic::Predicate.new(:is_equal) { |left, right| left == right }
60
-
61
- expect(is_equal.arity).to eql(2)
62
- end
63
- end
64
-
65
- describe '#parameters' do
66
- it 'returns arity of the predicate function' do
67
- is_equal = Dry::Logic::Predicate.new(:is_equal) { |left, right| left == right }
68
-
69
- expect(is_equal.parameters).to eql([[:opt, :left], [:opt, :right]])
70
- end
71
- end
72
-
73
- describe '#arity' do
74
- it 'returns arity of the predicate function' do
75
- is_equal = Dry::Logic::Predicate.new(:is_equal) { |left, right| left == right }
76
-
77
- expect(is_equal.arity).to eql(2)
78
- end
79
- end
80
-
81
- describe '#parameters' do
82
- it 'returns arity of the predicate function' do
83
- is_equal = Dry::Logic::Predicate.new(:is_equal) { |left, right| left == right }
84
-
85
- expect(is_equal.parameters).to eql([[:opt, :left], [:opt, :right]])
86
- end
87
- end
88
-
89
- describe '#curry' do
90
- it 'returns curried predicate' do
91
- min_age = Dry::Logic::Predicate.new(:min_age) { |age, input| input >= age }
92
-
93
- min_age_18 = min_age.curry(18)
94
-
95
- expect(min_age_18.args).to eql([18])
96
-
97
- expect(min_age_18.(18)).to be(true)
98
- expect(min_age_18.(19)).to be(true)
99
- expect(min_age_18.(17)).to be(false)
100
- end
101
-
102
- it 'can curry again & again' do
103
- min_age = Dry::Logic::Predicate.new(:min_age) { |age, input| input >= age }
104
-
105
- min_age_18 = min_age.curry(18)
106
-
107
- expect(min_age_18.args).to eql([18])
108
-
109
- actual_age_19 = min_age_18.curry(19)
110
-
111
- expect(actual_age_19.()).to be(true)
112
- expect(actual_age_19.args).to eql([18,19])
113
- end
114
- end
115
- end
@@ -1,29 +0,0 @@
1
- require 'dry/logic/rule'
2
-
3
- RSpec.describe Dry::Logic::Rule::Attr do
4
- include_context 'predicates'
5
-
6
- let(:model) { Struct.new(:name) }
7
-
8
- subject(:rule) { described_class.new(str?, name: :name) }
9
-
10
- describe '#call' do
11
- it 'applies predicate to the value' do
12
- expect(rule.(model.new('Jane'))).to be_success
13
- expect(rule.(model.new(nil))).to be_failure
14
- end
15
- end
16
-
17
- describe '#and' do
18
- let(:other) { Dry::Logic::Rule::Attr.new(min_size?.curry(3), name: :name) }
19
-
20
- it 'returns conjunction rule where value is passed to the right' do
21
- present_and_string = rule.and(other)
22
-
23
- expect(present_and_string.(model.new('Jane'))).to be_success
24
-
25
- expect(present_and_string.(model.new('Ja'))).to be_failure
26
- expect(present_and_string.(model.new(1))).to be_failure
27
- end
28
- end
29
- end
@@ -1,44 +0,0 @@
1
- RSpec.describe Rule::Check do
2
- include_context 'predicates'
3
-
4
- describe '#call' do
5
- context 'with 1-level nesting' do
6
- subject(:rule) do
7
- Rule::Check.new(eql?.curry(1), name: :compare, keys: [:num])
8
- end
9
-
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
13
-
14
- expect(rule.(num: 1).to_ast).to eql(
15
- [:input, [:compare, [
16
- :result, [1, [:check, [:compare, [:predicate, [:eql?, [[:left, 1], [:right, 1]]]]]]]]]
17
- ]
18
- )
19
- end
20
- end
21
-
22
- context 'with 2-levels nesting' do
23
- subject(:rule) do
24
- Rule::Check.new(eql?, name: :compare, keys: [[:nums, :left], [:nums, :right]])
25
- end
26
-
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
31
-
32
- #check rules reverse the order of params to enable cases like `left.gt(right)` to work
33
- it 'curries args properly' do
34
- result = rule.(nums: { left: 1, right: 2 })
35
-
36
- expect(result.to_ast).to eql([
37
- :input, [:compare, [
38
- :result, [1, [:check, [:compare, [:predicate, [:eql?, [[:left, 2], [:right, 1]]]]]]]]
39
- ]
40
- ])
41
- end
42
- end
43
- end
44
- end