dry-logic 0.1.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 +7 -0
- data/.gitignore +8 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/.rubocop_todo.yml +7 -0
- data/.travis.yml +31 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +11 -0
- data/LICENSE +20 -0
- data/README.md +45 -0
- data/Rakefile +12 -0
- data/dry-logic.gemspec +24 -0
- data/examples/basic.rb +13 -0
- data/lib/dry-logic.rb +1 -0
- data/lib/dry/logic.rb +10 -0
- data/lib/dry/logic/predicate.rb +35 -0
- data/lib/dry/logic/predicate_set.rb +23 -0
- data/lib/dry/logic/predicates.rb +129 -0
- data/lib/dry/logic/result.rb +119 -0
- data/lib/dry/logic/rule.rb +91 -0
- data/lib/dry/logic/rule/check.rb +15 -0
- data/lib/dry/logic/rule/composite.rb +63 -0
- data/lib/dry/logic/rule/each.rb +13 -0
- data/lib/dry/logic/rule/group.rb +21 -0
- data/lib/dry/logic/rule/key.rb +17 -0
- data/lib/dry/logic/rule/set.rb +22 -0
- data/lib/dry/logic/rule/value.rb +13 -0
- data/lib/dry/logic/rule_compiler.rb +81 -0
- data/lib/dry/logic/version.rb +5 -0
- data/rakelib/rubocop.rake +18 -0
- data/spec/shared/predicates.rb +41 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/unit/predicate_spec.rb +27 -0
- data/spec/unit/predicates/bool_spec.rb +34 -0
- data/spec/unit/predicates/date_spec.rb +31 -0
- data/spec/unit/predicates/date_time_spec.rb +31 -0
- data/spec/unit/predicates/decimal_spec.rb +32 -0
- data/spec/unit/predicates/empty_spec.rb +38 -0
- data/spec/unit/predicates/eql_spec.rb +21 -0
- data/spec/unit/predicates/exclusion_spec.rb +35 -0
- data/spec/unit/predicates/filled_spec.rb +38 -0
- data/spec/unit/predicates/float_spec.rb +31 -0
- data/spec/unit/predicates/format_spec.rb +21 -0
- data/spec/unit/predicates/gt_spec.rb +40 -0
- data/spec/unit/predicates/gteq_spec.rb +40 -0
- data/spec/unit/predicates/inclusion_spec.rb +35 -0
- data/spec/unit/predicates/int_spec.rb +34 -0
- data/spec/unit/predicates/key_spec.rb +29 -0
- data/spec/unit/predicates/lt_spec.rb +40 -0
- data/spec/unit/predicates/lteq_spec.rb +40 -0
- data/spec/unit/predicates/max_size_spec.rb +49 -0
- data/spec/unit/predicates/min_size_spec.rb +49 -0
- data/spec/unit/predicates/none_spec.rb +28 -0
- data/spec/unit/predicates/size_spec.rb +55 -0
- data/spec/unit/predicates/str_spec.rb +32 -0
- data/spec/unit/predicates/time_spec.rb +31 -0
- data/spec/unit/rule/check_spec.rb +29 -0
- data/spec/unit/rule/conjunction_spec.rb +30 -0
- data/spec/unit/rule/disjunction_spec.rb +38 -0
- data/spec/unit/rule/each_spec.rb +20 -0
- data/spec/unit/rule/group_spec.rb +12 -0
- data/spec/unit/rule/implication_spec.rb +16 -0
- data/spec/unit/rule/key_spec.rb +27 -0
- data/spec/unit/rule/set_spec.rb +32 -0
- data/spec/unit/rule/value_spec.rb +42 -0
- data/spec/unit/rule_compiler_spec.rb +123 -0
- metadata +221 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
module Dry
|
2
|
+
module Logic
|
3
|
+
class Rule
|
4
|
+
include Dry::Equalizer(:name, :predicate)
|
5
|
+
|
6
|
+
attr_reader :name, :predicate
|
7
|
+
|
8
|
+
class Negation < Rule
|
9
|
+
include Dry::Equalizer(:rule)
|
10
|
+
|
11
|
+
attr_reader :rule
|
12
|
+
|
13
|
+
def initialize(rule)
|
14
|
+
@rule = rule
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(*args)
|
18
|
+
rule.(*args).negated
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_ary
|
22
|
+
[:not, rule.to_ary]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(name, predicate)
|
27
|
+
@name = name
|
28
|
+
@predicate = predicate
|
29
|
+
end
|
30
|
+
|
31
|
+
def predicate_id
|
32
|
+
predicate.id
|
33
|
+
end
|
34
|
+
|
35
|
+
def type
|
36
|
+
:rule
|
37
|
+
end
|
38
|
+
|
39
|
+
def call(*args)
|
40
|
+
Logic.Result(args, predicate.call, self)
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_ary
|
44
|
+
[type, [name, predicate.to_ary]]
|
45
|
+
end
|
46
|
+
alias_method :to_a, :to_ary
|
47
|
+
|
48
|
+
def and(other)
|
49
|
+
Conjunction.new(self, other)
|
50
|
+
end
|
51
|
+
alias_method :&, :and
|
52
|
+
|
53
|
+
def or(other)
|
54
|
+
Disjunction.new(self, other)
|
55
|
+
end
|
56
|
+
alias_method :|, :or
|
57
|
+
|
58
|
+
def xor(other)
|
59
|
+
ExclusiveDisjunction.new(self, other)
|
60
|
+
end
|
61
|
+
alias_method :^, :xor
|
62
|
+
|
63
|
+
def then(other)
|
64
|
+
Implication.new(self, other)
|
65
|
+
end
|
66
|
+
alias_method :>, :then
|
67
|
+
|
68
|
+
def negation
|
69
|
+
Negation.new(self)
|
70
|
+
end
|
71
|
+
|
72
|
+
def new(predicate)
|
73
|
+
self.class.new(name, predicate)
|
74
|
+
end
|
75
|
+
|
76
|
+
def curry(*args)
|
77
|
+
self.class.new(name, predicate.curry(*args))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
require 'dry/logic/rule/key'
|
84
|
+
require 'dry/logic/rule/value'
|
85
|
+
require 'dry/logic/rule/each'
|
86
|
+
require 'dry/logic/rule/set'
|
87
|
+
require 'dry/logic/rule/composite'
|
88
|
+
require 'dry/logic/rule/check'
|
89
|
+
require 'dry/logic/rule/group'
|
90
|
+
|
91
|
+
require 'dry/logic/result'
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Dry
|
2
|
+
module Logic
|
3
|
+
class Rule::Composite < Rule
|
4
|
+
include Dry::Equalizer(:left, :right)
|
5
|
+
|
6
|
+
attr_reader :name, :left, :right
|
7
|
+
|
8
|
+
def initialize(left, right)
|
9
|
+
@left = left
|
10
|
+
@right = right
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
:"#{left.name}_#{type}_#{right.name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_ary
|
18
|
+
[type, [left.to_ary, right.to_ary]]
|
19
|
+
end
|
20
|
+
alias_method :to_a, :to_ary
|
21
|
+
end
|
22
|
+
|
23
|
+
class Rule::Implication < Rule::Composite
|
24
|
+
def call(*args)
|
25
|
+
left.(*args) > right
|
26
|
+
end
|
27
|
+
|
28
|
+
def type
|
29
|
+
:implication
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Rule::Conjunction < Rule::Composite
|
34
|
+
def call(*args)
|
35
|
+
left.(*args).and(right)
|
36
|
+
end
|
37
|
+
|
38
|
+
def type
|
39
|
+
:and
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Rule::Disjunction < Rule::Composite
|
44
|
+
def call(*args)
|
45
|
+
left.(*args).or(right)
|
46
|
+
end
|
47
|
+
|
48
|
+
def type
|
49
|
+
:or
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Rule::ExclusiveDisjunction < Rule::Composite
|
54
|
+
def call(*args)
|
55
|
+
left.(*args).xor(right)
|
56
|
+
end
|
57
|
+
|
58
|
+
def type
|
59
|
+
:xor
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Dry
|
2
|
+
module Logic
|
3
|
+
class Rule::Group < Rule
|
4
|
+
attr_reader :rules
|
5
|
+
|
6
|
+
def initialize(identifier, predicate)
|
7
|
+
name, rules = identifier.to_a.first
|
8
|
+
@rules = rules
|
9
|
+
super(name, predicate)
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(*input)
|
13
|
+
Logic.Result(input, predicate.(*input), self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def type
|
17
|
+
:group
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Dry
|
2
|
+
module Logic
|
3
|
+
class Rule::Key < Rule
|
4
|
+
def self.new(name, predicate)
|
5
|
+
super(name, predicate.curry(name))
|
6
|
+
end
|
7
|
+
|
8
|
+
def type
|
9
|
+
:key
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(input)
|
13
|
+
Logic.Result(input[name], predicate.(input), self)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Dry
|
2
|
+
module Logic
|
3
|
+
class Rule::Set < Rule
|
4
|
+
def call(input)
|
5
|
+
Logic.Result(input, predicate.map { |rule| rule.(input) }, self)
|
6
|
+
end
|
7
|
+
|
8
|
+
def type
|
9
|
+
:set
|
10
|
+
end
|
11
|
+
|
12
|
+
def at(*args)
|
13
|
+
self.class.new(name, predicate.values_at(*args))
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_ary
|
17
|
+
[type, [name, predicate.map(&:to_ary)]]
|
18
|
+
end
|
19
|
+
alias_method :to_a, :to_ary
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'dry/logic/rule'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Logic
|
5
|
+
class RuleCompiler
|
6
|
+
attr_reader :predicates
|
7
|
+
|
8
|
+
def initialize(predicates)
|
9
|
+
@predicates = predicates
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(ast)
|
13
|
+
ast.map { |node| visit(node) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def visit(node)
|
17
|
+
name, nodes = node
|
18
|
+
send(:"visit_#{name}", nodes)
|
19
|
+
end
|
20
|
+
|
21
|
+
def visit_check(node)
|
22
|
+
name, predicate = node
|
23
|
+
Rule::Check.new(name, visit(predicate))
|
24
|
+
end
|
25
|
+
|
26
|
+
def visit_not(node)
|
27
|
+
visit(node).negation
|
28
|
+
end
|
29
|
+
|
30
|
+
def visit_key(node)
|
31
|
+
name, predicate = node
|
32
|
+
Rule::Key.new(name, visit(predicate))
|
33
|
+
end
|
34
|
+
|
35
|
+
def visit_val(node)
|
36
|
+
name, predicate = node
|
37
|
+
Rule::Value.new(name, visit(predicate))
|
38
|
+
end
|
39
|
+
|
40
|
+
def visit_set(node)
|
41
|
+
name, rules = node
|
42
|
+
Rule::Set.new(name, call(rules))
|
43
|
+
end
|
44
|
+
|
45
|
+
def visit_each(node)
|
46
|
+
name, rule = node
|
47
|
+
Rule::Each.new(name, visit(rule))
|
48
|
+
end
|
49
|
+
|
50
|
+
def visit_predicate(node)
|
51
|
+
name, args = node
|
52
|
+
predicates[name].curry(*args)
|
53
|
+
end
|
54
|
+
|
55
|
+
def visit_and(node)
|
56
|
+
left, right = node
|
57
|
+
visit(left) & visit(right)
|
58
|
+
end
|
59
|
+
|
60
|
+
def visit_or(node)
|
61
|
+
left, right = node
|
62
|
+
visit(left) | visit(right)
|
63
|
+
end
|
64
|
+
|
65
|
+
def visit_xor(node)
|
66
|
+
left, right = node
|
67
|
+
visit(left) ^ visit(right)
|
68
|
+
end
|
69
|
+
|
70
|
+
def visit_implication(node)
|
71
|
+
left, right = node
|
72
|
+
visit(left) > visit(right)
|
73
|
+
end
|
74
|
+
|
75
|
+
def visit_group(node)
|
76
|
+
identifier, predicate = node
|
77
|
+
Rule::Group.new(identifier, visit(predicate))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubocop/rake_task'
|
3
|
+
|
4
|
+
Rake::Task[:default].enhance [:rubocop]
|
5
|
+
|
6
|
+
RuboCop::RakeTask.new do |task|
|
7
|
+
task.options << '--display-cop-names'
|
8
|
+
end
|
9
|
+
|
10
|
+
namespace :rubocop do
|
11
|
+
desc 'Generate a configuration file acting as a TODO list.'
|
12
|
+
task :auto_gen_config do
|
13
|
+
exec 'bundle exec rubocop --auto-gen-config'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
rescue LoadError
|
18
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'dry/logic/predicates'
|
2
|
+
|
3
|
+
RSpec.shared_examples 'predicates' do
|
4
|
+
let(:none?) { Dry::Logic::Predicates[:none?] }
|
5
|
+
|
6
|
+
let(:str?) { Dry::Logic::Predicates[:str?] }
|
7
|
+
|
8
|
+
let(:int?) { Dry::Logic::Predicates[:int?] }
|
9
|
+
|
10
|
+
let(:filled?) { Dry::Logic::Predicates[:filled?] }
|
11
|
+
|
12
|
+
let(:min_size?) { Dry::Logic::Predicates[:min_size?] }
|
13
|
+
|
14
|
+
let(:lt?) { Dry::Logic::Predicates[:lt?] }
|
15
|
+
|
16
|
+
let(:gt?) { Dry::Logic::Predicates[:gt?] }
|
17
|
+
|
18
|
+
let(:key?) { Dry::Logic::Predicates[:key?] }
|
19
|
+
|
20
|
+
let(:eql?) { Dry::Logic::Predicates[:eql?] }
|
21
|
+
end
|
22
|
+
|
23
|
+
RSpec.shared_examples 'a passing predicate' do
|
24
|
+
let(:predicate) { Dry::Logic::Predicates[predicate_name] }
|
25
|
+
|
26
|
+
it do
|
27
|
+
arguments_list.each do |(left, right)|
|
28
|
+
expect(predicate.call(left, right)).to be(true)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
RSpec.shared_examples 'a failing predicate' do
|
34
|
+
let(:predicate) { Dry::Logic::Predicates[predicate_name] }
|
35
|
+
|
36
|
+
it do
|
37
|
+
arguments_list.each do |(left, right)|
|
38
|
+
expect(predicate.call(left, right)).to be(false)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'byebug'
|
5
|
+
rescue LoadError; end
|
6
|
+
|
7
|
+
require 'dry-logic'
|
8
|
+
|
9
|
+
SPEC_ROOT = Pathname(__dir__)
|
10
|
+
|
11
|
+
Dir[SPEC_ROOT.join('shared/**/*.rb')].each(&method(:require))
|
12
|
+
Dir[SPEC_ROOT.join('support/**/*.rb')].each(&method(:require))
|
13
|
+
|
14
|
+
include Dry::Logic
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
config.disable_monkey_patching!
|
18
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'dry/logic/predicate'
|
2
|
+
|
3
|
+
RSpec.describe Dry::Logic::Predicate do
|
4
|
+
describe '#call' do
|
5
|
+
it 'returns result of the predicate function' do
|
6
|
+
is_empty = Dry::Logic::Predicate.new(:is_empty) { |str| str.empty? }
|
7
|
+
|
8
|
+
expect(is_empty.('')).to be(true)
|
9
|
+
|
10
|
+
expect(is_empty.('filled')).to be(false)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#curry' do
|
15
|
+
it 'returns curried predicate' do
|
16
|
+
min_age = Dry::Logic::Predicate.new(:min_age) { |age, input| input >= age }
|
17
|
+
|
18
|
+
min_age_18 = min_age.curry(18)
|
19
|
+
|
20
|
+
expect(min_age_18.args).to eql([18])
|
21
|
+
|
22
|
+
expect(min_age_18.(18)).to be(true)
|
23
|
+
expect(min_age_18.(19)).to be(true)
|
24
|
+
expect(min_age_18.(17)).to be(false)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|