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