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/result.rb
CHANGED
@@ -1,55 +1,98 @@
|
|
1
|
+
require 'dry/core/constants'
|
2
|
+
|
1
3
|
module Dry
|
2
4
|
module Logic
|
3
|
-
def self.Result(response, rule, input)
|
4
|
-
Result[rule].new(response, rule, input)
|
5
|
-
end
|
6
|
-
|
7
5
|
class Result
|
8
|
-
include
|
6
|
+
include Core::Constants
|
9
7
|
|
10
|
-
|
8
|
+
SUCCESS = Class.new {
|
9
|
+
def success?
|
10
|
+
true
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
when Rule::Each then Result::Each
|
15
|
-
when Rule::Set then Result::Set
|
16
|
-
when Rule::Key, Rule::Attr, Rule::Check then Result::Named
|
17
|
-
else Result::Value
|
13
|
+
def failure?
|
14
|
+
false
|
18
15
|
end
|
16
|
+
}.new.freeze
|
17
|
+
|
18
|
+
attr_reader :success
|
19
|
+
|
20
|
+
attr_reader :id
|
21
|
+
|
22
|
+
attr_reader :serializer
|
23
|
+
|
24
|
+
def initialize(success, id = nil, &block)
|
25
|
+
@success = success
|
26
|
+
@id = id
|
27
|
+
@serializer = block
|
19
28
|
end
|
20
29
|
|
21
|
-
def
|
22
|
-
|
23
|
-
@success = response.respond_to?(:success?) ? response.success? : response
|
24
|
-
@rule = rule
|
25
|
-
@input = input
|
30
|
+
def success?
|
31
|
+
success
|
26
32
|
end
|
27
33
|
|
28
|
-
def
|
29
|
-
|
34
|
+
def failure?
|
35
|
+
!success?
|
30
36
|
end
|
31
37
|
|
32
|
-
def
|
33
|
-
|
38
|
+
def type
|
39
|
+
success? ? :success : :failure
|
34
40
|
end
|
35
41
|
|
36
|
-
def
|
37
|
-
|
42
|
+
def ast(input = Undefined)
|
43
|
+
serializer.(input)
|
38
44
|
end
|
39
45
|
|
40
|
-
def
|
41
|
-
|
46
|
+
def to_ast
|
47
|
+
if id
|
48
|
+
[type, [id, ast]]
|
49
|
+
else
|
50
|
+
ast
|
51
|
+
end
|
42
52
|
end
|
43
53
|
|
44
|
-
def
|
45
|
-
|
54
|
+
def to_s
|
55
|
+
visit(to_ast)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def visit(ast)
|
61
|
+
__send__(:"visit_#{ast[0]}", ast[1])
|
62
|
+
end
|
63
|
+
|
64
|
+
def visit_predicate(node)
|
65
|
+
name, args = node
|
66
|
+
|
67
|
+
if args.empty?
|
68
|
+
name.to_s
|
69
|
+
else
|
70
|
+
"#{name}(#{args.map(&:last).map(&:inspect).join(', ')})"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def visit_and(node)
|
75
|
+
left, right = node
|
76
|
+
"#{visit(left)} AND #{visit(right)}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def visit_or(node)
|
80
|
+
left, right = node
|
81
|
+
"#{visit(left)} OR #{visit(right)}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def visit_xor(node)
|
85
|
+
left, right = node
|
86
|
+
"#{visit(left)} XOR #{visit(right)}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def visit_not(node)
|
90
|
+
"not(#{visit(node)})"
|
91
|
+
end
|
92
|
+
|
93
|
+
def visit_hint(node)
|
94
|
+
visit(node)
|
46
95
|
end
|
47
96
|
end
|
48
97
|
end
|
49
98
|
end
|
50
|
-
|
51
|
-
require 'dry/logic/result/value'
|
52
|
-
require 'dry/logic/result/named'
|
53
|
-
require 'dry/logic/result/multi'
|
54
|
-
require 'dry/logic/result/each'
|
55
|
-
require 'dry/logic/result/set'
|
data/lib/dry/logic/rule.rb
CHANGED
@@ -1,82 +1,98 @@
|
|
1
|
+
require 'dry/core/constants'
|
2
|
+
require 'dry/equalizer'
|
3
|
+
require 'dry/logic/operations'
|
4
|
+
require 'dry/logic/result'
|
5
|
+
|
1
6
|
module Dry
|
2
7
|
module Logic
|
8
|
+
def self.Rule(*args, **options, &block)
|
9
|
+
if args.any?
|
10
|
+
Rule.new(*args, Rule::DEFAULT_OPTIONS.merge(options))
|
11
|
+
elsif block
|
12
|
+
Rule.new(block, Rule::DEFAULT_OPTIONS.merge(options))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
3
16
|
class Rule
|
17
|
+
include Core::Constants
|
4
18
|
include Dry::Equalizer(:predicate, :options)
|
19
|
+
include Operators
|
20
|
+
|
21
|
+
DEFAULT_OPTIONS = { args: [].freeze }.freeze
|
5
22
|
|
6
23
|
attr_reader :predicate
|
7
24
|
|
8
25
|
attr_reader :options
|
9
26
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
alias_method :[], :call
|
14
|
-
end
|
15
|
-
end
|
27
|
+
attr_reader :args
|
28
|
+
|
29
|
+
attr_reader :arity
|
16
30
|
|
17
|
-
def initialize(predicate, options =
|
31
|
+
def initialize(predicate, options = DEFAULT_OPTIONS)
|
18
32
|
@predicate = predicate
|
19
33
|
@options = options
|
34
|
+
@args = options[:args]
|
35
|
+
@arity = options[:arity] || predicate.arity
|
20
36
|
end
|
21
37
|
|
22
|
-
def
|
23
|
-
|
38
|
+
def type
|
39
|
+
:rule
|
24
40
|
end
|
25
41
|
|
26
|
-
def
|
27
|
-
|
42
|
+
def id
|
43
|
+
options[:id]
|
44
|
+
end
|
45
|
+
|
46
|
+
def call(*input)
|
47
|
+
Result.new(self[*input], id) { ast(*input) }
|
28
48
|
end
|
29
49
|
|
30
|
-
def
|
31
|
-
|
50
|
+
def [](*input)
|
51
|
+
arity == 0 ? predicate.() : predicate[*args, *input]
|
32
52
|
end
|
33
|
-
alias_method :&, :and
|
34
53
|
|
35
|
-
def
|
36
|
-
|
54
|
+
def curry(*new_args)
|
55
|
+
all_args = args + new_args
|
56
|
+
|
57
|
+
if all_args.size > arity
|
58
|
+
raise ArgumentError, "wrong number of arguments (#{all_args.size} for #{arity})"
|
59
|
+
else
|
60
|
+
with(args: all_args)
|
61
|
+
end
|
37
62
|
end
|
38
|
-
alias_method :|, :or
|
39
63
|
|
40
|
-
def
|
41
|
-
|
64
|
+
def bind(object)
|
65
|
+
if predicate.instance_of?(UnboundMethod)
|
66
|
+
self.class.new(predicate.bind(object), options)
|
67
|
+
else
|
68
|
+
self.class.new(
|
69
|
+
-> *args { object.instance_exec(*args, &predicate) },
|
70
|
+
options.merge(arity: arity, parameters: parameters)
|
71
|
+
)
|
72
|
+
end
|
42
73
|
end
|
43
|
-
alias_method :^, :xor
|
44
74
|
|
45
|
-
def
|
46
|
-
|
75
|
+
def eval_args(object)
|
76
|
+
with(args: args.map { |arg| arg.instance_of?(UnboundMethod) ? arg.bind(object).() : arg })
|
47
77
|
end
|
48
|
-
alias_method :>, :then
|
49
78
|
|
50
|
-
def
|
51
|
-
|
79
|
+
def with(new_opts)
|
80
|
+
self.class.new(predicate, options.merge(new_opts))
|
52
81
|
end
|
53
82
|
|
54
|
-
def
|
55
|
-
|
83
|
+
def parameters
|
84
|
+
options[:parameters] || predicate.parameters
|
56
85
|
end
|
57
86
|
|
58
|
-
def
|
59
|
-
|
60
|
-
new(predicate.curry(*args))
|
61
|
-
else
|
62
|
-
self
|
63
|
-
end
|
87
|
+
def ast(input = Undefined)
|
88
|
+
[:predicate, [id, args_with_names(input)]]
|
64
89
|
end
|
65
90
|
|
66
|
-
|
67
|
-
|
91
|
+
private
|
92
|
+
|
93
|
+
def args_with_names(*input)
|
94
|
+
parameters.map(&:last).zip(args + input)
|
68
95
|
end
|
69
96
|
end
|
70
97
|
end
|
71
98
|
end
|
72
|
-
|
73
|
-
require 'dry/logic/rule/value'
|
74
|
-
require 'dry/logic/rule/key'
|
75
|
-
require 'dry/logic/rule/attr'
|
76
|
-
require 'dry/logic/rule/each'
|
77
|
-
require 'dry/logic/rule/set'
|
78
|
-
require 'dry/logic/rule/composite'
|
79
|
-
require 'dry/logic/rule/negation'
|
80
|
-
require 'dry/logic/rule/check'
|
81
|
-
|
82
|
-
require 'dry/logic/result'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'dry/logic/rule'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Logic
|
5
|
+
class Rule::Predicate < Rule
|
6
|
+
def type
|
7
|
+
:predicate
|
8
|
+
end
|
9
|
+
|
10
|
+
def name
|
11
|
+
predicate.name
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
if args.size > 0
|
16
|
+
"#{name}(#{args.map(&:inspect).join(', ')})"
|
17
|
+
else
|
18
|
+
"#{name}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def ast(input = Undefined)
|
23
|
+
[type, [name, args_with_names(input)]]
|
24
|
+
end
|
25
|
+
alias_method :to_ast, :ast
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -1,8 +1,11 @@
|
|
1
|
+
require 'dry/core/constants'
|
1
2
|
require 'dry/logic/rule'
|
2
3
|
|
3
4
|
module Dry
|
4
5
|
module Logic
|
5
6
|
class RuleCompiler
|
7
|
+
include Core::Constants
|
8
|
+
|
6
9
|
attr_reader :predicates
|
7
10
|
|
8
11
|
def initialize(predicates)
|
@@ -19,42 +22,38 @@ module Dry
|
|
19
22
|
end
|
20
23
|
|
21
24
|
def visit_check(node)
|
22
|
-
|
23
|
-
|
25
|
+
keys, predicate = node
|
26
|
+
Operations::Check.new(visit(predicate), keys: keys)
|
24
27
|
end
|
25
28
|
|
26
29
|
def visit_not(node)
|
27
|
-
visit(node)
|
30
|
+
Operations::Negation.new(visit(node))
|
28
31
|
end
|
29
32
|
|
30
33
|
def visit_key(node)
|
31
34
|
name, predicate = node
|
32
|
-
|
35
|
+
Operations::Key.new(visit(predicate), name: name)
|
33
36
|
end
|
34
37
|
|
35
38
|
def visit_attr(node)
|
36
39
|
name, predicate = node
|
37
|
-
|
38
|
-
end
|
39
|
-
|
40
|
-
def visit_val(node)
|
41
|
-
Rule::Value.new(visit(node))
|
40
|
+
Operations::Attr.new(visit(predicate), name: name)
|
42
41
|
end
|
43
42
|
|
44
43
|
def visit_set(node)
|
45
|
-
|
44
|
+
Operations::Set.new(*call(node))
|
46
45
|
end
|
47
46
|
|
48
47
|
def visit_each(node)
|
49
|
-
|
48
|
+
Operations::Each.new(visit(node))
|
50
49
|
end
|
51
50
|
|
52
51
|
def visit_predicate(node)
|
53
52
|
name, params = node
|
54
|
-
predicate = predicates[name]
|
53
|
+
predicate = Rule::Predicate.new(predicates[name])
|
55
54
|
|
56
55
|
if params.size > 1
|
57
|
-
args = params.map(&:last).reject { |val| val ==
|
56
|
+
args = params.map(&:last).reject { |val| val == Undefined }
|
58
57
|
predicate.curry(*args)
|
59
58
|
else
|
60
59
|
predicate
|
@@ -63,22 +62,22 @@ module Dry
|
|
63
62
|
|
64
63
|
def visit_and(node)
|
65
64
|
left, right = node
|
66
|
-
visit(left)
|
65
|
+
visit(left).and(visit(right))
|
67
66
|
end
|
68
67
|
|
69
68
|
def visit_or(node)
|
70
69
|
left, right = node
|
71
|
-
visit(left)
|
70
|
+
visit(left).or(visit(right))
|
72
71
|
end
|
73
72
|
|
74
73
|
def visit_xor(node)
|
75
74
|
left, right = node
|
76
|
-
visit(left)
|
75
|
+
visit(left).xor(visit(right))
|
77
76
|
end
|
78
77
|
|
79
78
|
def visit_implication(node)
|
80
79
|
left, right = node
|
81
|
-
visit(left)
|
80
|
+
visit(left).then(visit(right))
|
82
81
|
end
|
83
82
|
end
|
84
83
|
end
|
data/lib/dry/logic/version.rb
CHANGED
@@ -0,0 +1,59 @@
|
|
1
|
+
RSpec.describe Result do
|
2
|
+
include_context 'predicates'
|
3
|
+
|
4
|
+
describe '#to_s' do
|
5
|
+
shared_examples_for 'string representation' do
|
6
|
+
it 'returns string representation' do
|
7
|
+
expect(rule.(input).to_s).to eql(output)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'with a predicate' do
|
12
|
+
let(:rule) { Rule::Predicate.new(gt?, args: [18]) }
|
13
|
+
let(:input) { 17 }
|
14
|
+
let(:output) { 'gt?(18, 17)' }
|
15
|
+
|
16
|
+
it_behaves_like 'string representation'
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'with AND operation' do
|
20
|
+
let(:rule) { Rule::Predicate.new(array?).and(Rule::Predicate.new(empty?)) }
|
21
|
+
let(:input) { '' }
|
22
|
+
let(:output) { 'array?("") AND empty?("")' }
|
23
|
+
|
24
|
+
it_behaves_like 'string representation'
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'with OR operation' do
|
28
|
+
let(:rule) { Rule::Predicate.new(array?).or(Rule::Predicate.new(empty?)) }
|
29
|
+
let(:input) { 123 }
|
30
|
+
let(:output) { 'array?(123) OR empty?(123)' }
|
31
|
+
|
32
|
+
it_behaves_like 'string representation'
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'with XOR operation' do
|
36
|
+
let(:rule) { Rule::Predicate.new(array?).xor(Rule::Predicate.new(empty?)) }
|
37
|
+
let(:input) { [] }
|
38
|
+
let(:output) { 'array?([]) XOR empty?([])' }
|
39
|
+
|
40
|
+
it_behaves_like 'string representation'
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'with THEN operation' do
|
44
|
+
let(:rule) { Rule::Predicate.new(array?).then(Rule::Predicate.new(empty?)) }
|
45
|
+
let(:input) { [1, 2, 3] }
|
46
|
+
let(:output) { 'empty?([1, 2, 3])' }
|
47
|
+
|
48
|
+
it_behaves_like 'string representation'
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'with NOT operation' do
|
52
|
+
let(:rule) { Operations::Negation.new(Rule::Predicate.new(array?)) }
|
53
|
+
let(:input) { 'foo' }
|
54
|
+
let(:output) { 'not(array?("foo"))' }
|
55
|
+
|
56
|
+
it_behaves_like 'string representation'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|