dry-logic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8ed31141ff3e52c81a9afce4397631ce26372a7e
4
+ data.tar.gz: ee8d692843523fc91d4e9f6d28a03e4e749362b5
5
+ SHA512:
6
+ metadata.gz: 61463a874fb6281033cba937136a44131df14b59b1fab36532786f47f698eeaa96fbca1cdcc8235ee93b69d66e85da5230bd2c15094720c102024ee01355bc0b
7
+ data.tar.gz: 48f375ae23737143a1aea7ba6494f47de0aa86548278f66dedd45333473a177f1a3704e0451777ef1b128669e68006fee952ccf10634f0f928ec28fe5942f738
@@ -0,0 +1,8 @@
1
+ .DS_Store
2
+ coverage
3
+ /.bundle
4
+ vendor/bundle
5
+ bin/
6
+ tmp/
7
+ .idea/
8
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --require spec_helper
3
+ --order random
@@ -0,0 +1,16 @@
1
+ # Generated by `rubocop --auto-gen-config`
2
+ inherit_from: .rubocop_todo.yml
3
+
4
+ Metrics/LineLength:
5
+ Max: 100
6
+
7
+ Style/Documentation:
8
+ Enabled: false
9
+
10
+ Lint/HandleExceptions:
11
+ Exclude:
12
+ - rakelib/*.rake
13
+
14
+ Style/FileName:
15
+ Exclude:
16
+ - lib/dry-logic.rb
@@ -0,0 +1,7 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2015-10-30 01:32:46 +0000 using RuboCop version 0.34.2.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
@@ -0,0 +1,31 @@
1
+ language: ruby
2
+ sudo: false
3
+ cache: bundler
4
+ bundler_args: --without console
5
+ script:
6
+ - bundle exec rake spec
7
+ rvm:
8
+ - 2.0
9
+ - 2.1
10
+ - 2.2
11
+ - 2.3.0
12
+ - rbx-2
13
+ - jruby-9000
14
+ - ruby-head
15
+ - jruby-head
16
+ env:
17
+ global:
18
+ - JRUBY_OPTS='--dev -J-Xmx1024M'
19
+ matrix:
20
+ allow_failures:
21
+ - rvm: ruby-head
22
+ - rvm: jruby-head
23
+ - rvm: jruby-9000
24
+ notifications:
25
+ email: false
26
+ webhooks:
27
+ urls:
28
+ - https://webhooks.gitter.im/e/19098b4253a72c9796db
29
+ on_success: change # options: [always|never|change] default: always
30
+ on_failure: always # options: [always|never|change] default: always
31
+ on_start: false # default: false
@@ -0,0 +1,3 @@
1
+ # v0.1.0 to-be-released
2
+
3
+ Code extracted from dry-validation 0.4.1
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :tools do
6
+ gem 'rubocop'
7
+ gem 'guard'
8
+ gem 'guard-rspec'
9
+ gem 'guard-rubocop'
10
+ gem 'byebug', platform: :mri
11
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Ruby Object Mapper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,45 @@
1
+ [gem]: https://rubygems.org/gems/dry-logic
2
+ [travis]: https://travis-ci.org/dryrb/dry-logic
3
+ [gemnasium]: https://gemnasium.com/dryrb/dry-logic
4
+ [codeclimate]: https://codeclimate.com/github/dryrb/dry-logic
5
+ [coveralls]: https://coveralls.io/r/dryrb/dry-logic
6
+ [inchpages]: http://inch-ci.org/github/dryrb/dry-logic
7
+
8
+ # dry-logic [![Join the chat at https://gitter.im/dryrb/chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dryrb/chat)
9
+
10
+ [![Gem Version](https://badge.fury.io/rb/dry-logic.svg)][gem]
11
+ [![Build Status](https://travis-ci.org/dryrb/dry-logic.svg?branch=master)][travis]
12
+ [![Dependency Status](https://gemnasium.com/dryrb/dry-logic.svg)][gemnasium]
13
+ [![Code Climate](https://codeclimate.com/github/dryrb/dry-logic/badges/gpa.svg)][codeclimate]
14
+ [![Test Coverage](https://codeclimate.com/github/dryrb/dry-logic/badges/coverage.svg)][codeclimate]
15
+ [![Inline docs](http://inch-ci.org/github/dryrb/dry-logic.svg?branch=master)][inchpages]
16
+
17
+ Predicate logic and rule composition used by:
18
+
19
+ * [dry-data](https://github.com/dryrb/dry-data) for constrained types
20
+ * [dry-validation](https://github.com/dryrb/dry-validation) for composing validation rules
21
+ * your project...?
22
+
23
+ ## Synopsis
24
+
25
+ ``` ruby
26
+ require 'dry/logic'
27
+ require 'dry/logic/predicates'
28
+
29
+ include Dry::Logic
30
+
31
+ user_present = Rule::Key.new(:user, Predicates[:key?])
32
+ has_min_age = Rule::Key.new(:age, Predicates[:key?]) & Rule::Value.new(:age, Predicates[:gt?].curry(18))
33
+
34
+ user_rule = user_present & has_min_age
35
+
36
+ user_rule.(user: { age: 19 })
37
+ # #<Dry::Logic::Result::Value success?=true input=19 rule=#<Dry::Logic::Rule::Value name=:age predicate=#<Dry::Logic::Predicate id=:gt?>>>
38
+
39
+ user_rule.(user: { age: 18 })
40
+ # #<Dry::Logic::Result::Value success?=false input=18 rule=#<Dry::Logic::Rule::Value name=:age predicate=#<Dry::Logic::Predicate id=:gt?>>>
41
+ ```
42
+
43
+ ## License
44
+
45
+ See `LICENSE` file.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
5
+
6
+ require 'rspec/core'
7
+ require 'rspec/core/rake_task'
8
+
9
+ task default: :spec
10
+
11
+ desc 'Run all specs in spec directory'
12
+ RSpec::Core::RakeTask.new(:spec)
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ require File.expand_path('../lib/dry/logic/version', __FILE__)
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'dry-logic'
6
+ spec.version = Dry::Logic::VERSION
7
+ spec.authors = ['Piotr Solnica']
8
+ spec.email = ['piotr.solnica@gmail.com']
9
+ spec.summary = 'Predicate logic with rule composition'
10
+ spec.homepage = 'https://github.com/dryrb/dry-logic'
11
+ spec.license = 'MIT'
12
+
13
+ spec.files = `git ls-files -z`.split("\x0")
14
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
15
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.require_paths = ['lib']
17
+
18
+ spec.add_runtime_dependency 'dry-container', '~> 0.2', '>= 0.2.6'
19
+ spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
20
+
21
+ spec.add_development_dependency 'bundler'
22
+ spec.add_development_dependency 'rake'
23
+ spec.add_development_dependency 'rspec'
24
+ end
@@ -0,0 +1,13 @@
1
+ require 'dry/logic'
2
+ require 'dry/logic/predicates'
3
+
4
+ include Dry::Logic
5
+
6
+ user_present = Rule::Key.new(:user, Predicates[:key?])
7
+ has_min_age = Rule::Key.new(:age, Predicates[:key?]) & Rule::Value.new(:age, Predicates[:gt?].curry(18))
8
+
9
+ user_rule = user_present & has_min_age
10
+
11
+ puts user_rule.(user: { age: 19 }).inspect
12
+
13
+ puts user_rule.(user: { age: 18 }).inspect
@@ -0,0 +1 @@
1
+ require 'dry/logic'
@@ -0,0 +1,10 @@
1
+ require 'dry-equalizer'
2
+
3
+ # A collection of micro-libraries, each intended to encapsulate
4
+ # a common task in Ruby
5
+ module Dry
6
+ module Logic
7
+ end
8
+ end
9
+
10
+ require 'dry/logic/rule'
@@ -0,0 +1,35 @@
1
+ module Dry
2
+ module Logic
3
+ def self.Predicate(block)
4
+ case block
5
+ when Method then Predicate.new(block.name, &block)
6
+ else raise ArgumentError, 'predicate needs an :id'
7
+ end
8
+ end
9
+
10
+ class Predicate
11
+ include Dry::Equalizer(:id)
12
+
13
+ attr_reader :id, :args, :fn
14
+
15
+ def initialize(id, *args, &block)
16
+ @id = id
17
+ @fn = block
18
+ @args = args
19
+ end
20
+
21
+ def call(*args)
22
+ fn.(*args)
23
+ end
24
+
25
+ def curry(*args)
26
+ self.class.new(id, *args, &fn.curry.(*args))
27
+ end
28
+
29
+ def to_ary
30
+ [:predicate, [id, args]]
31
+ end
32
+ alias_method :to_a, :to_ary
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ require 'dry/logic/predicate'
2
+ require 'dry-container'
3
+
4
+ module Dry
5
+ module Logic
6
+ module PredicateSet
7
+ module Methods
8
+ def predicate(name, &block)
9
+ register(name) { Predicate.new(name, &block) }
10
+ end
11
+
12
+ def import(predicate_set)
13
+ merge(predicate_set)
14
+ end
15
+ end
16
+
17
+ def self.extended(other)
18
+ super
19
+ other.extend(Methods, Dry::Container::Mixin)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,129 @@
1
+ require 'bigdecimal'
2
+ require 'bigdecimal/util'
3
+ require 'date'
4
+
5
+ require 'dry/logic/predicate_set'
6
+
7
+ module Dry
8
+ module Logic
9
+ module Predicates
10
+ extend Logic::PredicateSet
11
+
12
+ def self.included(other)
13
+ super
14
+ other.extend(Logic::PredicateSet)
15
+ other.import(self)
16
+ end
17
+
18
+ predicate(:none?) do |input|
19
+ input.nil?
20
+ end
21
+
22
+ predicate(:key?) do |name, input|
23
+ input.key?(name)
24
+ end
25
+
26
+ predicate(:empty?) do |input|
27
+ case input
28
+ when String, Array, Hash then input.empty?
29
+ when nil then true
30
+ else
31
+ false
32
+ end
33
+ end
34
+
35
+ predicate(:filled?) do |input|
36
+ !self[:empty?].(input)
37
+ end
38
+
39
+ predicate(:bool?) do |input|
40
+ input.is_a?(TrueClass) || input.is_a?(FalseClass)
41
+ end
42
+
43
+ predicate(:date?) do |input|
44
+ input.is_a?(Date)
45
+ end
46
+
47
+ predicate(:date_time?) do |input|
48
+ input.is_a?(DateTime)
49
+ end
50
+
51
+ predicate(:time?) do |input|
52
+ input.is_a?(Time)
53
+ end
54
+
55
+ predicate(:int?) do |input|
56
+ input.is_a?(Fixnum)
57
+ end
58
+
59
+ predicate(:float?) do |input|
60
+ input.is_a?(Float)
61
+ end
62
+
63
+ predicate(:decimal?) do |input|
64
+ input.is_a?(BigDecimal)
65
+ end
66
+
67
+ predicate(:str?) do |input|
68
+ input.is_a?(String)
69
+ end
70
+
71
+ predicate(:hash?) do |input|
72
+ input.is_a?(Hash)
73
+ end
74
+
75
+ predicate(:array?) do |input|
76
+ input.is_a?(Array)
77
+ end
78
+
79
+ predicate(:lt?) do |num, input|
80
+ input < num
81
+ end
82
+
83
+ predicate(:gt?) do |num, input|
84
+ input > num
85
+ end
86
+
87
+ predicate(:lteq?) do |num, input|
88
+ !self[:gt?].(num, input)
89
+ end
90
+
91
+ predicate(:gteq?) do |num, input|
92
+ !self[:lt?].(num, input)
93
+ end
94
+
95
+ predicate(:size?) do |size, input|
96
+ case size
97
+ when Fixnum then size == input.size
98
+ when Range, Array then size.include?(input.size)
99
+ else
100
+ raise ArgumentError, "+#{size}+ is not supported type for size? predicate"
101
+ end
102
+ end
103
+
104
+ predicate(:min_size?) do |num, input|
105
+ input.size >= num
106
+ end
107
+
108
+ predicate(:max_size?) do |num, input|
109
+ input.size <= num
110
+ end
111
+
112
+ predicate(:inclusion?) do |list, input|
113
+ list.include?(input)
114
+ end
115
+
116
+ predicate(:exclusion?) do |list, input|
117
+ !self[:inclusion?].(list, input)
118
+ end
119
+
120
+ predicate(:eql?) do |left, right|
121
+ left.eql?(right)
122
+ end
123
+
124
+ predicate(:format?) do |regex, input|
125
+ !regex.match(input).nil?
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,119 @@
1
+ module Dry
2
+ module Logic
3
+ def self.Result(input, value, rule)
4
+ case value
5
+ when Result then value.class.new(value.input, value.success?, rule)
6
+ when Array then Result::Set.new(input, value, rule)
7
+ else Result::Value.new(input, value, rule)
8
+ end
9
+ end
10
+
11
+ class Result
12
+ include Dry::Equalizer(:success?, :input, :rule)
13
+
14
+ attr_reader :input, :value, :rule, :name
15
+
16
+ class Result::Set < Result
17
+ def success?
18
+ value.all?(&:success?)
19
+ end
20
+
21
+ def to_ary
22
+ indices = value.map { |v| v.failure? ? value.index(v) : nil }.compact
23
+ [:input, [rule.name, input, value.values_at(*indices).map(&:to_ary)]]
24
+ end
25
+ end
26
+
27
+ class Result::Value < Result
28
+ def to_ary
29
+ [:input, [rule.name, input, [rule.to_ary]]]
30
+ end
31
+ alias_method :to_a, :to_ary
32
+ end
33
+
34
+ class Result::Verified < Result
35
+ attr_reader :predicate_id
36
+
37
+ def initialize(result, predicate_id)
38
+ @input = result.input
39
+ @value = result.value
40
+ @rule = result.rule
41
+ @name = result.name
42
+ @predicate_id = predicate_id
43
+ end
44
+
45
+ def call
46
+ Logic.Result(input, success?, rule)
47
+ end
48
+
49
+ def to_ary
50
+ [:input, [name, input, [rule.to_ary]]]
51
+ end
52
+ alias_method :to_a, :to_ary
53
+
54
+ def success?
55
+ rule.predicate_id == predicate_id
56
+ end
57
+ end
58
+
59
+ def initialize(input, value, rule)
60
+ @input = input
61
+ @value = value
62
+ @rule = rule
63
+ @name = rule.name
64
+ end
65
+
66
+ def call
67
+ self
68
+ end
69
+
70
+ def curry(predicate_id = nil)
71
+ if predicate_id
72
+ Result::Verified.new(self, predicate_id)
73
+ else
74
+ self
75
+ end
76
+ end
77
+
78
+ def negated
79
+ self.class.new(input, !value, rule)
80
+ end
81
+
82
+ def >(other)
83
+ if success?
84
+ other.(input)
85
+ else
86
+ Logic.Result(input, true, rule)
87
+ end
88
+ end
89
+
90
+ def and(other)
91
+ if success?
92
+ other.(input)
93
+ else
94
+ self
95
+ end
96
+ end
97
+
98
+ def or(other)
99
+ if success?
100
+ self
101
+ else
102
+ other.(input)
103
+ end
104
+ end
105
+
106
+ def xor(other)
107
+ Logic.Result(input, success? ^ other.(input).success?, rule)
108
+ end
109
+
110
+ def success?
111
+ @value
112
+ end
113
+
114
+ def failure?
115
+ ! success?
116
+ end
117
+ end
118
+ end
119
+ end