dry-logic 0.1.4 → 0.2.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.
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