dry-logic 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -1
  3. data/README.md +6 -2
  4. data/lib/dry/logic/evaluator.rb +46 -0
  5. data/lib/dry/logic/predicate.rb +3 -3
  6. data/lib/dry/logic/result.rb +26 -126
  7. data/lib/dry/logic/result/each.rb +10 -0
  8. data/lib/dry/logic/result/multi.rb +14 -0
  9. data/lib/dry/logic/result/named.rb +17 -0
  10. data/lib/dry/logic/result/set.rb +10 -0
  11. data/lib/dry/logic/result/value.rb +13 -0
  12. data/lib/dry/logic/rule.rb +14 -36
  13. data/lib/dry/logic/rule/attr.rb +3 -11
  14. data/lib/dry/logic/rule/check.rb +23 -22
  15. data/lib/dry/logic/rule/composite.rb +32 -12
  16. data/lib/dry/logic/rule/each.rb +3 -3
  17. data/lib/dry/logic/rule/key.rb +24 -5
  18. data/lib/dry/logic/rule/negation.rb +15 -0
  19. data/lib/dry/logic/rule/set.rb +9 -8
  20. data/lib/dry/logic/rule/value.rb +15 -3
  21. data/lib/dry/logic/rule_compiler.rb +8 -40
  22. data/lib/dry/logic/version.rb +1 -1
  23. data/spec/shared/predicates.rb +2 -0
  24. data/spec/spec_helper.rb +1 -0
  25. data/spec/unit/rule/attr_spec.rb +5 -5
  26. data/spec/unit/rule/check_spec.rb +26 -39
  27. data/spec/unit/rule/conjunction_spec.rb +4 -4
  28. data/spec/unit/rule/disjunction_spec.rb +3 -3
  29. data/spec/unit/rule/each_spec.rb +2 -2
  30. data/spec/unit/rule/exclusive_disjunction_spec.rb +19 -0
  31. data/spec/unit/rule/implication_spec.rb +2 -2
  32. data/spec/unit/rule/key_spec.rb +103 -9
  33. data/spec/unit/rule/set_spec.rb +7 -9
  34. data/spec/unit/rule/value_spec.rb +29 -3
  35. data/spec/unit/rule_compiler_spec.rb +21 -49
  36. metadata +12 -9
  37. data/lib/dry/logic/rule/group.rb +0 -21
  38. data/lib/dry/logic/rule/result.rb +0 -33
  39. data/rakelib/rubocop.rake +0 -18
  40. data/spec/unit/rule/group_spec.rb +0 -12
  41. data/spec/unit/rule/result_spec.rb +0 -102
@@ -1,21 +1,13 @@
1
1
  module Dry
2
2
  module Logic
3
- class Rule::Attr < Rule
4
- def self.new(name, predicate)
5
- super(name, predicate.curry(name))
3
+ class Rule::Attr < Rule::Key
4
+ def self.evaluator(options)
5
+ Evaluator::Attr.new(options.fetch(:name))
6
6
  end
7
7
 
8
8
  def type
9
9
  :attr
10
10
  end
11
-
12
- def evaluate_input(input)
13
- input.public_send(name)
14
- end
15
-
16
- def call(input)
17
- Logic::Result::LazyValue.new(input, predicate.(input), self)
18
- end
19
11
  end
20
12
  end
21
13
  end
@@ -1,39 +1,40 @@
1
+ require 'dry/logic/evaluator'
2
+
1
3
  module Dry
2
4
  module Logic
3
- class Rule::Check < Rule
4
- attr_reader :keys
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)
5
11
 
6
- class Unary < Rule::Check
7
- def evaluate_input(*)
8
- predicate.input
9
- end
12
+ super(predicate, options.merge(evaluator: evaluator))
10
13
  end
11
14
 
12
- class Binary < Rule::Check
13
- def evaluate_input(result)
14
- keys.map do |key|
15
- if key.is_a?(Hash)
16
- parent, child = key.to_a.flatten
17
- result[parent].input[child]
18
- else
19
- result[key].input
20
- end
21
- end
22
- end
15
+ def initialize(predicate, options)
16
+ super
17
+ @name = options.fetch(:name)
18
+ @evaluator = options[:evaluator]
23
19
  end
24
20
 
25
- def initialize(name, predicate, keys)
26
- super(name, predicate)
27
- @keys = keys
21
+ def call(input)
22
+ args = evaluator[input].reverse
23
+ *head, tail = args
24
+ Logic.Result(predicate.curry(*head).(tail), head.size > 0 ? curry(*head) : self, input)
28
25
  end
29
26
 
30
- def call(result)
31
- Logic.Result(evaluate_input(result), predicate.(result), self)
27
+ def evaluate(input)
28
+ evaluator[input].first
32
29
  end
33
30
 
34
31
  def type
35
32
  :check
36
33
  end
34
+
35
+ def to_ast
36
+ [type, [name, predicate.to_ast]]
37
+ end
37
38
  end
38
39
  end
39
40
  end
@@ -3,7 +3,7 @@ module Dry
3
3
  class Rule::Composite < Rule
4
4
  include Dry::Equalizer(:left, :right)
5
5
 
6
- attr_reader :name, :left, :right
6
+ attr_reader :left, :right
7
7
 
8
8
  def initialize(left, right)
9
9
  @left = left
@@ -14,15 +14,19 @@ module Dry
14
14
  :"#{left.name}_#{type}_#{right.name}"
15
15
  end
16
16
 
17
- def to_ary
18
- [type, [left.to_ary, right.to_ary]]
17
+ def to_ast
18
+ [type, [left.to_ast, right.to_ast]]
19
19
  end
20
- alias_method :to_a, :to_ary
20
+ alias_method :to_a, :to_ast
21
21
  end
22
22
 
23
23
  class Rule::Implication < Rule::Composite
24
- def call(*args)
25
- left.(*args).then(right)
24
+ def call(input)
25
+ if left.(input).success?
26
+ right.(input)
27
+ else
28
+ Logic.Result(true, left, input)
29
+ end
26
30
  end
27
31
 
28
32
  def type
@@ -31,8 +35,14 @@ module Dry
31
35
  end
32
36
 
33
37
  class Rule::Conjunction < Rule::Composite
34
- def call(*args)
35
- left.(*args).and(right)
38
+ def call(input)
39
+ result = left.(input)
40
+
41
+ if result.success?
42
+ right.(input)
43
+ else
44
+ result
45
+ end
36
46
  end
37
47
 
38
48
  def type
@@ -41,8 +51,14 @@ module Dry
41
51
  end
42
52
 
43
53
  class Rule::Disjunction < Rule::Composite
44
- def call(*args)
45
- left.(*args).or(right)
54
+ def call(input)
55
+ result = left.(input)
56
+
57
+ if result.success?
58
+ result
59
+ else
60
+ right.(input)
61
+ end
46
62
  end
47
63
 
48
64
  def type
@@ -51,8 +67,12 @@ module Dry
51
67
  end
52
68
 
53
69
  class Rule::ExclusiveDisjunction < Rule::Composite
54
- def call(*args)
55
- left.(*args).xor(right)
70
+ def call(input)
71
+ Logic.Result(left.(input).success? ^ right.(input).success?, self, input)
72
+ end
73
+
74
+ def evaluate(input)
75
+ [left.evaluate(input), right.evaluate(input)]
56
76
  end
57
77
 
58
78
  def type
@@ -1,8 +1,8 @@
1
1
  module Dry
2
2
  module Logic
3
- class Rule::Each < Rule
4
- def call(input)
5
- Logic.Result(input, input.map { |element| predicate.(element) }, self)
3
+ class Rule::Each < Rule::Value
4
+ def apply(input)
5
+ input.map { |element| predicate.(element) }
6
6
  end
7
7
 
8
8
  def type
@@ -1,16 +1,35 @@
1
+ require 'dry/logic/evaluator'
2
+
1
3
  module Dry
2
4
  module Logic
3
- class Rule::Key < Rule
4
- def self.new(name, predicate)
5
- super(name, predicate.curry(name))
5
+ class Rule::Key < Rule::Value
6
+ attr_reader :name, :evaluator
7
+
8
+ def self.new(predicate, options)
9
+ name = options.fetch(:name)
10
+ super(predicate, evaluator: evaluator(options), name: name)
11
+ end
12
+
13
+ def self.evaluator(options)
14
+ Evaluator::Key.new(options.fetch(:name))
15
+ end
16
+
17
+ def initialize(predicate, options)
18
+ super
19
+ @name = options[:name]
20
+ @evaluator = options[:evaluator]
21
+ end
22
+
23
+ def evaluate(input)
24
+ evaluator[input]
6
25
  end
7
26
 
8
27
  def type
9
28
  :key
10
29
  end
11
30
 
12
- def call(input)
13
- Logic.Result(input[name], predicate.(input), self)
31
+ def to_ast
32
+ [type, [name, predicate.to_ast]]
14
33
  end
15
34
  end
16
35
  end
@@ -0,0 +1,15 @@
1
+ module Dry
2
+ module Logic
3
+ class Rule
4
+ class Negation < Rule::Value
5
+ def type
6
+ :not
7
+ end
8
+
9
+ def call(input)
10
+ predicate.(input).negated
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,22 +1,23 @@
1
1
  module Dry
2
2
  module Logic
3
- class Rule::Set < Rule
4
- def call(input)
5
- Logic.Result(input, predicate.map { |rule| rule.(input) }, self)
6
- end
3
+ class Rule::Set < Rule::Value
4
+ alias_method :rules, :predicate
7
5
 
8
6
  def type
9
7
  :set
10
8
  end
11
9
 
10
+ def apply(input)
11
+ rules.map { |rule| rule.(input) }
12
+ end
13
+
12
14
  def at(*args)
13
- self.class.new(name, predicate.values_at(*args))
15
+ new(rules.values_at(*args))
14
16
  end
15
17
 
16
- def to_ary
17
- [type, [name, predicate.map(&:to_ary)]]
18
+ def to_ast
19
+ [type, rules.map { |rule| rule.to_ast }]
18
20
  end
19
- alias_method :to_a, :to_ary
20
21
  end
21
22
  end
22
23
  end
@@ -1,12 +1,24 @@
1
1
  module Dry
2
2
  module Logic
3
3
  class Rule::Value < Rule
4
+ def type
5
+ :val
6
+ end
7
+
4
8
  def call(input)
5
- Logic.Result(input, predicate.(input), self)
9
+ Logic.Result(apply(input), self, input)
6
10
  end
7
11
 
8
- def type
9
- :val
12
+ def apply(input)
13
+ predicate.(evaluate(input))
14
+ end
15
+
16
+ def evaluate(input)
17
+ input
18
+ end
19
+
20
+ def to_ast
21
+ [type, predicate.to_ast]
10
22
  end
11
23
  end
12
24
  end
@@ -10,7 +10,7 @@ module Dry
10
10
  end
11
11
 
12
12
  def call(ast)
13
- ast.to_ary.map { |node| visit(node) }
13
+ ast.map { |node| visit(node) }
14
14
  end
15
15
 
16
16
  def visit(node)
@@ -20,26 +20,7 @@ module Dry
20
20
 
21
21
  def visit_check(node)
22
22
  name, predicate, keys = node
23
- check_keys = keys ? keys : [name]
24
- klass = check_keys.size == 1 ? Rule::Check::Unary : Rule::Check::Binary
25
- klass.new(name, visit(predicate), check_keys)
26
- end
27
-
28
- def visit_res(node)
29
- name, predicate = node
30
- Rule::Result.new(name, visit(predicate))
31
- end
32
-
33
- def visit_args(nodes)
34
- nodes.map { |node| visit(node) }
35
- end
36
-
37
- def visit_res_arg(name)
38
- predicates[name].input
39
- end
40
-
41
- def visit_arg(value)
42
- value
23
+ Rule::Check.new(visit(predicate), name: name, keys: keys || [name])
43
24
  end
44
25
 
45
26
  def visit_not(node)
@@ -48,37 +29,29 @@ module Dry
48
29
 
49
30
  def visit_key(node)
50
31
  name, predicate = node
51
- Rule::Key.new(name, visit(predicate))
32
+ Rule::Key.new(visit(predicate), name: name)
52
33
  end
53
34
 
54
35
  def visit_attr(node)
55
36
  name, predicate = node
56
- Rule::Attr.new(name, visit(predicate))
37
+ Rule::Attr.new(visit(predicate), name: name)
57
38
  end
58
39
 
59
40
  def visit_val(node)
60
- name, predicate = node
61
- Rule::Value.new(name, visit(predicate))
41
+ Rule::Value.new(visit(node))
62
42
  end
63
43
 
64
44
  def visit_set(node)
65
- name, rules = node
66
- Rule::Set.new(name, call(rules))
45
+ Rule::Set.new(call(node))
67
46
  end
68
47
 
69
48
  def visit_each(node)
70
- name, rule = node
71
- Rule::Each.new(name, visit(rule))
49
+ Rule::Each.new(visit(node))
72
50
  end
73
51
 
74
52
  def visit_predicate(node)
75
53
  name, args = node
76
-
77
- if args[0] == :args
78
- predicates[name].curry(*visit(args))
79
- else
80
- predicates[name].curry(*args)
81
- end
54
+ predicates[name].curry(*args)
82
55
  end
83
56
 
84
57
  def visit_and(node)
@@ -100,11 +73,6 @@ module Dry
100
73
  left, right = node
101
74
  visit(left) > visit(right)
102
75
  end
103
-
104
- def visit_group(node)
105
- identifier, predicate = node
106
- Rule::Group.new(identifier, visit(predicate))
107
- end
108
76
  end
109
77
  end
110
78
  end
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  module Logic
3
- VERSION = '0.1.4'.freeze
3
+ VERSION = '0.2.0'.freeze
4
4
  end
5
5
  end
@@ -5,6 +5,8 @@ RSpec.shared_examples 'predicates' do
5
5
 
6
6
  let(:str?) { Dry::Logic::Predicates[:str?] }
7
7
 
8
+ let(:true?) { Dry::Logic::Predicates[:true?] }
9
+
8
10
  let(:hash?) { Dry::Logic::Predicates[:hash?] }
9
11
 
10
12
  let(:int?) { Dry::Logic::Predicates[:int?] }
@@ -3,6 +3,7 @@ begin
3
3
  rescue LoadError; end
4
4
 
5
5
  require 'dry-logic'
6
+ require 'pathname'
6
7
 
7
8
  SPEC_ROOT = Pathname(__dir__)
8
9
 
@@ -5,24 +5,24 @@ RSpec.describe Dry::Logic::Rule::Attr do
5
5
 
6
6
  let(:model) { Struct.new(:name) }
7
7
 
8
- subject(:rule) { described_class.new(:name, attr?) }
8
+ subject(:rule) { described_class.new(str?, name: :name) }
9
9
 
10
10
  describe '#call' do
11
11
  it 'applies predicate to the value' do
12
- expect(rule.(model.new(name: 'Jane'))).to be_success
13
- expect(rule.(nil)).to be_failure
12
+ expect(rule.(model.new('Jane'))).to be_success
13
+ expect(rule.(model.new(nil))).to be_failure
14
14
  end
15
15
  end
16
16
 
17
17
  describe '#and' do
18
- let(:other) { Dry::Logic::Rule::Value.new(:name, str?) }
18
+ let(:other) { Dry::Logic::Rule::Attr.new(min_size?.curry(3), name: :name) }
19
19
 
20
20
  it 'returns conjunction rule where value is passed to the right' do
21
21
  present_and_string = rule.and(other)
22
22
 
23
23
  expect(present_and_string.(model.new('Jane'))).to be_success
24
24
 
25
- expect(present_and_string.(nil)).to be_failure
25
+ expect(present_and_string.(model.new('Ja'))).to be_failure
26
26
  expect(present_and_string.(model.new(1))).to be_failure
27
27
  end
28
28
  end