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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -5
  3. data/CHANGELOG.md +28 -0
  4. data/Gemfile +7 -4
  5. data/dry-logic.gemspec +1 -0
  6. data/lib/dry/logic.rb +2 -3
  7. data/lib/dry/logic/appliable.rb +33 -0
  8. data/lib/dry/logic/evaluator.rb +2 -0
  9. data/lib/dry/logic/operations.rb +13 -0
  10. data/lib/dry/logic/operations/abstract.rb +44 -0
  11. data/lib/dry/logic/operations/and.rb +35 -0
  12. data/lib/dry/logic/operations/attr.rb +17 -0
  13. data/lib/dry/logic/operations/binary.rb +26 -0
  14. data/lib/dry/logic/operations/check.rb +52 -0
  15. data/lib/dry/logic/operations/each.rb +32 -0
  16. data/lib/dry/logic/operations/implication.rb +37 -0
  17. data/lib/dry/logic/operations/key.rb +66 -0
  18. data/lib/dry/logic/operations/negation.rb +18 -0
  19. data/lib/dry/logic/operations/or.rb +35 -0
  20. data/lib/dry/logic/operations/set.rb +35 -0
  21. data/lib/dry/logic/operations/unary.rb +24 -0
  22. data/lib/dry/logic/operations/xor.rb +27 -0
  23. data/lib/dry/logic/operators.rb +25 -0
  24. data/lib/dry/logic/predicates.rb +143 -136
  25. data/lib/dry/logic/result.rb +76 -33
  26. data/lib/dry/logic/rule.rb +62 -46
  27. data/lib/dry/logic/rule/predicate.rb +28 -0
  28. data/lib/dry/logic/rule_compiler.rb +16 -17
  29. data/lib/dry/logic/version.rb +1 -1
  30. data/spec/integration/result_spec.rb +59 -0
  31. data/spec/integration/rule_spec.rb +53 -0
  32. data/spec/shared/predicates.rb +6 -0
  33. data/spec/shared/rule.rb +67 -0
  34. data/spec/spec_helper.rb +10 -3
  35. data/spec/support/mutant.rb +9 -0
  36. data/spec/unit/operations/and_spec.rb +64 -0
  37. data/spec/unit/operations/attr_spec.rb +27 -0
  38. data/spec/unit/operations/check_spec.rb +49 -0
  39. data/spec/unit/operations/each_spec.rb +47 -0
  40. data/spec/unit/operations/implication_spec.rb +30 -0
  41. data/spec/unit/operations/key_spec.rb +119 -0
  42. data/spec/unit/operations/negation_spec.rb +40 -0
  43. data/spec/unit/operations/or_spec.rb +73 -0
  44. data/spec/unit/operations/set_spec.rb +41 -0
  45. data/spec/unit/operations/xor_spec.rb +61 -0
  46. data/spec/unit/predicates_spec.rb +23 -0
  47. data/spec/unit/rule/predicate_spec.rb +53 -0
  48. data/spec/unit/rule_compiler_spec.rb +38 -38
  49. data/spec/unit/rule_spec.rb +94 -0
  50. metadata +67 -40
  51. data/lib/dry/logic/predicate.rb +0 -100
  52. data/lib/dry/logic/predicate_set.rb +0 -23
  53. data/lib/dry/logic/result/each.rb +0 -20
  54. data/lib/dry/logic/result/multi.rb +0 -14
  55. data/lib/dry/logic/result/named.rb +0 -17
  56. data/lib/dry/logic/result/set.rb +0 -10
  57. data/lib/dry/logic/result/value.rb +0 -17
  58. data/lib/dry/logic/rule/attr.rb +0 -13
  59. data/lib/dry/logic/rule/check.rb +0 -40
  60. data/lib/dry/logic/rule/composite.rb +0 -91
  61. data/lib/dry/logic/rule/each.rb +0 -13
  62. data/lib/dry/logic/rule/key.rb +0 -37
  63. data/lib/dry/logic/rule/negation.rb +0 -15
  64. data/lib/dry/logic/rule/set.rb +0 -31
  65. data/lib/dry/logic/rule/value.rb +0 -48
  66. data/spec/unit/predicate_spec.rb +0 -115
  67. data/spec/unit/rule/attr_spec.rb +0 -29
  68. data/spec/unit/rule/check_spec.rb +0 -44
  69. data/spec/unit/rule/conjunction_spec.rb +0 -30
  70. data/spec/unit/rule/disjunction_spec.rb +0 -38
  71. data/spec/unit/rule/each_spec.rb +0 -31
  72. data/spec/unit/rule/exclusive_disjunction_spec.rb +0 -19
  73. data/spec/unit/rule/implication_spec.rb +0 -16
  74. data/spec/unit/rule/key_spec.rb +0 -121
  75. data/spec/unit/rule/set_spec.rb +0 -30
  76. data/spec/unit/rule/value_spec.rb +0 -99
@@ -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 Dry::Equalizer(:success?, :input, :rule)
6
+ include Core::Constants
9
7
 
10
- attr_reader :input, :rule, :response, :success
8
+ SUCCESS = Class.new {
9
+ def success?
10
+ true
11
+ end
11
12
 
12
- def self.[](type)
13
- case type
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 initialize(response, rule, input)
22
- @response = response
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 [](name)
29
- response[name] if response.respond_to?(:[])
34
+ def failure?
35
+ !success?
30
36
  end
31
37
 
32
- def name
33
- nil
38
+ def type
39
+ success? ? :success : :failure
34
40
  end
35
41
 
36
- def negated
37
- self.class.new(!success, rule, input)
42
+ def ast(input = Undefined)
43
+ serializer.(input)
38
44
  end
39
45
 
40
- def success?
41
- @success
46
+ def to_ast
47
+ if id
48
+ [type, [id, ast]]
49
+ else
50
+ ast
51
+ end
42
52
  end
43
53
 
44
- def failure?
45
- !success?
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'
@@ -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
- def self.method_added(meth)
11
- super
12
- if meth == :call
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 predicate_id
23
- predicate.id
38
+ def type
39
+ :rule
24
40
  end
25
41
 
26
- def type
27
- raise NotImplementedError
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 and(other)
31
- Conjunction.new(self, other)
50
+ def [](*input)
51
+ arity == 0 ? predicate.() : predicate[*args, *input]
32
52
  end
33
- alias_method :&, :and
34
53
 
35
- def or(other)
36
- Disjunction.new(self, other)
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 xor(other)
41
- ExclusiveDisjunction.new(self, other)
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 then(other)
46
- Implication.new(self, other)
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 negation
51
- Negation.new(self)
79
+ def with(new_opts)
80
+ self.class.new(predicate, options.merge(new_opts))
52
81
  end
53
82
 
54
- def new(predicate)
55
- self.class.new(predicate, options)
83
+ def parameters
84
+ options[:parameters] || predicate.parameters
56
85
  end
57
86
 
58
- def curry(*args)
59
- if arity > 0
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
- def each?
67
- predicate.is_a?(Rule::Each)
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
- name, predicate, keys = node
23
- Rule::Check.new(visit(predicate), name: name, keys: keys || [name])
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).negation
30
+ Operations::Negation.new(visit(node))
28
31
  end
29
32
 
30
33
  def visit_key(node)
31
34
  name, predicate = node
32
- Rule::Key.new(visit(predicate), name: name)
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
- Rule::Attr.new(visit(predicate), name: name)
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
- Rule::Set.new(call(node))
44
+ Operations::Set.new(*call(node))
46
45
  end
47
46
 
48
47
  def visit_each(node)
49
- Rule::Each.new(visit(node))
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 == Predicate::Undefined }
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) & visit(right)
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) | visit(right)
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) ^ visit(right)
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) > visit(right)
80
+ visit(left).then(visit(right))
82
81
  end
83
82
  end
84
83
  end
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  module Logic
3
- VERSION = '0.3.0'.freeze
3
+ VERSION = '0.4.0'.freeze
4
4
  end
5
5
  end
@@ -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