validation_delegation 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZmMyYTIyMzkzMDI5ZDU0MDQ4OWEwNGI5MzNhNzdlMGExOWE1MGJkZg==
5
+ data.tar.gz: !binary |-
6
+ ZTZkZjMzZjFmMjIwM2M1NDc0OTAwYjliNThiZWQ2Y2YzYmQ3NjBkYw==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ YjFkYTc0Yzk3OWVhYjIwOGQ1MDVkZjRiMGM5OGJiYmM2ZjNjNTk5MGZjNTNi
10
+ Mzc2YjMxN2UwMjQ3NDY2N2E5OWZlNGJkYjQ2ZmUxMjU5M2NiYTM3ZDlhYTIx
11
+ M2Q4MjFiZTlkMzE0NmU5NWNmOGNjM2FjYzRjNGI1YTIwYTZiYzY=
12
+ data.tar.gz: !binary |-
13
+ NDE4ZjRjNGYwNWQ2ODk1NDFmOTc2Y2QxY2E1Y2IyZDc2OGVlYzQ3MTRjYWEy
14
+ ZDM5NTQxYTBmOGM3ZTBhZDljZDlhMGRmMjNhYjMyMTcyNjliMzUyZTBmZTU0
15
+ MWJiOTM3MjdjODA2MGZjY2I5NDY5MjMxMjI1OGNiN2QyYmIxYzE=
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in validation_delegation.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Ben Eddy
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # Validation Delegation
2
+
3
+ Validation delegation allows an object to proxy validations to other objects. This facilitates [composition](http://en.wikipedia.org/wiki/Object_composition) and prevents the duplication of validation logic.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'validation_delegation'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install validation_delegation
18
+
19
+ ## Usage
20
+
21
+ An example use case for validation delegation is a `SignUp` object which simultaneously creates a `User` and an `Organization`. The `SignUp` object is only valid if both the user and organization are valid.
22
+
23
+ ```Ruby
24
+ class User < ActiveRecord::Base
25
+ validates :email, presence: true
26
+ end
27
+
28
+ class Organization < ActiveRecord::Base
29
+ validates :name, presence: true
30
+ end
31
+
32
+ class SignUp
33
+ include ActiveModel::Validations
34
+
35
+ # delegate validation to the user
36
+ delegate_validation to: :user
37
+
38
+ # and also delegate validation to the organization
39
+ delegate_validation to: :organization
40
+
41
+ attr_reader :user, :organization
42
+
43
+ def initialize
44
+ @user = user
45
+ @organization = organization
46
+ end
47
+
48
+ def email=(email)
49
+ @user.email = email
50
+ end
51
+
52
+ def name=(name)
53
+ @organization.name = name
54
+ end
55
+ end
56
+ ```
57
+
58
+ Assigning invalid user and organization attributes, which are in turn assigned to the `@user` and `@organization` instance variables, invalidates the `SignUp`, and faithfully copies the user and organization errors.
59
+
60
+ ```Ruby
61
+ signup = SignUp.new
62
+ signup.email = ""
63
+ signup.name = ""
64
+
65
+ signup.valid?
66
+ # => false
67
+
68
+ signup.errors.full_messages
69
+ # => ["email can't be blank", "name can't be blank"]
70
+
71
+ ```
72
+
73
+ ```Ruby
74
+ signup.email = "email@example.com"
75
+ signup.name = "My Organization"
76
+
77
+ signup.valid?
78
+
79
+ ```
80
+
81
+ If you do not want to copy errors directly onto the composing object, you can specify to which attribute the errors should apply. In this case, we copy errors onto the "organization" attribute. This is useful for nesting forms via `fields_for`.
82
+
83
+ ```Ruby
84
+ class SignUp
85
+ include ActiveModel::Model
86
+
87
+ delegate_validation :organization, to: :organization
88
+
89
+ attr_reader :organization
90
+
91
+ def initialize
92
+ @organization = Organization.new
93
+ end
94
+
95
+ def name=(name)
96
+ @organization.name = name
97
+ end
98
+ end
99
+
100
+ signup = SignUp.new
101
+ signup.name = ""
102
+ signup.valid?
103
+ # => false
104
+
105
+ signup.errors.full_messages
106
+ # => ["organization name can't be blank"]
107
+ ```
108
+
109
+ ### Options
110
+
111
+ `delegate_validation` accepts several options.
112
+
113
+ - `:if` - errors are only copied if the method specified by the `:if` option returns true
114
+
115
+ ```Ruby
116
+ class SignUp
117
+ delegate_validation to: :user, if: :validate_user?
118
+
119
+ def validate_user?
120
+ # logic
121
+ end
122
+ end
123
+ ```
124
+
125
+ - `:unless` - errors are only copied if the method specified by the `:unless` option returns false
126
+
127
+ ```Ruby
128
+ class SignUp
129
+ delegate_validation to: :user, unless: :skip_validation?
130
+
131
+ def skip_validation?
132
+ # logic
133
+ end
134
+ end
135
+ ```
136
+
137
+ - `:only` - a whitelist of errors to be copied
138
+
139
+ ```Ruby
140
+ class SignUp
141
+ delegate_validation to: :user, only: :email
142
+ end
143
+
144
+ signup = SignUp.new
145
+ signup.user.errors.add(:email, :required)
146
+ signup.user.errors.add(:phone_number, :required)
147
+
148
+ signup.valid?
149
+ signup.errors.full_messages
150
+ # => ["email can't be blank"]
151
+ ```
152
+
153
+ - `:except` - a blacklist of errors to be copied
154
+
155
+ ```Ruby
156
+ class SignUp
157
+ delegate_validation to: :user, except: :email
158
+ end
159
+
160
+ signup = SignUp.new
161
+ signup.user.errors.add(:email, :required)
162
+ signup.user.errors.add(:phone_number, :required)
163
+
164
+ signup.valid?
165
+ signup.errors.full_messages
166
+ # => ["phone number can't be blank"]
167
+ ```
168
+
169
+ ## Contributing
170
+
171
+ 1. Fork it
172
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
173
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
174
+ 4. Push to the branch (`git push origin my-new-feature`)
175
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,3 @@
1
+ module ValidationDelegation
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,82 @@
1
+ require "validation_delegation/version"
2
+ require "active_support"
3
+ require "active_support/core_ext"
4
+
5
+ module ValidationDelegation
6
+ def self.included(base)
7
+ base.send(:extend, ClassMethods)
8
+ end
9
+
10
+ def receive_errors(attribute, object_attribute, errors)
11
+ Array.wrap(errors).each{ |error| receive_error attribute, object_attribute, error }
12
+ end
13
+
14
+ def receive_error(attribute, object_attribute, error)
15
+ attribute ? receive_error_on_attribute(attribute, object_attribute, error) : receive_error_on_self(object_attribute, error)
16
+ end
17
+
18
+ def receive_error_on_attribute(attribute, prefix, error)
19
+ errors[attribute] << [format_attribute(prefix), error].join(" ")
20
+ end
21
+
22
+ def receive_error_on_self(attribute, error)
23
+ errors[attribute] << error
24
+ end
25
+
26
+ def format_attribute(attribute_name)
27
+ attribute_name.to_s.humanize.downcase.gsub(/\./, " ")
28
+ end
29
+
30
+ module ClassMethods
31
+ def delegate_validation(*args)
32
+ if args.first.is_a?(Hash)
33
+ transplant_errors(args.first)
34
+ else
35
+ transplant_errors(args.last.merge(attribute: args.first))
36
+ end
37
+ end
38
+
39
+ def transplant_errors(options)
40
+ validate do
41
+ return unless send(options[:if]) if options[:if]
42
+ return if send(options[:unless]) if options[:unless]
43
+
44
+ object = send(options[:to])
45
+ ErrorTransplanter.new(self, object, options).transplant unless object.valid?
46
+ end
47
+ end
48
+ end
49
+
50
+ class ErrorTransplanter
51
+ delegate :errors, to: :donor
52
+ delegate :receive_errors, to: :recipient
53
+
54
+ attr_accessor :recipient, :donor, :options
55
+
56
+ def initialize(recipient, donor, options)
57
+ self.recipient = recipient
58
+ self.donor = donor
59
+ self.options = options
60
+ end
61
+
62
+ def transplant
63
+ errors.each do |object_attribute, object_errors|
64
+ next if ignore_attribute? object_attribute
65
+ receive_errors options[:attribute], object_attribute, object_errors
66
+ end
67
+ end
68
+
69
+ def ignore_attribute?(attribute)
70
+ excepted_attribute?(attribute) || !specified_attribute?(attribute)
71
+ end
72
+
73
+ def excepted_attribute?(attribute)
74
+ Array.wrap(options[:except]).include?(attribute.to_sym)
75
+ end
76
+
77
+ def specified_attribute?(attribute)
78
+ return true unless options[:only]
79
+ Array.wrap(options[:only]).include?(attribute.to_sym)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,147 @@
1
+ require "active_model"
2
+ require "validation_delegation"
3
+
4
+ class TestClass ; end
5
+
6
+ class Validator
7
+ include ActiveModel::Validations
8
+ end
9
+
10
+ RSpec::Matchers.define :have_error do |message|
11
+ match do |actual|
12
+ actual.errors[@attribute].include?(message)
13
+ end
14
+
15
+ chain :on do |attribute|
16
+ @attribute = attribute
17
+ end
18
+ end
19
+
20
+ describe ValidationDelegation do
21
+ let(:validator) { Validator.new }
22
+ let(:test) { TestClass.new }
23
+
24
+ before do
25
+ Object.send(:remove_const, "TestClass")
26
+
27
+ class TestClass
28
+ include ActiveModel::Validations
29
+ include ValidationDelegation
30
+ end
31
+
32
+ allow(validator).to receive(:valid?).and_return(false)
33
+ validator.errors[:attr] << "is invalid"
34
+ allow(test).to receive(:validator).and_return(validator)
35
+ end
36
+
37
+ subject { test.tap(&:valid?) }
38
+
39
+ it "copies errors from the validator to itself" do
40
+ TestClass.delegate_validation to: :validator
41
+ expect(subject).to have_error("is invalid").on(:attr)
42
+ end
43
+
44
+ it "copies all errors" do
45
+ validator.errors[:attr] << "is bananas"
46
+ TestClass.delegate_validation to: :validator
47
+
48
+ expect(subject).to have_error("is invalid").on(:attr)
49
+ expect(subject).to have_error("is bananas").on(:attr)
50
+ end
51
+
52
+ it "copies errors if the :if option is true" do
53
+ allow(test).to receive(:validate?).and_return(true)
54
+ TestClass.delegate_validation to: :validator, if: :validate?
55
+ expect(subject).to have_error("is invalid").on(:attr)
56
+ end
57
+
58
+ it "does not copy errors if the :if option is false" do
59
+ allow(test).to receive(:validate?).and_return(false)
60
+ TestClass.delegate_validation to: :validator, if: :validate?
61
+ expect(subject).to_not have_error("is invalid").on(:attr)
62
+ end
63
+
64
+ it "copies errors if the :unless option is false" do
65
+ allow(test).to receive(:validate?).and_return(false)
66
+ TestClass.delegate_validation to: :validator, unless: :validate?
67
+ expect(subject).to have_error("is invalid").on(:attr)
68
+ end
69
+
70
+ it "does not copy errors if the :unless option is true" do
71
+ allow(test).to receive(:validate?).and_return(true)
72
+ TestClass.delegate_validation to: :validator, unless: :validate?
73
+ expect(subject).to_not have_error("is invalid").on(:attr)
74
+ end
75
+
76
+ it "does not copy errors for excluded attributes" do
77
+ allow(test).to receive(:validate?).and_return(true)
78
+ TestClass.delegate_validation to: :validator, except: :attr
79
+ expect(subject).to_not have_error("is invalid").on(:attr)
80
+ end
81
+
82
+ it "only copies errors for specified attributes" do
83
+ allow(test).to receive(:validate?).and_return(true)
84
+ TestClass.delegate_validation to: :validator, only: :attr
85
+ expect(subject).to have_error("is invalid").on(:attr)
86
+ end
87
+
88
+ context "an method is supplied" do
89
+ it "copies errors to the specified method and prefixes the attribute name" do
90
+ TestClass.delegate_validation :my_method, to: :validator
91
+ expect(subject).to have_error("attr is invalid").on(:my_method)
92
+ end
93
+
94
+ it "copies all errors" do
95
+ allow(validator).to receive(:errors).and_return({attr: ["is invalid", "is bananas"]})
96
+ TestClass.delegate_validation :my_method, to: :validator
97
+
98
+ expect(subject).to have_error("attr is invalid").on(:my_method)
99
+ expect(subject).to have_error("attr is bananas").on(:my_method)
100
+ end
101
+
102
+ it "reformats nested attribute errors" do
103
+ TestClass.delegate_validation :my_method, to: :validator
104
+ validator.errors[:"associated_class.attribute"] << "is bananas"
105
+
106
+ expect(subject).to have_error("associated class attribute is bananas").on(:my_method)
107
+ end
108
+
109
+ it "copies errors if the :if option is true" do
110
+ allow(test).to receive(:validate?).and_return(true)
111
+ TestClass.delegate_validation :my_method, to: :validator, if: :validate?
112
+
113
+ expect(subject).to have_error("attr is invalid").on(:my_method)
114
+ end
115
+
116
+ it "does not copy errors if the :if option is false" do
117
+ allow(test).to receive(:validate?).and_return(false)
118
+ TestClass.delegate_validation :my_method, to: :validator, if: :validate?
119
+ expect(subject).to_not have_error("attr is invalid").on(:my_method)
120
+ end
121
+
122
+ it "copies errors if the :unless option is false" do
123
+ allow(test).to receive(:validate?).and_return(false)
124
+ TestClass.delegate_validation :my_method, to: :validator, unless: :validate?
125
+ expect(subject).to have_error("attr is invalid").on(:my_method)
126
+ end
127
+
128
+ it "does not copy errors if the :unless option is true" do
129
+ allow(test).to receive(:validate?).and_return(true)
130
+ TestClass.delegate_validation :my_method, to: :validator, unless: :validate?
131
+ expect(subject).to_not have_error("attr is invalid").on(:my_method)
132
+ end
133
+
134
+ it "does not copy errors for excluded attributes" do
135
+ allow(test).to receive(:validate?).and_return(true)
136
+ TestClass.delegate_validation :my_method, to: :validator, except: :attr
137
+ expect(subject).to_not have_error("attr is invalid").on(:my_method)
138
+ end
139
+
140
+ it "only copies errors for specified attributes" do
141
+ allow(test).to receive(:validate?).and_return(true)
142
+ TestClass.delegate_validation :my_method, to: :validator, only: :attr
143
+ expect(subject).to have_error("attr is invalid").on(:my_method)
144
+ end
145
+ end
146
+
147
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require "validation_delegation/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "validation_delegation"
9
+ spec.version = ValidationDelegation::VERSION
10
+ spec.authors = ["Ben Eddy"]
11
+ spec.email = ["bae@foraker.com"]
12
+ spec.description = %q{Delegates validation between objects}
13
+ spec.summary = %q{Validation delegation allows an object to proxy validations to other objects. This facilitates composition and prevents the duplication of validation logic.}
14
+ spec.homepage = "https://github.com/foraker/validation_delegation"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files`.split($/)
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "activesupport", "~> 3.1"
23
+ spec.add_dependency "activemodel", "~> 3.1"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec"
28
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: validation_delegation
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ben Eddy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activemodel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '3.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '3.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Delegates validation between objects
84
+ email:
85
+ - bae@foraker.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - .gitignore
91
+ - Gemfile
92
+ - LICENSE.txt
93
+ - README.md
94
+ - Rakefile
95
+ - lib/validation_delegation.rb
96
+ - lib/validation_delegation/version.rb
97
+ - spec/validation_delegation_spec.rb
98
+ - validation_delegation.gemspec
99
+ homepage: https://github.com/foraker/validation_delegation
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ! '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ! '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 2.0.3
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: Validation delegation allows an object to proxy validations to other objects.
123
+ This facilitates composition and prevents the duplication of validation logic.
124
+ test_files:
125
+ - spec/validation_delegation_spec.rb
126
+ has_rdoc: