nested_validator 1.0.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.
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