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