active_model_type_validator 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +3 -0
- data/Appraisals +3 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.md +22 -0
- data/README.md +74 -0
- data/Rakefile +8 -0
- data/active_model_type_validator.gemspec +37 -0
- data/gemfiles/rails_4.2.gemfile +7 -0
- data/lib/active_model_type_validator.rb +3 -0
- data/lib/active_model_type_validator/contents.rb +30 -0
- data/lib/active_model_type_validator/locale/en.yml +4 -0
- data/lib/active_model_type_validator/object_type.rb +99 -0
- data/lib/active_model_type_validator/version.rb +7 -0
- data/test/active_model_type_validator/active_model_type_validator_test.rb +11 -0
- data/test/active_model_type_validator/contents_test.rb +129 -0
- data/test/active_model_type_validator/object_type_test.rb +201 -0
- data/test/app/rails_4.2/Rakefile +6 -0
- data/test/app/rails_4.2/app/assets/javascripts/application.js +16 -0
- data/test/app/rails_4.2/app/assets/stylesheets/application.css +15 -0
- data/test/app/rails_4.2/app/controllers/application_controller.rb +5 -0
- data/test/app/rails_4.2/app/helpers/application_helper.rb +2 -0
- data/test/app/rails_4.2/app/views/layouts/application.html.erb +14 -0
- data/test/app/rails_4.2/bin/bundle +3 -0
- data/test/app/rails_4.2/bin/rails +8 -0
- data/test/app/rails_4.2/bin/rake +8 -0
- data/test/app/rails_4.2/bin/setup +29 -0
- data/test/app/rails_4.2/bin/spring +15 -0
- data/test/app/rails_4.2/config.ru +4 -0
- data/test/app/rails_4.2/config/application.rb +26 -0
- data/test/app/rails_4.2/config/boot.rb +3 -0
- data/test/app/rails_4.2/config/database.yml +25 -0
- data/test/app/rails_4.2/config/environment.rb +5 -0
- data/test/app/rails_4.2/config/environments/development.rb +41 -0
- data/test/app/rails_4.2/config/environments/production.rb +79 -0
- data/test/app/rails_4.2/config/environments/test.rb +42 -0
- data/test/app/rails_4.2/config/initializers/assets.rb +11 -0
- data/test/app/rails_4.2/config/initializers/backtrace_silencers.rb +7 -0
- data/test/app/rails_4.2/config/initializers/cookies_serializer.rb +3 -0
- data/test/app/rails_4.2/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/app/rails_4.2/config/initializers/inflections.rb +16 -0
- data/test/app/rails_4.2/config/initializers/mime_types.rb +4 -0
- data/test/app/rails_4.2/config/initializers/session_store.rb +3 -0
- data/test/app/rails_4.2/config/initializers/wrap_parameters.rb +14 -0
- data/test/app/rails_4.2/config/locales/en.yml +23 -0
- data/test/app/rails_4.2/config/routes.rb +56 -0
- data/test/app/rails_4.2/config/secrets.yml +22 -0
- data/test/app/rails_4.2/db/schema.rb +0 -0
- data/test/app/rails_4.2/db/seeds.rb +7 -0
- data/test/app/rails_4.2/public/404.html +67 -0
- data/test/app/rails_4.2/public/422.html +67 -0
- data/test/app/rails_4.2/public/500.html +66 -0
- data/test/app/rails_4.2/public/favicon.ico +0 -0
- data/test/app/rails_4.2/public/robots.txt +5 -0
- data/test/test_helper.rb +20 -0
- metadata +260 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 72c8544aafca46e9293835f295a5a8e87013d4c6
|
4
|
+
data.tar.gz: 9f9b086ba8474d5ed70057371c69f538455ea17a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b2769e445e21187b1108d9863d5cbbb2dd78996eb396926c14397072d97ff090811aa0f40422365addac2a41ca732fbd319c01eaecc1dcb0c2815e3192f93713
|
7
|
+
data.tar.gz: ae548970359df5bc19f1ab603935d5e50ff139f19b4d0950fabf76d3733fd819410ce0f7f591bf0d0012923dae3e262ad0501f8a3d2c16199e2094cafada4da8
|
data/.yardopts
ADDED
data/Appraisals
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Todd Knarr
|
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,74 @@
|
|
1
|
+
# ActiveModelTypeValidator
|
2
|
+
|
3
|
+
Provides two ActiveModel validators:
|
4
|
+
|
5
|
+
1. One to validate that an attribute is of the correct type. Multiple types are allowed.
|
6
|
+
2. One to validate the contents of an attribute by calling `#valid?` on it, performing
|
7
|
+
the same job as ActiveRecord's `validates_associated`.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'active_model_type_validator'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install active_model_type_validator
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
The usage is fairly standard. The `object_type` validator takes one option `:type` which
|
28
|
+
is either a type name (string, symbol or type) or an array of type names. The `contents`
|
29
|
+
validator needs no options. Both take the standard options all validators do, except
|
30
|
+
that `object_type` ignores the `:allow_blank` option (you should use `:allow_nil` if you
|
31
|
+
need equivalent behavior).
|
32
|
+
|
33
|
+
Examples:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
class MyClass
|
37
|
+
include ActiveModel::Model
|
38
|
+
include ActiveModel::Validations
|
39
|
+
|
40
|
+
validates :string_field, object_type: { type: String }
|
41
|
+
validates :integer_field, object_type: { type: Integer }
|
42
|
+
validates :numeric_field, object_type: { type: [Integer, Float] }
|
43
|
+
validates :object_field, contents: true, object_type: { type: ChildClass }
|
44
|
+
|
45
|
+
attr_accessor :string_field
|
46
|
+
attr_accessor :integer_field
|
47
|
+
attr_accessor :numeric_field
|
48
|
+
attr_accessor :object_field
|
49
|
+
|
50
|
+
def initialize
|
51
|
+
@string_field = 'my string'
|
52
|
+
@integer_field = 42
|
53
|
+
@numeric_field = 7
|
54
|
+
@object_field = ChildClass.new
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
The `contents` validator adds what's equivalent to ActiveModel's `associated` validator,
|
61
|
+
passing if calling `#valid?` on the attribute returns true.
|
62
|
+
|
63
|
+
The `object_type` validator lets you test ActiveModel objects used in an API to make sure
|
64
|
+
they contain objects of the expected types rather than something you aren't expecting. It
|
65
|
+
fills in a gap I was annoyed by when I validate incoming API objects immediately upon
|
66
|
+
receiving them and go no further if they fail validation.
|
67
|
+
|
68
|
+
## Contributing
|
69
|
+
|
70
|
+
1. Fork it ( https://github.com/tknarr/active_model_type_validator/fork )
|
71
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
72
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
73
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
74
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'active_model_type_validator/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "active_model_type_validator"
|
9
|
+
spec.version = ActiveModelTypeValidator::VERSION
|
10
|
+
spec.date = Time.now.strftime('%Y-%m-%d')
|
11
|
+
spec.author = 'Todd Knarr'
|
12
|
+
spec.email = 'tknarr@silverglass.org'
|
13
|
+
spec.summary = %q{ActiveModel validators to validate object types and contained/associated objects.}
|
14
|
+
spec.description = %q{ActiveModel validators that validate the type of attributes, and that do recursive validation of contained objects parallel to ActiveRecord's validates_associated.}
|
15
|
+
spec.homepage = 'https://github.com/tknarr/active_model_type_validator'
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
spec.files = Dir.glob('lib/**/*').select { |path| File.file?(path) } +
|
19
|
+
[ __FILE__ ]
|
20
|
+
spec.test_files = Dir.glob('test/**/*').select { |path| File.file?(path) && !File.fnmatch('*.{log,sqlite3}', path, File::FNM_EXTGLOB) } +
|
21
|
+
Dir.glob('gemfiles/*.gemfile') + [ 'Rakefile', 'Appraisals' ]
|
22
|
+
spec.extra_rdoc_files = [ 'README.md', 'CHANGELOG.md', 'LICENSE.md', '.yardopts' ]
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_dependency 'rails', '~> 4.2'
|
26
|
+
spec.add_dependency 'activemodel', '~> 4.2'
|
27
|
+
|
28
|
+
# Development
|
29
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
30
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
31
|
+
spec.add_development_dependency 'yard', '~> 0'
|
32
|
+
|
33
|
+
# Testing
|
34
|
+
spec.add_development_dependency 'minitest', '~> 5'
|
35
|
+
spec.add_development_dependency 'appraisal', '~> 2'
|
36
|
+
spec.add_development_dependency 'sqlite3', '~> 1'
|
37
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ActiveModel
|
2
|
+
|
3
|
+
module Validations
|
4
|
+
|
5
|
+
# Implements the mechanics of <tt>validates_contents_of</tt>.
|
6
|
+
class ContentsValidator < ActiveModel::EachValidator
|
7
|
+
|
8
|
+
# Standard validation method for an <tt>EachValidator</tt>.
|
9
|
+
def validate_each(object, attribute, value)
|
10
|
+
if Array.wrap(value).reject { |r| r.valid? }.any?
|
11
|
+
object.errors.add(attribute, :invalid, options.merge(:value => value))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
module HelperMethods
|
18
|
+
|
19
|
+
# An implementation of the associated validator from ActiveRecord.
|
20
|
+
# All options and behaviors are identical. It simply calls #valid? on each
|
21
|
+
# of the attributes listed and returns the logical AND of them all.
|
22
|
+
def validates_contents_of(*attr_names)
|
23
|
+
validates_with ContentsValidator, _merge_attributes(attr_names)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module ActiveModel
|
2
|
+
|
3
|
+
module Validations
|
4
|
+
|
5
|
+
# Implements the mechanics of <tt>validates_object_type_of</tt>.
|
6
|
+
#
|
7
|
+
# The standard <tt>#initialize</tt> and <tt>#validate_each</tt> methods are implemented.
|
8
|
+
class ObjectTypeValidator < EachValidator
|
9
|
+
|
10
|
+
# Standard initializer for an <tt>EachValidator</tt>.
|
11
|
+
def initialize(options)
|
12
|
+
@types = []
|
13
|
+
# Remove :allow_blank since it's nonsensical when testing types
|
14
|
+
options.delete(:allow_blank)
|
15
|
+
# Set :allow_nil true if it's not present
|
16
|
+
t = options[:allow_nil]
|
17
|
+
options[:allow_nil] = true if t.nil?
|
18
|
+
# Filter out missing or blank type entries and convert others
|
19
|
+
# to class via constantize.
|
20
|
+
t = Array(options.delete(:type))
|
21
|
+
t.each do |te|
|
22
|
+
if te.is_a?(Class)
|
23
|
+
@types.append(te)
|
24
|
+
elsif te.is_a?(String)
|
25
|
+
begin
|
26
|
+
@types.append(te.constantize) unless te.blank?
|
27
|
+
rescue NameError
|
28
|
+
raise ArgumentError, "Type #{te} is invalid"
|
29
|
+
end
|
30
|
+
elsif te.is_a?(Symbol)
|
31
|
+
begin
|
32
|
+
@types.append(te.to_s.constantize)
|
33
|
+
rescue NameError
|
34
|
+
raise ArgumentError, "Type #{te.to_s} is invalid"
|
35
|
+
end
|
36
|
+
else
|
37
|
+
raise ArgumentError, "Type #{te.to_s} is a #{te.class.name} which is invalid"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
raise ArgumentError, 'At least one type must be given' if @types.empty?
|
41
|
+
super(options)
|
42
|
+
check_validity!
|
43
|
+
end
|
44
|
+
|
45
|
+
# Standard validation method for an <tt>EachValidator</tt>.
|
46
|
+
def validate_each(record, attr_name, value)
|
47
|
+
errors_options = options.except(:type)
|
48
|
+
errors_options[:type] = value.class
|
49
|
+
right_type = false
|
50
|
+
@types.each do |t|
|
51
|
+
# Entries in @types are classes, not strings
|
52
|
+
right_type = true if value.is_a?(t)
|
53
|
+
end
|
54
|
+
record.errors.add(attr_name, (options[:message] || :wrong_type), errors_options) unless right_type
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
module HelperMethods
|
60
|
+
|
61
|
+
# Validates that the specified attributes are of a given type (as defined by <tt>#is_a?</tt>).
|
62
|
+
#
|
63
|
+
# class Person < ActiveRecord::Base
|
64
|
+
# validates_type_of :first_name, type: String
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# The +first_name+ attribute must be in the object and it must be a X.
|
68
|
+
#
|
69
|
+
# If you want to validate the type of a boolean field (where the real
|
70
|
+
# types are +TrueClass+ and +FalseClass+), you will want to use
|
71
|
+
# both those classes in the list of types and set +:allow_nil+ to false.
|
72
|
+
#
|
73
|
+
# By default this validator sets +:allow_nil+ to true to allow nil values to
|
74
|
+
# pass through validation even though +NilClass+ isn't explicitly listed, and
|
75
|
+
# you're expected to use a presence validator if you want the attribute to have
|
76
|
+
# to be present (non-nil). You can set +:allow_nil+ to false to cause even nil
|
77
|
+
# values to be checked against the type list and fail if NilClass isn't listed,
|
78
|
+
# mimicking the effect of a presence validator.
|
79
|
+
#
|
80
|
+
# Configuration options:
|
81
|
+
# * <tt>:type</tt> - An array of class names (or symbols or strings naming class
|
82
|
+
# names) which are legal for values of the attribute.
|
83
|
+
# * <tt>:message</tt> - A custom error message (default is: "cannot be of type X").
|
84
|
+
# Messages can find the offending type name for interpolation in +:type+.
|
85
|
+
#
|
86
|
+
# There is also a list of default options supported by every validator:
|
87
|
+
# +:if+, +:unless+, +:on+, +:allow_nil+, and +:strict+. +:allow_blank+ is
|
88
|
+
# not allowed because it's nonsensical and confusing when applied to type
|
89
|
+
# checking.
|
90
|
+
# See <tt>ActiveModel::Validation#validates</tt> for more information
|
91
|
+
def validates_object_type_of(*attr_names)
|
92
|
+
validates_with ObjectTypeValidator, _merge_attributes(attr_names)
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require '../test_helper'
|
2
|
+
require 'active_model_type_validator'
|
3
|
+
|
4
|
+
class ActiveModelTypeValidatorTest < ActiveSupport::TestCase
|
5
|
+
|
6
|
+
test 'API' do
|
7
|
+
assert_not_nil ActiveModelTypeValidator::VERSION
|
8
|
+
assert_kind_of String, ActiveModelTypeValidator::VERSION
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require '../test_helper'
|
2
|
+
require 'active_model_type_validator'
|
3
|
+
|
4
|
+
class MyClass1
|
5
|
+
include ActiveModel::Model
|
6
|
+
include ActiveModel::Validations
|
7
|
+
validates :a1, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
8
|
+
validates :b1, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
9
|
+
validates :c1, presence: true, contents: true
|
10
|
+
|
11
|
+
attr_accessor :a1
|
12
|
+
attr_accessor :b1
|
13
|
+
attr_accessor :c1
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
class MyClass1a
|
18
|
+
include ActiveModel::Model
|
19
|
+
include ActiveModel::Validations
|
20
|
+
validates :a1, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
21
|
+
validates :b1, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
22
|
+
validates :c1, presence: true
|
23
|
+
|
24
|
+
attr_accessor :a1
|
25
|
+
attr_accessor :b1
|
26
|
+
attr_accessor :c1
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
class MyClass1b
|
31
|
+
include ActiveModel::Model
|
32
|
+
include ActiveModel::Validations
|
33
|
+
validates :a1, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
34
|
+
validates :b1, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
35
|
+
validates :c1, contents: { allow_nil: true }
|
36
|
+
validates :c2, contents: true
|
37
|
+
|
38
|
+
attr_accessor :a1
|
39
|
+
attr_accessor :b1
|
40
|
+
attr_accessor :c1
|
41
|
+
attr_accessor :c2
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
class MyClass2
|
46
|
+
include ActiveModel::Model
|
47
|
+
include ActiveModel::Validations
|
48
|
+
validates :a2, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
49
|
+
validates :b2, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
50
|
+
|
51
|
+
attr_accessor :a2
|
52
|
+
attr_accessor :b2
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
class RecursiveTest < ActiveSupport::TestCase
|
57
|
+
|
58
|
+
def setup
|
59
|
+
@model = MyClass1.new
|
60
|
+
@model.a1 = 57
|
61
|
+
@model.b1 = nil
|
62
|
+
@model.c1 = MyClass2.new
|
63
|
+
@model.c1.a2 = 99
|
64
|
+
@model.c1.b2 = nil
|
65
|
+
|
66
|
+
@not_validated = MyClass1a.new
|
67
|
+
@not_validated.a1 = 57
|
68
|
+
@not_validated.b1 = nil
|
69
|
+
@not_validated.c1 = MyClass2.new
|
70
|
+
@not_validated.c1.a2 = 99
|
71
|
+
@not_validated.c1.b2 = nil
|
72
|
+
|
73
|
+
@no_child = MyClass1b.new
|
74
|
+
@no_child.a1 = 57
|
75
|
+
@no_child.b1 = nil
|
76
|
+
@no_child.c1 = MyClass2.new
|
77
|
+
@no_child.c1.a2 = 99
|
78
|
+
@no_child.c1.b2 = nil
|
79
|
+
@no_child.c2 = MyClass2.new
|
80
|
+
@no_child.c2.a2 = 99
|
81
|
+
@no_child.c2.b2 = nil
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
test 'valid parent valid child' do
|
86
|
+
@model.b1 = 17
|
87
|
+
@model.c1.b2 = 23
|
88
|
+
assert @model.valid?
|
89
|
+
end
|
90
|
+
|
91
|
+
test 'valid parent invalid child' do
|
92
|
+
@model.b1 = 17
|
93
|
+
@model.c1.b2 = -23
|
94
|
+
assert_not @model.valid?
|
95
|
+
end
|
96
|
+
|
97
|
+
test 'invalid parent valid child' do
|
98
|
+
@model.b1 = -17
|
99
|
+
@model.c1.b2 = 23
|
100
|
+
assert_not @model.valid?
|
101
|
+
end
|
102
|
+
|
103
|
+
test 'invalid parent invalid child' do
|
104
|
+
@model.b1 = -17
|
105
|
+
@model.c1.b2 = -23
|
106
|
+
assert_not @model.valid?
|
107
|
+
end
|
108
|
+
|
109
|
+
test 'valid parent invalid child not validated' do
|
110
|
+
@not_validated.b1 = 17
|
111
|
+
@not_validated.c1.b2 = -23
|
112
|
+
assert @not_validated.valid?
|
113
|
+
end
|
114
|
+
|
115
|
+
test 'valid parent without child allow nil' do
|
116
|
+
@no_child.b1 = 17
|
117
|
+
@no_child.c1 = nil
|
118
|
+
@no_child.c2.b2 = 23
|
119
|
+
assert @no_child.valid?
|
120
|
+
end
|
121
|
+
|
122
|
+
test 'valid parent without child no allow nil' do
|
123
|
+
@no_child.b1 = 17
|
124
|
+
@no_child.c1.b2 = 23
|
125
|
+
@no_child.c2 = nil
|
126
|
+
assert_not @model.valid?
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|