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