dry-logic 0.2.3 → 0.3.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/CHANGELOG.md +15 -1
- data/README.md +14 -7
- data/examples/basic.rb +8 -2
- data/lib/dry/logic/predicate.rb +72 -7
- data/lib/dry/logic/predicates.rb +4 -0
- data/lib/dry/logic/result/each.rb +1 -1
- data/lib/dry/logic/result/set.rb +1 -1
- data/lib/dry/logic/result/value.rb +5 -1
- data/lib/dry/logic/rule.rb +12 -1
- data/lib/dry/logic/rule/attr.rb +2 -2
- data/lib/dry/logic/rule/check.rb +1 -1
- data/lib/dry/logic/rule/composite.rb +12 -4
- data/lib/dry/logic/rule/key.rb +4 -3
- data/lib/dry/logic/rule/set.rb +8 -0
- data/lib/dry/logic/rule/value.rb +25 -2
- data/lib/dry/logic/rule_compiler.rb +9 -2
- data/lib/dry/logic/version.rb +1 -1
- data/spec/shared/predicates.rb +4 -4
- data/spec/unit/predicate_spec.rb +89 -1
- data/spec/unit/predicates/array_spec.rb +41 -0
- data/spec/unit/predicates/type_spec.rb +35 -0
- data/spec/unit/rule/check_spec.rb +3 -2
- data/spec/unit/rule/each_spec.rb +2 -2
- data/spec/unit/rule/key_spec.rb +3 -3
- data/spec/unit/rule/set_spec.rb +2 -2
- data/spec/unit/rule/value_spec.rb +33 -2
- data/spec/unit/rule_compiler_spec.rb +13 -13
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ea86d568127098e2777868066c2026dbc380eb9
|
4
|
+
data.tar.gz: 5553735b89ab2ef6a1c5bdd3f24874bab46c2581
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c943b1c67b55618b3e3fa1e982e371614a3e34c0ec2ca7457c775e755335a9579f52434d50545b411bf053f3c8e5a72209bdc86a58d7f13abef521df92b1fde5
|
7
|
+
data.tar.gz: 87598a0a72b1196d966464b1a731746922613fa8eb72cfa3d002ecfe3dee917502126bcfb2e506ae67c958335a6343b05db131e7012d1a0c64d74320d4e3d2d6
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
# v0.3.0 2016-07-01
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* `:type?` predicate imported from dry-types (solnic)
|
6
|
+
* `Rule#curry` interface (solnic)
|
7
|
+
|
8
|
+
### Changed
|
9
|
+
|
10
|
+
* Predicates AST now includes information about args (names & possible values) (fran-worley + solnic)
|
11
|
+
* Predicates raise errors when they are called with invalid arity (fran-worley + solnic)
|
12
|
+
* Rules no longer evaluate input twice when building result objects (solnic)
|
13
|
+
|
14
|
+
[Compare v0.2.3...v0.3.0](https://github.com/dryrb/dry-logic/compare/v0.2.3...v0.3.0)
|
15
|
+
|
1
16
|
# v0.2.3 2016-05-11
|
2
17
|
|
3
18
|
### Added
|
@@ -11,7 +26,6 @@
|
|
11
26
|
|
12
27
|
[Compare v0.2.2...v0.2.3](https://github.com/dryrb/dry-logic/compare/v0.2.2...v0.2.3)
|
13
28
|
|
14
|
-
|
15
29
|
# v0.2.2 2016-03-30
|
16
30
|
|
17
31
|
### Added
|
data/README.md
CHANGED
@@ -28,20 +28,27 @@ require 'dry/logic/predicates'
|
|
28
28
|
|
29
29
|
include Dry::Logic
|
30
30
|
|
31
|
-
user_present = Rule::Key.new(Predicates[:
|
31
|
+
user_present = Rule::Key.new(Predicates[:filled?], name: :user)
|
32
32
|
|
33
|
-
has_min_age = Rule::Key.new(
|
34
|
-
|
35
|
-
name: :age
|
36
|
-
)
|
33
|
+
has_min_age = Rule::Key.new(Predicates[:int?], name: [:user, :age])
|
34
|
+
& Rule::Key.new(Predicates[:gt?].curry(18), name: [:user, :age])
|
37
35
|
|
38
36
|
user_rule = user_present & has_min_age
|
39
37
|
|
40
38
|
user_rule.(user: { age: 19 })
|
41
|
-
# #<Dry::Logic::Result::
|
39
|
+
# #<Dry::Logic::Result::Named success?=true input={:user=>{:age=>19}} rule=#<Dry::Logic::Rule::Key predicate=#<Dry::Logic::Predicate id=:gt? args=[18, 19]> options={:evaluator=>#<Dry::Logic::Evaluator::Key path=[:user, :age]>, :name=>[:user, :age]}>>
|
42
40
|
|
43
41
|
user_rule.(user: { age: 18 })
|
44
|
-
# #<Dry::Logic::Result::
|
42
|
+
# #<Dry::Logic::Result::Named success?=false input={:user=>{:age=>18}} rule=#<Dry::Logic::Rule::Key predicate=#<Dry::Logic::Predicate id=:gt? args=[18, 18]> options={:evaluator=>#<Dry::Logic::Evaluator::Key path=[:user, :age]>, :name=>[:user, :age]}>>
|
43
|
+
|
44
|
+
user_rule.(user: { age: 'seventeen' }).inspect
|
45
|
+
#<Dry::Logic::Result::Named success?=false input={:user=>{:age=>"seventeen"}} rule=#<Dry::Logic::Rule::Key predicate=#<Dry::Logic::Predicate id=:int? args=["seventeen"]> options={:evaluator=>#<Dry::Logic::Evaluator::Key path=[:user, :age]>, :name=>[:user, :age]}>>
|
46
|
+
|
47
|
+
user_rule.(user: { }).inspect
|
48
|
+
#<Dry::Logic::Result::Named success?=false input={:user=>{}} rule=#<Dry::Logic::Rule::Key predicate=#<Dry::Logic::Predicate id=:filled? args=[{}]> options={:evaluator=>#<Dry::Logic::Evaluator::Key path=[:user]>, :name=>:user}>>
|
49
|
+
|
50
|
+
puts user_rule.({}).inspect
|
51
|
+
#<Dry::Logic::Result::Named success?=false input={} rule=#<Dry::Logic::Rule::Key predicate=#<Dry::Logic::Predicate id=:filled? args=[nil]> options={:evaluator=>#<Dry::Logic::Evaluator::Key path=[:user]>, :name=>:user}>>
|
45
52
|
```
|
46
53
|
|
47
54
|
## License
|
data/examples/basic.rb
CHANGED
@@ -3,11 +3,17 @@ require 'dry/logic/predicates'
|
|
3
3
|
|
4
4
|
include Dry::Logic
|
5
5
|
|
6
|
-
user_present = Rule::Key.new(
|
7
|
-
has_min_age = Rule::Key.new(:
|
6
|
+
user_present = Rule::Key.new(Predicates[:filled?], name: :user)
|
7
|
+
has_min_age = Rule::Key.new(Predicates[:int?], name: [:user, :age]) & Rule::Key.new(Predicates[:gt?].curry(18), name: [:user, :age])
|
8
8
|
|
9
9
|
user_rule = user_present & has_min_age
|
10
10
|
|
11
11
|
puts user_rule.(user: { age: 19 }).inspect
|
12
12
|
|
13
13
|
puts user_rule.(user: { age: 18 }).inspect
|
14
|
+
|
15
|
+
puts user_rule.(user: { age: 'seventeen' }).inspect
|
16
|
+
|
17
|
+
puts user_rule.(user: { }).inspect
|
18
|
+
|
19
|
+
puts user_rule.({}).inspect
|
data/lib/dry/logic/predicate.rb
CHANGED
@@ -3,33 +3,98 @@ module Dry
|
|
3
3
|
def self.Predicate(block)
|
4
4
|
case block
|
5
5
|
when Method then Predicate.new(block.name, &block)
|
6
|
+
when UnboundMethod then Predicate.new(block.name, fn: block)
|
6
7
|
else raise ArgumentError, 'predicate needs an :id'
|
7
8
|
end
|
8
9
|
end
|
9
10
|
|
10
11
|
class Predicate
|
12
|
+
Undefined = Class.new {
|
13
|
+
def inspect
|
14
|
+
"undefined"
|
15
|
+
end
|
16
|
+
alias_method :to_s, :inspect
|
17
|
+
}.new.freeze
|
18
|
+
|
11
19
|
include Dry::Equalizer(:id, :args)
|
12
20
|
|
13
|
-
attr_reader :id, :args, :fn
|
21
|
+
attr_reader :id, :args, :fn, :arity
|
22
|
+
|
23
|
+
class Curried < Predicate
|
24
|
+
def call(*args)
|
25
|
+
all_args = @args + args
|
14
26
|
|
15
|
-
|
27
|
+
if all_args.size == arity
|
28
|
+
super(*args)
|
29
|
+
else
|
30
|
+
curry(*args)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
alias_method :[], :call
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(id, args: [], fn: nil, arity: nil, &block)
|
16
37
|
@id = id
|
17
|
-
@fn = block
|
18
38
|
@args = args
|
39
|
+
@fn = fn || block
|
40
|
+
@arity = arity || @fn.arity
|
19
41
|
end
|
20
42
|
|
43
|
+
#as long as we keep track of the args, we don't actually need to curry the proc...
|
44
|
+
#if we never curry the proc then fn.arity & fn.parameters stay intact
|
45
|
+
def curry(*args)
|
46
|
+
if args.size > 0
|
47
|
+
all_args = @args + args
|
48
|
+
size = all_args.size
|
49
|
+
|
50
|
+
if size <= arity
|
51
|
+
Curried.new(id, args: all_args, fn: fn, arity: arity)
|
52
|
+
else
|
53
|
+
raise_arity_error(all_args.size)
|
54
|
+
end
|
55
|
+
else
|
56
|
+
self
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @api public
|
61
|
+
def bind(object)
|
62
|
+
self.class.new(id, *args, &fn.bind(object))
|
63
|
+
end
|
64
|
+
|
65
|
+
#enables a rule to call with its params & have them ignored if the
|
66
|
+
#predicate doesn't need them.
|
67
|
+
#if @args.size == arity then we should ignore called args
|
21
68
|
def call(*args)
|
22
|
-
|
69
|
+
all_args = @args + args
|
70
|
+
size = all_args.size
|
71
|
+
|
72
|
+
if size == arity
|
73
|
+
fn.(*all_args)
|
74
|
+
else
|
75
|
+
raise_arity_error(size)
|
76
|
+
end
|
23
77
|
end
|
78
|
+
alias_method :[], :call
|
24
79
|
|
25
|
-
def
|
26
|
-
|
80
|
+
def parameters
|
81
|
+
fn.parameters
|
27
82
|
end
|
28
83
|
|
29
84
|
def to_ast
|
30
|
-
[:predicate, [id,
|
85
|
+
[:predicate, [id, args_with_names]]
|
31
86
|
end
|
32
87
|
alias_method :to_a, :to_ast
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def args_with_names
|
92
|
+
parameters.map(&:last).zip(args + Array.new(arity - args.size, Undefined))
|
93
|
+
end
|
94
|
+
|
95
|
+
def raise_arity_error(args_size)
|
96
|
+
raise ArgumentError, "wrong number of arguments (#{args_size} for #{arity})"
|
97
|
+
end
|
33
98
|
end
|
34
99
|
end
|
35
100
|
end
|
data/lib/dry/logic/predicates.rb
CHANGED
data/lib/dry/logic/result/set.rb
CHANGED
@@ -5,9 +5,13 @@ module Dry
|
|
5
5
|
if response.respond_to?(:to_ast)
|
6
6
|
response.to_ast
|
7
7
|
else
|
8
|
-
[:result, [
|
8
|
+
[:result, [input, rule.to_ast]]
|
9
9
|
end
|
10
10
|
end
|
11
|
+
|
12
|
+
def input
|
13
|
+
rule.input != Predicate::Undefined ? rule.input : super
|
14
|
+
end
|
11
15
|
end
|
12
16
|
end
|
13
17
|
end
|
data/lib/dry/logic/rule.rb
CHANGED
@@ -7,6 +7,13 @@ module Dry
|
|
7
7
|
|
8
8
|
attr_reader :options
|
9
9
|
|
10
|
+
def self.method_added(meth)
|
11
|
+
super
|
12
|
+
if meth == :call
|
13
|
+
alias_method :[], :call
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
10
17
|
def initialize(predicate, options = {})
|
11
18
|
@predicate = predicate
|
12
19
|
@options = options
|
@@ -49,7 +56,11 @@ module Dry
|
|
49
56
|
end
|
50
57
|
|
51
58
|
def curry(*args)
|
52
|
-
|
59
|
+
if arity > 0
|
60
|
+
new(predicate.curry(*args))
|
61
|
+
else
|
62
|
+
self
|
63
|
+
end
|
53
64
|
end
|
54
65
|
|
55
66
|
def each?
|
data/lib/dry/logic/rule/attr.rb
CHANGED
data/lib/dry/logic/rule/check.rb
CHANGED
@@ -21,7 +21,7 @@ module Dry
|
|
21
21
|
def call(input)
|
22
22
|
args = evaluator[input].reverse
|
23
23
|
*head, tail = args
|
24
|
-
Logic.Result(predicate.curry(*head).(tail),
|
24
|
+
Logic.Result(predicate.curry(*head).(tail), curry(*args), input)
|
25
25
|
end
|
26
26
|
|
27
27
|
def evaluate(input)
|
@@ -10,6 +10,18 @@ module Dry
|
|
10
10
|
@right = right
|
11
11
|
end
|
12
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
|
+
|
13
25
|
def name
|
14
26
|
:"#{left.name}_#{type}_#{right.name}"
|
15
27
|
end
|
@@ -71,10 +83,6 @@ module Dry
|
|
71
83
|
Logic.Result(left.(input).success? ^ right.(input).success?, self, input)
|
72
84
|
end
|
73
85
|
|
74
|
-
def evaluate(input)
|
75
|
-
[left.evaluate(input), right.evaluate(input)]
|
76
|
-
end
|
77
|
-
|
78
86
|
def type
|
79
87
|
:xor
|
80
88
|
end
|
data/lib/dry/logic/rule/key.rb
CHANGED
@@ -7,11 +7,12 @@ module Dry
|
|
7
7
|
|
8
8
|
def self.new(predicate, options)
|
9
9
|
name = options.fetch(:name)
|
10
|
-
|
10
|
+
eval = options.fetch(:evaluator, evaluator(name))
|
11
|
+
super(predicate, evaluator: eval, name: name)
|
11
12
|
end
|
12
13
|
|
13
|
-
def self.evaluator(
|
14
|
-
Evaluator::Key.new(
|
14
|
+
def self.evaluator(name)
|
15
|
+
Evaluator::Key.new(name)
|
15
16
|
end
|
16
17
|
|
17
18
|
def initialize(predicate, options)
|
data/lib/dry/logic/rule/set.rb
CHANGED
@@ -7,10 +7,18 @@ module Dry
|
|
7
7
|
:set
|
8
8
|
end
|
9
9
|
|
10
|
+
def arity
|
11
|
+
-1
|
12
|
+
end
|
13
|
+
|
10
14
|
def apply(input)
|
11
15
|
rules.map { |rule| rule.(input) }
|
12
16
|
end
|
13
17
|
|
18
|
+
def curry(*args)
|
19
|
+
new(rules.map { |r| r.curry(*args) })
|
20
|
+
end
|
21
|
+
|
14
22
|
def at(*args)
|
15
23
|
new(rules.values_at(*args))
|
16
24
|
end
|
data/lib/dry/logic/rule/value.rb
CHANGED
@@ -5,12 +5,35 @@ module Dry
|
|
5
5
|
:val
|
6
6
|
end
|
7
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
|
+
|
8
24
|
def call(input)
|
9
|
-
|
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
|
10
33
|
end
|
11
34
|
|
12
35
|
def apply(input)
|
13
|
-
predicate.(
|
36
|
+
predicate.(input)
|
14
37
|
end
|
15
38
|
|
16
39
|
def evaluate(input)
|
@@ -50,8 +50,15 @@ module Dry
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def visit_predicate(node)
|
53
|
-
name,
|
54
|
-
predicates[name]
|
53
|
+
name, params = node
|
54
|
+
predicate = predicates[name]
|
55
|
+
|
56
|
+
if params.size > 1
|
57
|
+
args = params.map(&:last).reject { |val| val == Predicate::Undefined }
|
58
|
+
predicate.curry(*args)
|
59
|
+
else
|
60
|
+
predicate
|
61
|
+
end
|
55
62
|
end
|
56
63
|
|
57
64
|
def visit_and(node)
|
data/lib/dry/logic/version.rb
CHANGED
data/spec/shared/predicates.rb
CHANGED
@@ -30,8 +30,8 @@ RSpec.shared_examples 'a passing predicate' do
|
|
30
30
|
let(:predicate) { Dry::Logic::Predicates[predicate_name] }
|
31
31
|
|
32
32
|
it do
|
33
|
-
arguments_list.each do |
|
34
|
-
expect(predicate.call(
|
33
|
+
arguments_list.each do |args|
|
34
|
+
expect(predicate.call(*args)).to be(true)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
@@ -40,8 +40,8 @@ RSpec.shared_examples 'a failing predicate' do
|
|
40
40
|
let(:predicate) { Dry::Logic::Predicates[predicate_name] }
|
41
41
|
|
42
42
|
it do
|
43
|
-
arguments_list.each do |
|
44
|
-
expect(predicate.call(
|
43
|
+
arguments_list.each do |args|
|
44
|
+
expect(predicate.call(*args)).to be(false)
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
data/spec/unit/predicate_spec.rb
CHANGED
@@ -1,6 +1,35 @@
|
|
1
1
|
require 'dry/logic/predicate'
|
2
2
|
|
3
|
-
RSpec.describe
|
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
|
+
|
4
33
|
describe '#call' do
|
5
34
|
it 'returns result of the predicate function' do
|
6
35
|
is_empty = Dry::Logic::Predicate.new(:is_empty) { |str| str.empty? }
|
@@ -9,6 +38,52 @@ RSpec.describe Dry::Logic::Predicate do
|
|
9
38
|
|
10
39
|
expect(is_empty.('filled')).to be(false)
|
11
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
|
12
87
|
end
|
13
88
|
|
14
89
|
describe '#curry' do
|
@@ -23,5 +98,18 @@ RSpec.describe Dry::Logic::Predicate do
|
|
23
98
|
expect(min_age_18.(19)).to be(true)
|
24
99
|
expect(min_age_18.(17)).to be(false)
|
25
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
|
26
114
|
end
|
27
115
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'dry/logic/predicates'
|
2
|
+
|
3
|
+
RSpec.describe Dry::Logic::Predicates do
|
4
|
+
describe '#array?' do
|
5
|
+
let(:predicate_name) { :array? }
|
6
|
+
|
7
|
+
context 'when value is an array' do
|
8
|
+
let(:arguments_list) do
|
9
|
+
[
|
10
|
+
[ [] ],
|
11
|
+
[ ['other', 'array'] ],
|
12
|
+
[ [123, 'really', :blah] ],
|
13
|
+
[ Array.new ],
|
14
|
+
[ [nil] ],
|
15
|
+
[ [false] ],
|
16
|
+
[ [true] ]
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
it_behaves_like 'a passing predicate'
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when value is not an array' do
|
24
|
+
let(:arguments_list) do
|
25
|
+
[
|
26
|
+
[''],
|
27
|
+
[{}],
|
28
|
+
[nil],
|
29
|
+
[:symbol],
|
30
|
+
[String],
|
31
|
+
[1],
|
32
|
+
[1.0],
|
33
|
+
[true],
|
34
|
+
[Hash.new]
|
35
|
+
]
|
36
|
+
end
|
37
|
+
|
38
|
+
it_behaves_like 'a failing predicate'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'dry/logic/predicates'
|
2
|
+
|
3
|
+
RSpec.describe Dry::Logic::Predicates do
|
4
|
+
describe '#type?' do
|
5
|
+
let(:predicate_name) { :type? }
|
6
|
+
|
7
|
+
context 'when value has a correct type' do
|
8
|
+
let(:arguments_list) do
|
9
|
+
[[TrueClass, true]]
|
10
|
+
end
|
11
|
+
|
12
|
+
it_behaves_like 'a passing predicate'
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'with value is not true' do
|
16
|
+
let(:arguments_list) do
|
17
|
+
[
|
18
|
+
[TrueClass, false],
|
19
|
+
[TrueClass, ''],
|
20
|
+
[TrueClass, []],
|
21
|
+
[TrueClass, {}],
|
22
|
+
[TrueClass, nil],
|
23
|
+
[TrueClass, :symbol],
|
24
|
+
[TrueClass, String],
|
25
|
+
[TrueClass, 1],
|
26
|
+
[TrueClass, 0],
|
27
|
+
[TrueClass, 'true'],
|
28
|
+
[TrueClass, 'false']
|
29
|
+
]
|
30
|
+
end
|
31
|
+
|
32
|
+
it_behaves_like 'a failing predicate'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -13,7 +13,7 @@ RSpec.describe Rule::Check do
|
|
13
13
|
|
14
14
|
expect(rule.(num: 1).to_ast).to eql(
|
15
15
|
[:input, [:compare, [
|
16
|
-
:result, [1, [:check, [:compare, [:predicate, [:eql?, [1]]]]]]]]
|
16
|
+
:result, [1, [:check, [:compare, [:predicate, [:eql?, [[:left, 1], [:right, 1]]]]]]]]]
|
17
17
|
]
|
18
18
|
)
|
19
19
|
end
|
@@ -29,12 +29,13 @@ RSpec.describe Rule::Check do
|
|
29
29
|
expect(rule.(nums: { left: 1, right: 2 })).to be_failure
|
30
30
|
end
|
31
31
|
|
32
|
+
#check rules reverse the order of params to enable cases like `left.gt(right)` to work
|
32
33
|
it 'curries args properly' do
|
33
34
|
result = rule.(nums: { left: 1, right: 2 })
|
34
35
|
|
35
36
|
expect(result.to_ast).to eql([
|
36
37
|
:input, [:compare, [
|
37
|
-
:result, [1, [:check, [:compare, [:predicate, [:eql?, [2]]]]]]]
|
38
|
+
:result, [1, [:check, [:compare, [:predicate, [:eql?, [[:left, 2], [:right, 1]]]]]]]]
|
38
39
|
]
|
39
40
|
])
|
40
41
|
end
|
data/spec/unit/rule/each_spec.rb
CHANGED
@@ -21,8 +21,8 @@ RSpec.describe Dry::Logic::Rule::Each do
|
|
21
21
|
expect(address_rule.([nil, nil]).to_ast).to eql([
|
22
22
|
:result, [[nil, nil], [
|
23
23
|
:each, [
|
24
|
-
[:el, [0, [:result, [nil, [:val, [:predicate, [:str?, []]]]]]]],
|
25
|
-
[:el, [1, [:result, [nil, [:val, [:predicate, [:str?, []]]]]]]]
|
24
|
+
[:el, [0, [:result, [nil, [:val, [:predicate, [:str?, [[:input, nil]]]]]]]]],
|
25
|
+
[:el, [1, [:result, [nil, [:val, [:predicate, [:str?, [[:input, nil]]]]]]]]]
|
26
26
|
]
|
27
27
|
]]
|
28
28
|
])
|
data/spec/unit/rule/key_spec.rb
CHANGED
@@ -56,7 +56,7 @@ RSpec.describe Rule::Key do
|
|
56
56
|
:address,
|
57
57
|
[:result, [
|
58
58
|
{ city: "NYC" },
|
59
|
-
[:set, [[:result, [{ city: 'NYC' }, [:val, [:predicate, [:key?, [:zipcode]]]]]]]]
|
59
|
+
[:set, [[:result, [{ city: 'NYC' }, [:val, [:predicate, [:key?, [[:name, :zipcode], [:input, {:city=>"NYC"}]]]]]]]]]
|
60
60
|
]]
|
61
61
|
]
|
62
62
|
])
|
@@ -93,8 +93,8 @@ RSpec.describe Rule::Key do
|
|
93
93
|
:result, [
|
94
94
|
[1, '3', 3],
|
95
95
|
[:each, [
|
96
|
-
[:el, [0, [:result, [1, [:val, [:predicate, [:str?, []]]]]]]],
|
97
|
-
[:el, [2, [:result, [3, [:val, [:predicate, [:str?, []]]]]]]]
|
96
|
+
[:el, [0, [:result, [1, [:val, [:predicate, [:str?, [[:input, 1]]]]]]]]],
|
97
|
+
[:el, [2, [:result, [3, [:val, [:predicate, [:str?, [[:input, 3]]]]]]]]]
|
98
98
|
]]
|
99
99
|
]
|
100
100
|
]
|
data/spec/unit/rule/set_spec.rb
CHANGED
@@ -21,8 +21,8 @@ RSpec.describe Dry::Logic::Rule::Set do
|
|
21
21
|
it 'returns an array representation' do
|
22
22
|
expect(rule.to_ast).to eql([
|
23
23
|
:set, [
|
24
|
-
[:val, [:predicate, [:str?, []]]],
|
25
|
-
[:val, [:predicate, [:min_size?, [6]]]]
|
24
|
+
[:val, [:predicate, [:str?, [[:input, Predicate::Undefined]]]]],
|
25
|
+
[:val, [:predicate, [:min_size?, [[:num, 6], [:input, Predicate::Undefined]]]]]
|
26
26
|
]
|
27
27
|
])
|
28
28
|
end
|
@@ -15,11 +15,31 @@ RSpec.describe Dry::Logic::Rule::Value do
|
|
15
15
|
expect(is_string.('1')).to be_success
|
16
16
|
end
|
17
17
|
|
18
|
+
context 'with a composite rule' do
|
19
|
+
subject(:rule) { Dry::Logic::Rule::Value.new(is_nil | is_string) }
|
20
|
+
|
21
|
+
it 'returns a success for valid input' do
|
22
|
+
expect(rule.(nil)).to be_success
|
23
|
+
expect(rule.('foo')).to be_success
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'returns a failure for invalid input' do
|
27
|
+
expect(rule.(312)).to be_failure
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'returns a failure result with curried args' do
|
31
|
+
expect(rule.(312).to_ast).to eql(
|
32
|
+
[:result, [312, [:val, [:predicate, [:str?, [[:input, 312]]]]]]]
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
18
37
|
context 'with a custom predicate' do
|
19
38
|
subject(:rule) { Dry::Logic::Rule::Value.new(predicate) }
|
20
39
|
|
21
|
-
let(:response) { double(success?: true) }
|
22
|
-
let(:predicate) {
|
40
|
+
let(:response) { double("response", success?: true) }
|
41
|
+
let(:predicate) { double("predicate", arity: 1, curry: curried, call: Result.new(response, double("rule"), test: true)) }
|
42
|
+
let(:curried) { double("curried", arity: 1, call: Result.new(response, double("rule"), test: true)) }
|
23
43
|
|
24
44
|
let(:result) { rule.(test: true) }
|
25
45
|
|
@@ -39,6 +59,17 @@ RSpec.describe Dry::Logic::Rule::Value do
|
|
39
59
|
it 'has no name by default' do
|
40
60
|
expect(result.name).to be(nil)
|
41
61
|
end
|
62
|
+
|
63
|
+
context "works with predicates.arity == 0" do
|
64
|
+
subject(:rule) { Dry::Logic::Rule::Value.new(predicate) }
|
65
|
+
|
66
|
+
let(:predicate) { Dry::Logic::Predicate.new(:without_args) { true } }
|
67
|
+
let(:result) { rule.('sutin') }
|
68
|
+
|
69
|
+
it "calls its predicate without any args" do
|
70
|
+
expect(result).to be_success
|
71
|
+
end
|
72
|
+
end
|
42
73
|
end
|
43
74
|
end
|
44
75
|
|
@@ -25,7 +25,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
|
|
25
25
|
let(:each_rule) { Rule::Each.new(val_rule) }
|
26
26
|
|
27
27
|
it 'compiles key rules' do
|
28
|
-
ast = [[:key, [:email, [:predicate, [:filled?, []]]]]]
|
28
|
+
ast = [[:key, [:email, [:predicate, [:filled?, [[:input, nil]]]]]]]
|
29
29
|
|
30
30
|
rules = compiler.(ast)
|
31
31
|
|
@@ -33,7 +33,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
it 'compiles attr rules' do
|
36
|
-
ast = [[:attr, [:email, [:predicate, [:filled?, []]]]]]
|
36
|
+
ast = [[:attr, [:email, [:predicate, [:filled?, [[:input, nil]]]]]]]
|
37
37
|
|
38
38
|
rules = compiler.(ast)
|
39
39
|
|
@@ -41,7 +41,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
|
|
41
41
|
end
|
42
42
|
|
43
43
|
it 'compiles check rules' do
|
44
|
-
ast = [[:check, [:email, [:predicate, [:filled?, []]]]]]
|
44
|
+
ast = [[:check, [:email, [:predicate, [:filled?, [[:input, nil]]]]]]]
|
45
45
|
|
46
46
|
rules = compiler.(ast)
|
47
47
|
|
@@ -49,7 +49,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
|
|
49
49
|
end
|
50
50
|
|
51
51
|
it 'compiles attr rules' do
|
52
|
-
ast = [[:attr, [:email, [:predicate, [:attr?,
|
52
|
+
ast = [[:attr, [:email, [:predicate, [:attr?, [[:name, :email]]]]]]]
|
53
53
|
|
54
54
|
rules = compiler.(ast)
|
55
55
|
|
@@ -57,7 +57,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
|
|
57
57
|
end
|
58
58
|
|
59
59
|
it 'compiles negated rules' do
|
60
|
-
ast = [[:not, [:key, [:email, [:predicate, [:filled?, []]]]]]]
|
60
|
+
ast = [[:not, [:key, [:email, [:predicate, [:filled?, [[:input, nil]]]]]]]]
|
61
61
|
|
62
62
|
rules = compiler.(ast)
|
63
63
|
|
@@ -68,8 +68,8 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
|
|
68
68
|
ast = [
|
69
69
|
[
|
70
70
|
:and, [
|
71
|
-
[:key, [:email, [:predicate, [:key?, []]]]],
|
72
|
-
[:val, [:predicate, [:filled?, []]]]
|
71
|
+
[:key, [:email, [:predicate, [:key?, [[:name, :email], [:input, nil]]]]]],
|
72
|
+
[:val, [:predicate, [:filled?, [[:input, nil]]]]]
|
73
73
|
]
|
74
74
|
]
|
75
75
|
]
|
@@ -83,8 +83,8 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
|
|
83
83
|
ast = [
|
84
84
|
[
|
85
85
|
:or, [
|
86
|
-
[:key, [:email, [:predicate, [:key?, []]]]],
|
87
|
-
[:val, [:predicate, [:filled?, []]]]
|
86
|
+
[:key, [:email, [:predicate, [:key?, [[:name, :email], [:input, nil]]]]]],
|
87
|
+
[:val, [:predicate, [:filled?, [[:input, nil]]]]]
|
88
88
|
]
|
89
89
|
]
|
90
90
|
]
|
@@ -98,8 +98,8 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
|
|
98
98
|
ast = [
|
99
99
|
[
|
100
100
|
:xor, [
|
101
|
-
[:key, [:email, [:predicate, [:key?, []]]]],
|
102
|
-
[:val, [:predicate, [:filled?, []]]]
|
101
|
+
[:key, [:email, [:predicate, [:key?, [[:name, :email], [:input, nil]]]]]],
|
102
|
+
[:val, [:predicate, [:filled?, [[:input, nil]]]]]
|
103
103
|
]
|
104
104
|
]
|
105
105
|
]
|
@@ -110,7 +110,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
|
|
110
110
|
end
|
111
111
|
|
112
112
|
it 'compiles set rules' do
|
113
|
-
ast = [[:set, [[:val, [:predicate, [:filled?, []]]]]]]
|
113
|
+
ast = [[:set, [[:val, [:predicate, [:filled?, [[:input, nil]]]]]]]]
|
114
114
|
|
115
115
|
rules = compiler.(ast)
|
116
116
|
|
@@ -118,7 +118,7 @@ RSpec.describe Dry::Logic::RuleCompiler, '#call' do
|
|
118
118
|
end
|
119
119
|
|
120
120
|
it 'compiles each rules' do
|
121
|
-
ast = [[:each, [:val, [:predicate, [:filled?, []]]]]]
|
121
|
+
ast = [[:each, [:val, [:predicate, [:filled?, [[:input, nil]]]]]]]
|
122
122
|
|
123
123
|
rules = compiler.(ast)
|
124
124
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dry-logic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Solnica
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-07-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-container
|
@@ -131,6 +131,7 @@ files:
|
|
131
131
|
- spec/shared/predicates.rb
|
132
132
|
- spec/spec_helper.rb
|
133
133
|
- spec/unit/predicate_spec.rb
|
134
|
+
- spec/unit/predicates/array_spec.rb
|
134
135
|
- spec/unit/predicates/attr_spec.rb
|
135
136
|
- spec/unit/predicates/bool_spec.rb
|
136
137
|
- spec/unit/predicates/date_spec.rb
|
@@ -163,6 +164,7 @@ files:
|
|
163
164
|
- spec/unit/predicates/str_spec.rb
|
164
165
|
- spec/unit/predicates/time_spec.rb
|
165
166
|
- spec/unit/predicates/true_spec.rb
|
167
|
+
- spec/unit/predicates/type_spec.rb
|
166
168
|
- spec/unit/rule/attr_spec.rb
|
167
169
|
- spec/unit/rule/check_spec.rb
|
168
170
|
- spec/unit/rule/conjunction_spec.rb
|
@@ -194,7 +196,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
194
196
|
version: '0'
|
195
197
|
requirements: []
|
196
198
|
rubyforge_project:
|
197
|
-
rubygems_version: 2.
|
199
|
+
rubygems_version: 2.5.1
|
198
200
|
signing_key:
|
199
201
|
specification_version: 4
|
200
202
|
summary: Predicate logic with rule composition
|
@@ -202,6 +204,7 @@ test_files:
|
|
202
204
|
- spec/shared/predicates.rb
|
203
205
|
- spec/spec_helper.rb
|
204
206
|
- spec/unit/predicate_spec.rb
|
207
|
+
- spec/unit/predicates/array_spec.rb
|
205
208
|
- spec/unit/predicates/attr_spec.rb
|
206
209
|
- spec/unit/predicates/bool_spec.rb
|
207
210
|
- spec/unit/predicates/date_spec.rb
|
@@ -234,6 +237,7 @@ test_files:
|
|
234
237
|
- spec/unit/predicates/str_spec.rb
|
235
238
|
- spec/unit/predicates/time_spec.rb
|
236
239
|
- spec/unit/predicates/true_spec.rb
|
240
|
+
- spec/unit/predicates/type_spec.rb
|
237
241
|
- spec/unit/rule/attr_spec.rb
|
238
242
|
- spec/unit/rule/check_spec.rb
|
239
243
|
- spec/unit/rule/conjunction_spec.rb
|
@@ -245,3 +249,4 @@ test_files:
|
|
245
249
|
- spec/unit/rule/set_spec.rb
|
246
250
|
- spec/unit/rule/value_spec.rb
|
247
251
|
- spec/unit/rule_compiler_spec.rb
|
252
|
+
has_rdoc:
|