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