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