nested_validator 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MDVlMmQxNWU4MjFkZjYxMmZkY2I2OGVlNzM0YzE4NzcwMGMwMjExYw==
5
+ data.tar.gz: !binary |-
6
+ MTk5MmQ5MmRmMWE4NGZjMzQxOGU2ZWUwNWRiMWMyMDI3NTAwNWZmMA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ YjNiNDNhNjc0YTBkYzQzN2ViMzViNDY4M2YzODZmYWMyYzQ2ZTY2NWI2M2Yy
10
+ YTQ3NWE3OGRmNjA4YmIzNGNlMTY5NDgyMzJiYWQyNDQwYzE1MTIyMmY3MmM5
11
+ NzA3M2E0YjczMDk5N2Y2NzA1Mjg1OGU0MDE2M2NhODVjMDkxM2U=
12
+ data.tar.gz: !binary |-
13
+ OGJjZDAyOTMzYWNjNzgwZmY2YzYzMWNkYTA3OTIxNWVhNDM0MDVhNjk0OGVk
14
+ MDU0YWJmY2FkMGI5MTdlZTcyM2ExMjBlNWQ1NjllYTI3MmQyN2IxZjc0OWM5
15
+ OTE2MGI3ZWU0MjkyNzAyZTQ0Y2M0ZWYyMzQzOWMyYWE0MjJlZWI=
data/.gitignore ADDED
File without changes
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1
6
+ - ruby-head
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,73 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ nested_validator (1.0.0)
5
+ activemodel
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (4.1.0)
11
+ activesupport (= 4.1.0)
12
+ builder (~> 3.1)
13
+ activesupport (4.1.0)
14
+ i18n (~> 0.6, >= 0.6.9)
15
+ json (~> 1.7, >= 1.7.7)
16
+ minitest (~> 5.1)
17
+ thread_safe (~> 0.1)
18
+ tzinfo (~> 1.1)
19
+ builder (3.2.2)
20
+ coveralls (0.7.2)
21
+ multi_json (~> 1.3)
22
+ rest-client (= 1.6.7)
23
+ simplecov (>= 0.7)
24
+ term-ansicolor (= 1.2.2)
25
+ thor (= 0.18.1)
26
+ diff-lcs (1.2.5)
27
+ docile (1.1.5)
28
+ i18n (0.6.11)
29
+ json (1.8.1)
30
+ mime-types (2.4.3)
31
+ minitest (5.3.3)
32
+ multi_json (1.10.1)
33
+ rake (10.4.2)
34
+ rest-client (1.6.7)
35
+ mime-types (>= 1.16)
36
+ rspec (3.1.0)
37
+ rspec-core (~> 3.1.0)
38
+ rspec-expectations (~> 3.1.0)
39
+ rspec-mocks (~> 3.1.0)
40
+ rspec-core (3.1.7)
41
+ rspec-support (~> 3.1.0)
42
+ rspec-expectations (3.1.2)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.1.0)
45
+ rspec-its (1.1.0)
46
+ rspec-core (>= 3.0.0)
47
+ rspec-expectations (>= 3.0.0)
48
+ rspec-mocks (3.1.3)
49
+ rspec-support (~> 3.1.0)
50
+ rspec-support (3.1.2)
51
+ simplecov (0.9.1)
52
+ docile (~> 1.1.0)
53
+ multi_json (~> 1.0)
54
+ simplecov-html (~> 0.8.0)
55
+ simplecov-html (0.8.0)
56
+ term-ansicolor (1.2.2)
57
+ tins (~> 0.8)
58
+ thor (0.18.1)
59
+ thread_safe (0.3.3)
60
+ tins (0.13.2)
61
+ tzinfo (1.1.0)
62
+ thread_safe (~> 0.1)
63
+
64
+ PLATFORMS
65
+ ruby
66
+
67
+ DEPENDENCIES
68
+ bundler (~> 1.6)
69
+ coveralls
70
+ nested_validator!
71
+ rake
72
+ rspec (~> 3.0)
73
+ rspec-its
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Eric Roberts
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,194 @@
1
+ [![Gem Version](https://badge.fury.io/rb/percentable.png)](http://badge.fury.io/rb/percentable)
2
+ [![Build Status](https://travis-ci.org/ericroberts/percentable.png?branch=master)](https://travis-ci.org/ericroberts/percentable)
3
+ [![Code Climate](https://codeclimate.com/github/ericroberts/percentable.png)](https://codeclimate.com/github/ericroberts/percentable)
4
+ [![Coverage Status](https://coveralls.io/repos/ericroberts/percentable/badge.png?branch=master)](https://coveralls.io/r/ericroberts/percentable?branch=master)
5
+
6
+ # Nested Validator
7
+
8
+ Nested validations allow a parent's class validity to include those of child
9
+ attributes. Errors messages will be copied from the child attribute to the parent.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'nested_validator'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install nested_validator
24
+
25
+ ## Usage
26
+
27
+ Assume we have a parent object and we want its validaty to depend on its child validity:
28
+
29
+ ``` ruby
30
+ class ParentBase
31
+ include ActiveModel::Validations
32
+
33
+ attr_accessor :child
34
+
35
+ def initialize
36
+ self.child = Child.new
37
+ end
38
+ end
39
+
40
+ class Child
41
+ include ActiveModel::Validations
42
+
43
+ attr_accessor :attribute1, :attribute2
44
+
45
+ validates :attribute1, presence: true
46
+ validates :attribute2, presence: true
47
+ end
48
+ ```
49
+
50
+ Well, we can use a nested validation to do just that:
51
+
52
+ ``` ruby
53
+ class Parent < ParentBase
54
+ validates :child, nested: true
55
+ end
56
+
57
+ parent = Parent.new
58
+ parent.valid?
59
+ puts parent.errors.messages
60
+
61
+ # => {:"child attribute1"=>["can't be blank"], :"child attribute2"=>["can't be blank"]}
62
+ ```
63
+ ### What if I want to validate with just some of the child attributes?
64
+
65
+ You can use an ```only``` option like this:
66
+
67
+ ``` ruby
68
+ class ParentOnly < ParentBase
69
+ validates :child, nested: { only: :attribute1 }
70
+ end
71
+
72
+ parent = ParentOnly.new
73
+ parent.valid?
74
+ puts parent.errors.messages
75
+
76
+ # => {:"child attribute1"=>["can't be blank"]}
77
+ ```
78
+
79
+ You can also provide an array of attributes to ```only``` if you want to include more than one.
80
+
81
+ ### OK, is there a way to exclude some child attributes?
82
+
83
+ Sure thing. You can use the ```except``` option:
84
+
85
+ ``` ruby
86
+ class ParentExcept < ParentBase
87
+ validates :child, nested: { except: :attribute2 }
88
+ end
89
+
90
+ parent = ParentExcept.new
91
+ parent.valid?
92
+ puts parent.errors.messages
93
+
94
+ # => {:"child attribute1"=>["can't be blank"]}
95
+ ```
96
+
97
+ ### Alright, what if I want a custom message?
98
+
99
+ You can specify a ```prefix``` instead of the child's attribute name:
100
+
101
+ ``` ruby
102
+ class ParentPrefix < ParentBase
103
+ validates :child, nested: { only: :attribute1, prefix: 'OMG'}
104
+ end
105
+
106
+ parent = ParentPrefix.new
107
+ parent.valid?
108
+ puts parent.errors.messages
109
+
110
+ # => {:"OMG attribute1"=>["can't be blank"]}
111
+ ```
112
+
113
+ ### What happens if the child is an Array or Hash?
114
+
115
+ In this case, each value in the array or hash will be validated and the error message will
116
+ include the index or key of the value.
117
+
118
+ For an array:
119
+
120
+ ``` ruby
121
+ class ParentArray < ParentBase
122
+ validates :child, nested: { only: :attribute1 }
123
+ end
124
+
125
+ parent = ParentArray.new
126
+ parent.child = [Child.new] * 2
127
+ parent.valid?
128
+ puts parent.errors.messages
129
+
130
+ # => {:"child[0] attribute1"=>["can't be blank"], :"child[1] attribute1"=>["can't be blank"]}
131
+ ```
132
+
133
+ For a hash:
134
+
135
+ ``` ruby
136
+ class ParentHash < ParentBase
137
+ validates :child, nested: { only: :attribute1 }
138
+ end
139
+
140
+ parent = ParentHash.new
141
+ parent.child = { thing1: Child.new, thing2: Child.new }
142
+ parent.valid?
143
+ puts parent.errors.messages
144
+
145
+ # => {:"child[thing1] attribute1"=>["can't be blank"], :"child[thing2] attribute1"=>["can't be blank"]}
146
+ ```
147
+
148
+ ### Can I easily use this for multiple child attributes?
149
+
150
+ You can use the ```validates_nested``` method:
151
+
152
+ ``` ruby
153
+ class ParentMultiple < ParentBase
154
+ attr_accessor :child2
155
+
156
+ validates_nested :child, :child2, only: :attribute1
157
+
158
+ def initialize
159
+ self.child = Child.new
160
+ self.child2 = Child.new
161
+ end
162
+ end
163
+
164
+ parent = ParentMultiple.new
165
+ parent.valid?
166
+ puts parent.errors.messages
167
+
168
+ # => {:"child attribute1"=>["can't be blank"], :"child2 attribute1"=>["can't be blank"]}
169
+ ```
170
+
171
+ ## Testing With RSpec
172
+
173
+ When you ```require nested_validator``` you will have access to the RSpec matcher ```validate_nested```
174
+ that you can use in your specs.
175
+
176
+ Here are some examples:
177
+
178
+ ``` ruby
179
+ describe Parent do
180
+ it { should validate_nested(:child) }
181
+ it { should validate_nested(:child).with_prefix(:thing1) }
182
+ it { should validate_nested(:child).only(:attribute1) }
183
+ it { should validate_nested(:child).only(:attribute1, :attribute2) }
184
+ it { should validate_nested(:child).except(:attribute1) }
185
+ it { should validate_nested(:child).except(:attribute1, :attribute2) }
186
+ end
187
+ ```
188
+ ## Contributing
189
+
190
+ 1. Fork it ( https://github.com/dwhelan/nested_validator/fork )
191
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
192
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
193
+ 4. Push to the branch (`git push origin my-new-feature`)
194
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,3 @@
1
+ require_relative 'nested_validator/version'
2
+ require_relative 'nested_validator/nested_validator'
3
+ require_relative 'nested_validator/validate_nested_matcher'
@@ -0,0 +1,71 @@
1
+ require 'active_model'
2
+ require 'active_support/core_ext/array'
3
+
4
+ module ActiveModel
5
+ module Validations
6
+ class NestedValidator < EachValidator
7
+
8
+ private
9
+
10
+ def validate_each(record, attribute, values)
11
+ with_each_value(values) do |index, value|
12
+ prefix = prefix(attribute, index, include_index?(values))
13
+ record_error(record, prefix, value) if value.invalid?
14
+ end
15
+ end
16
+
17
+ def with_each_value(values, &block)
18
+ case values
19
+ when Hash
20
+ values.each { |key, value| block.call key, value }
21
+ else
22
+ Array.wrap(values).each_with_index { |value, index| block.call index, value}
23
+ end
24
+ end
25
+
26
+ def include_index?(values)
27
+ values.respond_to? :each
28
+ end
29
+
30
+ def prefix(attribute, index, include_index)
31
+ prefix = (options.has_key?(:prefix) ? options[:prefix] : attribute).to_s
32
+ prefix << "[#{index}]" if include_index
33
+ prefix
34
+ end
35
+
36
+ def record_error(record, prefix, value)
37
+ value.errors.each do |key, error|
38
+ record.errors.add(nested_key(prefix, key), error) if include?(key)
39
+ end
40
+ end
41
+
42
+ def nested_key(prefix, key)
43
+ "#{prefix} #{key}".strip.to_sym
44
+ end
45
+
46
+ def include?(key)
47
+ if options[:only]
48
+ only.any?{|k| key =~ /^#{k}/}
49
+ elsif options[:except]
50
+ except.none?{|k| key =~ /^#{k}/}
51
+ else
52
+ true
53
+ end
54
+ end
55
+
56
+ def only
57
+ @only ||= Array.wrap(options[:only])
58
+ end
59
+
60
+ def except
61
+ @except ||= Array.wrap(options[:except])
62
+ end
63
+ end
64
+
65
+ module HelperMethods
66
+ def validates_nested(*attributes)
67
+ validates_with NestedValidator, _merge_attributes(attributes)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,135 @@
1
+ # RSpec matcher to spec nested validations.
2
+ #
3
+ # You can use symbols or strings for any values.
4
+ #
5
+ # Usage:
6
+ #
7
+ # describe Parent do
8
+ # it { should validate_nested(:child) }
9
+ # it { should validate_nested(:child).with_prefix(:thing1) }
10
+ # it { should validate_nested(:child).only(:attribute1) }
11
+ # it { should validate_nested(:child).only(:attribute1, :attribute2) }
12
+ # it { should validate_nested(:child).except(:attribute1) }
13
+ # end
14
+
15
+ RSpec::Matchers.define :validate_nested do |child_name|
16
+
17
+ attr_accessor :child_name, :prefix, :only_keys, :except_keys # inputs
18
+ attr_accessor :parent, :actual_keys
19
+
20
+ TEST_KEY ||= :__test_key__
21
+
22
+ match do |parent|
23
+ self.prefix ||= ''
24
+ self.only_keys ||= []
25
+ self.except_keys ||= []
26
+
27
+ self.child_name = child_name
28
+ self.parent = parent
29
+
30
+ return false unless parent.respond_to? child_name
31
+
32
+ self.actual_keys = (error_keys_when_child_validity_is(false) - error_keys_when_child_validity_is(true))
33
+
34
+ actual_keys == expected_keys
35
+ end
36
+
37
+ chain(:with_prefix) { |prefix| self.prefix = prefix }
38
+ chain(:only) { |*only| self.only_keys = only }
39
+ chain(:except) { |*except| self.except_keys = except }
40
+
41
+ def child
42
+ parent.send child_name
43
+ end
44
+
45
+ def error_keys_when_child_validity_is(valid)
46
+ child_error_keys = combine TEST_KEY, only_keys, except_keys
47
+ child_errors = child_error_keys.inject({}){|result, key| result[key] = ['error message'];result }
48
+
49
+ allow(child).to receive(:valid?) { valid }
50
+ allow(child).to receive(:errors) { valid ? [] : child_errors }
51
+
52
+ parent.valid?
53
+ parent.errors.keys
54
+ end
55
+
56
+ def expected_keys
57
+ expected_child_keys.map{|key| :"#{expected_prefix} #{key}"}
58
+ end
59
+
60
+ def expected_prefix
61
+ prefix.present? ? prefix : child_name
62
+ end
63
+
64
+ def actual_prefix
65
+ :"#{actual_keys.first.to_s.split.first}"
66
+ end
67
+
68
+ def expected_child_keys
69
+ expected_keys = only_keys.present? ? only_keys : [TEST_KEY]
70
+ unique_except_keys = except_keys - only_keys
71
+ combine expected_keys - unique_except_keys
72
+ end
73
+
74
+ def actual_child_keys
75
+ actual_keys.map{|key| key.to_s.sub(/^.*\s+/, '').to_sym }
76
+ end
77
+
78
+ def invalid_child_keys
79
+ (only_keys + except_keys).reject{|key| child.respond_to? key}
80
+ end
81
+
82
+ description do
83
+ message = "validate nested #{show child_name}"
84
+ message << " with only: #{show only_keys}" if only_keys.present?
85
+ message << " except: #{show except_keys}" if except_keys.present?
86
+ message << " with prefix #{show prefix}" if prefix.present?
87
+ message
88
+ end
89
+
90
+ failure_message do
91
+ case
92
+ when !parent.respond_to?(child_name)
93
+ "#{parent} doesn't respond to #{show child_name}"
94
+ when invalid_child_keys.present?
95
+ "#{child_name} doesn't respond to #{show invalid_child_keys}"
96
+ when (missing_child_keys = expected_child_keys - actual_child_keys - invalid_child_keys - [TEST_KEY]).present?
97
+ "#{parent} doesn't nest validations for: #{show missing_child_keys}"
98
+ when actual_keys.empty?
99
+ "parent doesn't nest validations for #{show child_name}"
100
+ when actual_prefix != expected_prefix
101
+ if prefix.present?
102
+ "parent uses a prefix of #{show actual_prefix} rather than #{show expected_prefix}"
103
+ else
104
+ "parent has a prefix of #{show actual_prefix}. Are you missing '.with_prefix(#{show actual_prefix})'?"
105
+ end
106
+ else
107
+ "parent does nest validations for: #{show except_keys & actual_child_keys}"
108
+ end
109
+ end
110
+
111
+ failure_message_when_negated do
112
+ case
113
+ when !parent.respond_to?(child_name)
114
+ "#{parent} doesn't respond to #{show child_name}"
115
+ when (extras = only_keys & actual_child_keys).present?
116
+ "#{parent} does nest #{show child_name} validations for: #{show extras}"
117
+ when invalid_child_keys.present?
118
+ "#{child_name} doesn't respond to #{show invalid_child_keys}"
119
+ when except_keys.present?
120
+ "#{parent} doesn't nest #{show child_name} validations for: #{show except_keys - actual_child_keys}"
121
+ when prefix.present?
122
+ "#{parent} does nest validations for: #{show child_name} with a prefix of #{show prefix}"
123
+ else
124
+ "#{parent} does nest validations for: #{show child_name}"
125
+ end
126
+ end
127
+
128
+ def show(value)
129
+ Array.wrap(value).map{|key| key.is_a?(Symbol) ? ":#{key}" : key.to_s}.join(', ')
130
+ end
131
+
132
+ def combine(*keys)
133
+ keys.flatten.compact
134
+ end
135
+ end
@@ -0,0 +1,3 @@
1
+ module NestedValidator
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'nested_validator/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'nested_validator'
9
+ spec.version = NestedValidator::VERSION
10
+ spec.authors = ['Declan Whelan']
11
+ spec.email = ['declanpwhelan@gmail.com']
12
+ spec.summary = 'A validator that supports nesting.'
13
+ spec.description = "Nested validations allow a parent's class validity to include those of child attributes. Errors messages will be copied from the child attribute to the parent."
14
+ spec.homepage = 'https://github.com/dwhelan/nested_validator'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
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 'activemodel'
23
+
24
+ #spec.add_development_dependency 'awesome_print'
25
+ #spec.add_development_dependency 'pry'
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.6'
28
+ spec.add_development_dependency 'rake'
29
+ spec.add_development_dependency 'rspec', '~> 3.0'
30
+ spec.add_development_dependency 'rspec-its'
31
+ spec.add_development_dependency 'coveralls'
32
+ end
@@ -0,0 +1,212 @@
1
+ require 'spec_helper'
2
+ require 'nested_validator'
3
+
4
+ describe NestedValidator do
5
+ let(:base_class) do
6
+ Class.new {
7
+ include ActiveModel::Validations
8
+
9
+ # To keep ActiveModel happy
10
+ def self.model_name
11
+ ActiveModel::Name.new(self, nil, 'temp')
12
+ end
13
+ }
14
+ end
15
+
16
+ let(:parent_class) do
17
+ Class.new(base_class) {
18
+ attr_accessor :child1, :child2
19
+ }
20
+ end
21
+
22
+ let(:child_class) do
23
+ Class.new(base_class) {
24
+ attr_accessor :attribute1
25
+ validates :attribute1, presence: true
26
+
27
+ attr_accessor :attribute2
28
+ validates :attribute2, presence: true
29
+
30
+ attr_accessor :attribute3
31
+ validates :attribute3, presence: true
32
+
33
+ def initialize
34
+ @attribute1 = 'valid'
35
+ @attribute2 = 'valid'
36
+ @attribute3 = 'valid'
37
+ end
38
+ }
39
+ end
40
+
41
+ let(:child1) { child_class.new }
42
+ let(:child2) { child_class.new }
43
+
44
+ def parent_with(&block)
45
+ parent = Class.new(parent_class) { instance_exec &block }.new
46
+ parent.child1 = child1
47
+ parent.child2 = child2
48
+ parent
49
+ end
50
+
51
+ def with_nested_options(options)
52
+ parent_with { validates :child1, nested: options }
53
+ end
54
+
55
+ shared_examples 'valid:' do |child_name, *attributes|
56
+ attributes.each do |attribute|
57
+ specify "when #{child_name}.#{attribute} set to nil" do
58
+ send(child_name).send("#{attribute}=", nil)
59
+ expect(subject).to be_valid
60
+ end
61
+ end
62
+ end
63
+
64
+ shared_examples 'invalid:' do |child_name, *attributes|
65
+ attributes.each do |attribute|
66
+ specify "when #{child_name}.#{attribute} set to nil" do
67
+ send(child_name).send("#{attribute}=", nil)
68
+ expect(subject).to be_invalid
69
+ end
70
+ end
71
+ end
72
+
73
+ describe 'with "nested: true"' do
74
+ subject { with_nested_options true }
75
+
76
+ it_should_validate_nested 'invalid:', :child1, :attribute1, :attribute2, :attribute3
77
+ end
78
+
79
+ describe 'error messages' do
80
+ context 'with scalar values' do
81
+ before { child1.attribute1 = nil;subject.valid? }
82
+
83
+ describe 'with no prefix' do
84
+ subject { with_nested_options true }
85
+
86
+ its('errors.messages') { should eq :'child1 attribute1' => ["can't be blank"] }
87
+ end
88
+
89
+ describe 'with "prefix: "OMG""' do
90
+ subject { with_nested_options prefix: 'OMG' }
91
+
92
+ its('errors.messages') { should eq :'OMG attribute1' => ["can't be blank"] }
93
+ end
94
+ end
95
+
96
+ context 'with an array of values' do
97
+ let(:child1) { [child_class.new, child_class.new] }
98
+
99
+ before { child1[0].attribute1 = nil;subject.valid? }
100
+ subject { with_nested_options true }
101
+
102
+ describe 'with single invalid value' do
103
+ its('errors.messages') { should eq :'child1[0] attribute1' => ["can't be blank"] }
104
+ end
105
+
106
+ describe 'with multiple invalid values' do
107
+ before { child1[1].attribute1 = nil;subject.valid? }
108
+ its('errors.messages') { should include :'child1[0] attribute1' => ["can't be blank"] }
109
+ its('errors.messages') { should include :'child1[1] attribute1' => ["can't be blank"] }
110
+ end
111
+
112
+ context 'with a prefix' do
113
+ subject { with_nested_options prefix: 'OMG' }
114
+
115
+ its('errors.messages') { should eq :'OMG[0] attribute1' => ["can't be blank"] }
116
+ end
117
+ end
118
+ end
119
+
120
+ context 'with a hash of values' do
121
+ let(:child1) { { first: child_class.new, second: child_class.new } }
122
+
123
+ before { child1[:first].attribute1 = nil;subject.valid? }
124
+ subject { with_nested_options true }
125
+
126
+ describe 'with single invalid value' do
127
+ its('errors.messages') { should eq :'child1[first] attribute1' => ["can't be blank"] }
128
+ end
129
+
130
+ describe 'with multiple invalid values' do
131
+ before { child1[:second].attribute1 = nil;subject.valid? }
132
+ its('errors.messages') { should include :'child1[first] attribute1' => ["can't be blank"] }
133
+ its('errors.messages') { should include :'child1[second] attribute1' => ["can't be blank"] }
134
+ end
135
+
136
+ context 'with a prefix' do
137
+ subject { with_nested_options prefix: 'OMG' }
138
+
139
+ its('errors.messages') { should eq :'OMG[first] attribute1' => ["can't be blank"] }
140
+ end
141
+ end
142
+
143
+ describe '"validates :child1, nested: {only: ...}"' do
144
+
145
+ describe 'with "only: :attribute1"' do
146
+
147
+ subject { with_nested_options only: :attribute1 }
148
+
149
+ it_should_validate_nested 'invalid:', :child1, :attribute1
150
+ it_should_validate_nested 'valid:', :child1, :attribute2, :attribute3
151
+ end
152
+
153
+ describe 'with "only: [:atnested_validator_spec.rbtribute1, :attribute2]"' do
154
+
155
+ subject { parent_with { validates :child1, nested: { only: [:attribute1, :attribute2] } } }
156
+
157
+ it_should_validate_nested 'invalid:', :child1, :attribute1, :attribute2
158
+ it_should_validate_nested 'valid:', :child1, :attribute3
159
+ end
160
+ end
161
+
162
+ describe '"validates :child1, nested: {except: ...}"' do
163
+ describe 'with "except: :attribute1"' do
164
+ subject { with_nested_options except: :attribute1 }
165
+
166
+ it_should_validate_nested 'invalid:', :child1, :attribute2, :attribute3
167
+ it_should_validate_nested 'valid:', :child1, :attribute1
168
+ end
169
+
170
+ describe 'with "except: [:attribute1, :attribute2"' do
171
+ subject { with_nested_options except: [:attribute1, :attribute2] }
172
+
173
+ it_should_validate_nested 'invalid:', :child1, :attribute3
174
+ it_should_validate_nested 'valid:', :child1, :attribute1, :attribute2
175
+ end
176
+ end
177
+
178
+ describe 'attributes in "only" option should take precedence over "except"' do
179
+ describe '"validates :child1, nested: {only: attribute1, except: :attribute1}"' do
180
+ subject { with_nested_options only: :attribute1, except: :attribute1 }
181
+
182
+ it_should_validate_nested 'invalid:', :child1, :attribute1
183
+ it_should_validate_nested 'valid:', :child1, :attribute2, :attribute3
184
+ end
185
+ end
186
+
187
+ describe 'validates_nested' do
188
+
189
+ describe 'validates_nested :child1' do
190
+ subject { parent_with { validates_nested :child1 } }
191
+
192
+ it_should_validate_nested 'invalid:', :child1, :attribute1, :attribute2, :attribute3
193
+ it_should_validate_nested 'valid:', :child2, :attribute1, :attribute2, :attribute3
194
+ end
195
+
196
+ describe 'validates_nested :child1, :child2' do
197
+ subject { parent_with { validates_nested :child1, :child2 } }
198
+
199
+ it_should_validate_nested 'invalid:', :child1, :attribute1, :attribute2, :attribute3
200
+ it_should_validate_nested 'invalid:', :child2, :attribute1, :attribute2, :attribute3
201
+ end
202
+
203
+ describe 'validates_nested :child1, :child2, only: :attribute1' do
204
+ subject { parent_with { validates_nested :child1, :child2, only: :attribute1 } }
205
+
206
+ it_should_validate_nested 'invalid:', :child1, :attribute1
207
+ it_should_validate_nested 'invalid:', :child2, :attribute1
208
+ it_should_validate_nested 'valid:', :child1, :attribute2, :attribute3
209
+ it_should_validate_nested 'valid:', :child2, :attribute2, :attribute3
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+ require 'nested_validator'
3
+
4
+
5
+ describe 'Typical usage' do
6
+ class Child
7
+ include ActiveModel::Validations
8
+
9
+ attr_accessor :attribute1, :attribute2
10
+ validates :attribute1, presence: true
11
+ validates :attribute2, presence: true
12
+ end
13
+
14
+ class ParentBase
15
+ include ActiveModel::Validations
16
+
17
+ attr_accessor :child
18
+
19
+ def initialize
20
+ self.child = Child.new
21
+ end
22
+ end
23
+
24
+ specify 'simple example' do
25
+ class Parent < ParentBase
26
+ validates :child, nested: true
27
+ end
28
+
29
+ parent = Parent.new
30
+ parent.valid?
31
+ puts parent.errors.messages
32
+ end
33
+
34
+ specify 'only certain child attributes' do
35
+ class ParentOnly < ParentBase
36
+ validates :child, nested: { only: :attribute1 }
37
+ end
38
+
39
+ parent = ParentOnly.new
40
+ parent.valid?
41
+ puts parent.errors.messages
42
+ end
43
+
44
+ specify 'except certain child attributes' do
45
+ class ParentExcept < ParentBase
46
+ validates :child, nested: { except: :attribute2 }
47
+ end
48
+
49
+ parent = ParentExcept.new
50
+ parent.valid?
51
+ puts parent.errors.messages
52
+ end
53
+
54
+ specify 'custom message prefix' do
55
+ class ParentPrefix < ParentBase
56
+ validates :child, nested: { only: :attribute1, prefix: 'OMG'}
57
+ end
58
+
59
+ parent = ParentPrefix.new
60
+ parent.valid?
61
+ puts parent.errors.messages
62
+ end
63
+
64
+ specify 'array of values' do
65
+ class ParentArray < ParentBase
66
+ validates :child, nested: { only: :attribute1 }
67
+ end
68
+
69
+ parent = ParentArray.new
70
+ parent.child = [Child.new] * 2
71
+ parent.valid?
72
+ puts parent.errors.messages
73
+ end
74
+
75
+ specify 'hash of values' do
76
+ class ParentHash < ParentBase
77
+ validates :child, nested: { only: :attribute1 }
78
+ end
79
+
80
+ parent = ParentHash.new
81
+ parent.child = { thing1: Child.new, thing2: Child.new }
82
+ parent.valid?
83
+ puts parent.errors.messages
84
+ end
85
+
86
+ specify 'multiple children' do
87
+ class ParentMultiple < ParentBase
88
+ attr_accessor :child2
89
+
90
+ validates_nested :child, :child2, only: :attribute1
91
+
92
+ def initialize
93
+ self.child = Child.new
94
+ self.child2 = Child.new
95
+ end
96
+ end
97
+
98
+ parent = ParentMultiple.new
99
+ parent.valid?
100
+ puts parent.errors.messages
101
+ end
102
+ end
@@ -0,0 +1,176 @@
1
+ require 'spec_helper'
2
+ require 'nested_validator'
3
+
4
+ describe 'validates_nested with [parent class with "validates, child1]"' do
5
+ let(:parent_class) do
6
+ Class.new {
7
+ include ActiveModel::Validations
8
+
9
+ attr_accessor :child1, :child2
10
+
11
+ def initialize(child1=nil, child2=nil)
12
+ self.child1 = child1
13
+ self.child2 = child2
14
+ end
15
+
16
+ def to_s
17
+ 'parent'
18
+ end
19
+ }
20
+
21
+ end
22
+
23
+ let(:child_class) do
24
+ Class.new {
25
+ include ActiveModel::Validations
26
+
27
+ attr_accessor :attribute1, :attribute2, :attribute3
28
+
29
+ validates :attribute1, presence: true
30
+ validates :attribute2, presence: true
31
+ validates :attribute3, presence: true
32
+
33
+ def initialize
34
+ @attribute1 = 'valid'
35
+ @attribute2 = 'valid'
36
+ @attribute3 = 'valid'
37
+ end
38
+ }
39
+ end
40
+
41
+ let(:parent) { parent_class.new child_class.new, child_class.new }
42
+ let(:options) { { presence: true } }
43
+
44
+ before { parent_class.class_eval "validates :child1, #{options}" }
45
+
46
+ describe 'its validations with options:' do
47
+ let(:options) { self.class.description }
48
+
49
+ subject { parent }
50
+
51
+ context 'nested: true' do
52
+ it { should validate_nested(:child1) }
53
+ it { should validate_nested('child1') }
54
+
55
+ it { should_not validate_nested(:child2) }
56
+ it { should_not validate_nested(:invalid_child_name) }
57
+ end
58
+
59
+ context 'nested: {prefix: "OMG"}' do
60
+
61
+ it { should validate_nested(:child1).with_prefix('OMG') }
62
+ it { should validate_nested(:child1).with_prefix(:OMG) }
63
+
64
+ it { should_not validate_nested(:child1) }
65
+ it { should_not validate_nested(:child1).with_prefix('WTF') }
66
+ it { should_not validate_nested(:child1).with_prefix(:WTF) }
67
+ end
68
+
69
+ context 'nested: {only: :attribute1}' do
70
+ it { should validate_nested(:child1).only(:attribute1) }
71
+ it { should validate_nested(:child1).only('attribute1') }
72
+
73
+ it { should_not validate_nested(:child1).only(:attribute2) }
74
+ it { should_not validate_nested(:child1).only('attribute2') }
75
+ it { should_not validate_nested(:child1).only(:invalid_attribute_name) }
76
+ it { should_not validate_nested(:child1).only(:attribute1, :attribute2) }
77
+ end
78
+
79
+ context 'nested: {only: [:attribute1, :attribute2]}' do
80
+ it { should validate_nested(:child1).only(:attribute1) }
81
+ it { should validate_nested(:child1).only(:attribute2) }
82
+ it { should validate_nested(:child1).only(:attribute1, :attribute2) }
83
+ it { should validate_nested(:child1).only('attribute1', 'attribute2') }
84
+
85
+ it { should_not validate_nested(:child1).only(:attribute2, :attribute3) }
86
+ it { should_not validate_nested(:child1).only(:invalid_attribute_name) }
87
+ end
88
+
89
+ context 'nested: {except: :attribute1}' do
90
+ it { should validate_nested(:child1).except(:attribute1) }
91
+ it { should validate_nested(:child1).except('attribute1') }
92
+
93
+ it { should_not validate_nested(:child1).except(:attribute2) }
94
+ end
95
+
96
+ context 'nested: {except: [:attribute1, :attribute2]}' do
97
+ it { should validate_nested(:child1).except(:attribute1, :attribute2) }
98
+ end
99
+ end
100
+
101
+ describe 'its description for:' do
102
+ let(:validator) { instance_eval self.class.description }
103
+
104
+ subject { validator.description }
105
+
106
+ context('validate_nested(:child1)') { it { should eq 'validate nested :child1' } }
107
+ context('validate_nested(:child1).only(:attribute1)') { it { should eq 'validate nested :child1 with only: :attribute1' } }
108
+ context('validate_nested(:child1).except(:attribute1)') { it { should eq 'validate nested :child1 except: :attribute1' } }
109
+ context('validate_nested(:child1).with_prefix(:OMG)') { it { should eq 'validate nested :child1 with prefix :OMG' } }
110
+ end
111
+
112
+ describe 'its error messages:' do
113
+ let(:options) { self.class.parent.description }
114
+ let(:validator) { instance_eval self.class.description }
115
+
116
+ describe 'should failure messages for' do
117
+ before { expect(validator.matches? parent).to be false }
118
+
119
+ subject { validator.failure_message }
120
+
121
+ context 'nested: true' do
122
+ describe('validate_nested(:child2)') { it { should eq "parent doesn't nest validations for :child2" } }
123
+ describe('validate_nested(:invalid_child_name)') { it { should eq "parent doesn't respond to :invalid_child_name" } }
124
+ end
125
+
126
+ context 'nested: {prefix: :OMG}' do
127
+ describe('validate_nested(:child1)') { it { should eq "parent has a prefix of :OMG. Are you missing '.with_prefix(:OMG)'?" } }
128
+ describe('validate_nested(:child1).with_prefix(:WTF)') { it { should eq 'parent uses a prefix of :OMG rather than :WTF' } }
129
+ end
130
+
131
+ context 'nested: {only: :attribute1}' do
132
+ describe('validate_nested(:child1).only(:invalid_attribute_name)') { it { should eq "child1 doesn't respond to :invalid_attribute_name" } }
133
+ describe('validate_nested(:child1).only(:attribute2)') { it { should eq "parent doesn't nest validations for: :attribute2" } }
134
+ describe('validate_nested(:child1).only(:attribute1, :attribute2)') { it { should eq "parent doesn't nest validations for: :attribute2" } }
135
+ end
136
+
137
+ context 'nested: {except: :attribute1}' do
138
+ describe('validate_nested(:child1).except(:invalid_attribute_name)') { it { should eq "child1 doesn't respond to :invalid_attribute_name" } }
139
+ describe('validate_nested(:child1).except(:attribute2)') { it { should eq 'parent does nest validations for: :attribute2' } }
140
+ describe('validate_nested(:child1).except(:attribute1, :attribute2)') { it { should eq 'parent does nest validations for: :attribute2' } }
141
+ end
142
+ end
143
+
144
+ describe 'should_not failure messages for' do
145
+ before { expect(validator.matches? parent).to be true }
146
+
147
+ subject { validator.failure_message_when_negated }
148
+
149
+ context 'nested: true' do
150
+ describe('validate_nested(:child1)') { it { should eq 'parent does nest validations for: :child1' } }
151
+ end
152
+
153
+ context 'nested: {prefix: :OMG}' do
154
+ describe('validate_nested(:child1).with_prefix(:OMG)') { it { should eq 'parent does nest validations for: :child1 with a prefix of :OMG' } }
155
+ end
156
+
157
+ context 'nested: {only: :attribute1}' do
158
+ describe('validate_nested(:child1).only(:attribute1)') { it { should eq 'parent does nest :child1 validations for: :attribute1' } }
159
+ end
160
+
161
+ context 'nested: {only: [:attribute1, :attribute2]}' do
162
+ describe('validate_nested(:child1).only(:attribute1)') { it { should eq 'parent does nest :child1 validations for: :attribute1' } }
163
+ describe('validate_nested(:child1).only(:attribute1, :attribute2)') { it { should eq 'parent does nest :child1 validations for: :attribute1, :attribute2' } }
164
+ end
165
+
166
+ context 'nested: {except: :attribute1}' do
167
+ describe('validate_nested(:child1).except(:attribute1)') { it { should eq "parent doesn't nest :child1 validations for: :attribute1" } }
168
+ end
169
+
170
+ context 'nested: {except: [:attribute1, :attribute2]}' do
171
+ describe('validate_nested(:child1).except(:attribute1)') { it { should eq "parent doesn't nest :child1 validations for: :attribute1" } }
172
+ describe('validate_nested(:child1).except(:attribute1, :attribute2)') { it { should eq "parent doesn't nest :child1 validations for: :attribute1, :attribute2" } }
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,16 @@
1
+ require 'i18n'
2
+ require 'rspec/its'
3
+ require 'coveralls'
4
+
5
+ #require 'pry'
6
+ #require 'awesome_print'
7
+
8
+ I18n.enforce_available_locales = true
9
+ Coveralls.wear!
10
+
11
+ RSpec.configure do |config|
12
+ config.filter_run focus: true
13
+ config.run_all_when_everything_filtered = true
14
+
15
+ config.alias_it_should_behave_like_to :it_should_validate_nested, 'the parent object should be'
16
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nested_validator
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Declan Whelan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-its
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
+ - !ruby/object:Gem::Dependency
84
+ name: coveralls
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Nested validations allow a parent's class validity to include those of
98
+ child attributes. Errors messages will be copied from the child attribute to the
99
+ parent.
100
+ email:
101
+ - declanpwhelan@gmail.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - .gitignore
107
+ - .rspec
108
+ - .travis.yml
109
+ - Gemfile
110
+ - Gemfile.lock
111
+ - LICENSE.txt
112
+ - README.md
113
+ - Rakefile
114
+ - lib/nested_validator.rb
115
+ - lib/nested_validator/nested_validator.rb
116
+ - lib/nested_validator/validate_nested_matcher.rb
117
+ - lib/nested_validator/version.rb
118
+ - nested_validator.gemspec
119
+ - spec/lib/nested_validator/nested_validator_spec.rb
120
+ - spec/lib/nested_validator/usage_spec.rb
121
+ - spec/lib/nested_validator/validate_nested_matcher_spec.rb
122
+ - spec/spec_helper.rb
123
+ homepage: https://github.com/dwhelan/nested_validator
124
+ licenses:
125
+ - MIT
126
+ metadata: {}
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ! '>='
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ! '>='
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubyforge_project:
143
+ rubygems_version: 2.1.10
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: A validator that supports nesting.
147
+ test_files:
148
+ - spec/lib/nested_validator/nested_validator_spec.rb
149
+ - spec/lib/nested_validator/usage_spec.rb
150
+ - spec/lib/nested_validator/validate_nested_matcher_spec.rb
151
+ - spec/spec_helper.rb