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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -5
- data/CHANGELOG.md +28 -0
- data/Gemfile +7 -4
- data/dry-logic.gemspec +1 -0
- data/lib/dry/logic.rb +2 -3
- data/lib/dry/logic/appliable.rb +33 -0
- data/lib/dry/logic/evaluator.rb +2 -0
- data/lib/dry/logic/operations.rb +13 -0
- data/lib/dry/logic/operations/abstract.rb +44 -0
- data/lib/dry/logic/operations/and.rb +35 -0
- data/lib/dry/logic/operations/attr.rb +17 -0
- data/lib/dry/logic/operations/binary.rb +26 -0
- data/lib/dry/logic/operations/check.rb +52 -0
- data/lib/dry/logic/operations/each.rb +32 -0
- data/lib/dry/logic/operations/implication.rb +37 -0
- data/lib/dry/logic/operations/key.rb +66 -0
- data/lib/dry/logic/operations/negation.rb +18 -0
- data/lib/dry/logic/operations/or.rb +35 -0
- data/lib/dry/logic/operations/set.rb +35 -0
- data/lib/dry/logic/operations/unary.rb +24 -0
- data/lib/dry/logic/operations/xor.rb +27 -0
- data/lib/dry/logic/operators.rb +25 -0
- data/lib/dry/logic/predicates.rb +143 -136
- data/lib/dry/logic/result.rb +76 -33
- data/lib/dry/logic/rule.rb +62 -46
- data/lib/dry/logic/rule/predicate.rb +28 -0
- data/lib/dry/logic/rule_compiler.rb +16 -17
- data/lib/dry/logic/version.rb +1 -1
- data/spec/integration/result_spec.rb +59 -0
- data/spec/integration/rule_spec.rb +53 -0
- data/spec/shared/predicates.rb +6 -0
- data/spec/shared/rule.rb +67 -0
- data/spec/spec_helper.rb +10 -3
- data/spec/support/mutant.rb +9 -0
- data/spec/unit/operations/and_spec.rb +64 -0
- data/spec/unit/operations/attr_spec.rb +27 -0
- data/spec/unit/operations/check_spec.rb +49 -0
- data/spec/unit/operations/each_spec.rb +47 -0
- data/spec/unit/operations/implication_spec.rb +30 -0
- data/spec/unit/operations/key_spec.rb +119 -0
- data/spec/unit/operations/negation_spec.rb +40 -0
- data/spec/unit/operations/or_spec.rb +73 -0
- data/spec/unit/operations/set_spec.rb +41 -0
- data/spec/unit/operations/xor_spec.rb +61 -0
- data/spec/unit/predicates_spec.rb +23 -0
- data/spec/unit/rule/predicate_spec.rb +53 -0
- data/spec/unit/rule_compiler_spec.rb +38 -38
- data/spec/unit/rule_spec.rb +94 -0
- metadata +67 -40
- data/lib/dry/logic/predicate.rb +0 -100
- data/lib/dry/logic/predicate_set.rb +0 -23
- data/lib/dry/logic/result/each.rb +0 -20
- data/lib/dry/logic/result/multi.rb +0 -14
- data/lib/dry/logic/result/named.rb +0 -17
- data/lib/dry/logic/result/set.rb +0 -10
- data/lib/dry/logic/result/value.rb +0 -17
- data/lib/dry/logic/rule/attr.rb +0 -13
- data/lib/dry/logic/rule/check.rb +0 -40
- data/lib/dry/logic/rule/composite.rb +0 -91
- data/lib/dry/logic/rule/each.rb +0 -13
- data/lib/dry/logic/rule/key.rb +0 -37
- data/lib/dry/logic/rule/negation.rb +0 -15
- data/lib/dry/logic/rule/set.rb +0 -31
- data/lib/dry/logic/rule/value.rb +0 -48
- data/spec/unit/predicate_spec.rb +0 -115
- data/spec/unit/rule/attr_spec.rb +0 -29
- data/spec/unit/rule/check_spec.rb +0 -44
- data/spec/unit/rule/conjunction_spec.rb +0 -30
- data/spec/unit/rule/disjunction_spec.rb +0 -38
- data/spec/unit/rule/each_spec.rb +0 -31
- data/spec/unit/rule/exclusive_disjunction_spec.rb +0 -19
- data/spec/unit/rule/implication_spec.rb +0 -16
- data/spec/unit/rule/key_spec.rb +0 -121
- data/spec/unit/rule/set_spec.rb +0 -30
- data/spec/unit/rule/value_spec.rb +0 -99
data/lib/dry/logic/rule/attr.rb
DELETED
data/lib/dry/logic/rule/check.rb
DELETED
@@ -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
|
data/lib/dry/logic/rule/each.rb
DELETED
data/lib/dry/logic/rule/key.rb
DELETED
@@ -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
|
data/lib/dry/logic/rule/set.rb
DELETED
@@ -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
|
data/lib/dry/logic/rule/value.rb
DELETED
@@ -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
|
data/spec/unit/predicate_spec.rb
DELETED
@@ -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
|
data/spec/unit/rule/attr_spec.rb
DELETED
@@ -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
|