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.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +16 -0
  5. data/.rubocop_todo.yml +7 -0
  6. data/.travis.yml +31 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +11 -0
  9. data/LICENSE +20 -0
  10. data/README.md +45 -0
  11. data/Rakefile +12 -0
  12. data/dry-logic.gemspec +24 -0
  13. data/examples/basic.rb +13 -0
  14. data/lib/dry-logic.rb +1 -0
  15. data/lib/dry/logic.rb +10 -0
  16. data/lib/dry/logic/predicate.rb +35 -0
  17. data/lib/dry/logic/predicate_set.rb +23 -0
  18. data/lib/dry/logic/predicates.rb +129 -0
  19. data/lib/dry/logic/result.rb +119 -0
  20. data/lib/dry/logic/rule.rb +91 -0
  21. data/lib/dry/logic/rule/check.rb +15 -0
  22. data/lib/dry/logic/rule/composite.rb +63 -0
  23. data/lib/dry/logic/rule/each.rb +13 -0
  24. data/lib/dry/logic/rule/group.rb +21 -0
  25. data/lib/dry/logic/rule/key.rb +17 -0
  26. data/lib/dry/logic/rule/set.rb +22 -0
  27. data/lib/dry/logic/rule/value.rb +13 -0
  28. data/lib/dry/logic/rule_compiler.rb +81 -0
  29. data/lib/dry/logic/version.rb +5 -0
  30. data/rakelib/rubocop.rake +18 -0
  31. data/spec/shared/predicates.rb +41 -0
  32. data/spec/spec_helper.rb +18 -0
  33. data/spec/unit/predicate_spec.rb +27 -0
  34. data/spec/unit/predicates/bool_spec.rb +34 -0
  35. data/spec/unit/predicates/date_spec.rb +31 -0
  36. data/spec/unit/predicates/date_time_spec.rb +31 -0
  37. data/spec/unit/predicates/decimal_spec.rb +32 -0
  38. data/spec/unit/predicates/empty_spec.rb +38 -0
  39. data/spec/unit/predicates/eql_spec.rb +21 -0
  40. data/spec/unit/predicates/exclusion_spec.rb +35 -0
  41. data/spec/unit/predicates/filled_spec.rb +38 -0
  42. data/spec/unit/predicates/float_spec.rb +31 -0
  43. data/spec/unit/predicates/format_spec.rb +21 -0
  44. data/spec/unit/predicates/gt_spec.rb +40 -0
  45. data/spec/unit/predicates/gteq_spec.rb +40 -0
  46. data/spec/unit/predicates/inclusion_spec.rb +35 -0
  47. data/spec/unit/predicates/int_spec.rb +34 -0
  48. data/spec/unit/predicates/key_spec.rb +29 -0
  49. data/spec/unit/predicates/lt_spec.rb +40 -0
  50. data/spec/unit/predicates/lteq_spec.rb +40 -0
  51. data/spec/unit/predicates/max_size_spec.rb +49 -0
  52. data/spec/unit/predicates/min_size_spec.rb +49 -0
  53. data/spec/unit/predicates/none_spec.rb +28 -0
  54. data/spec/unit/predicates/size_spec.rb +55 -0
  55. data/spec/unit/predicates/str_spec.rb +32 -0
  56. data/spec/unit/predicates/time_spec.rb +31 -0
  57. data/spec/unit/rule/check_spec.rb +29 -0
  58. data/spec/unit/rule/conjunction_spec.rb +30 -0
  59. data/spec/unit/rule/disjunction_spec.rb +38 -0
  60. data/spec/unit/rule/each_spec.rb +20 -0
  61. data/spec/unit/rule/group_spec.rb +12 -0
  62. data/spec/unit/rule/implication_spec.rb +16 -0
  63. data/spec/unit/rule/key_spec.rb +27 -0
  64. data/spec/unit/rule/set_spec.rb +32 -0
  65. data/spec/unit/rule/value_spec.rb +42 -0
  66. data/spec/unit/rule_compiler_spec.rb +123 -0
  67. 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,15 @@
1
+ module Dry
2
+ module Logic
3
+ class Rule::Check < Rule
4
+ alias_method :result, :predicate
5
+
6
+ def call(*)
7
+ Logic.Result(nil, result.call, self)
8
+ end
9
+
10
+ def type
11
+ :check
12
+ end
13
+ end
14
+ end
15
+ end
@@ -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,13 @@
1
+ module Dry
2
+ module Logic
3
+ class Rule::Each < Rule
4
+ def call(input)
5
+ Logic.Result(input, input.map { |element| predicate.(element) }, self)
6
+ end
7
+
8
+ def type
9
+ :each
10
+ end
11
+ end
12
+ end
13
+ 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,13 @@
1
+ module Dry
2
+ module Logic
3
+ class Rule::Value < Rule
4
+ def call(input)
5
+ Logic.Result(input, predicate.(input), self)
6
+ end
7
+
8
+ def type
9
+ :val
10
+ end
11
+ end
12
+ end
13
+ 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,5 @@
1
+ module Dry
2
+ module Logic
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ 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
@@ -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