activerecord-databasevalidations 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +9 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +77 -0
- data/Rakefile +9 -0
- data/activerecord-databasevalidations.gemspec +27 -0
- data/lib/active_model/validations/basic_multilingual_plane.rb +23 -0
- data/lib/active_model/validations/bytesize.rb +40 -0
- data/lib/active_model/validations/not_null.rb +20 -0
- data/lib/active_record/database_validations.rb +7 -0
- data/lib/active_record/database_validations/locale/en.yml +8 -0
- data/lib/active_record/database_validations/version.rb +5 -0
- data/lib/active_record/validations/database_constraints.rb +108 -0
- data/lib/activerecord-databasevalidations.rb +1 -0
- data/lib/activerecord/databasevalidations.rb +1 -0
- data/test/basic_multilingual_plane_validator_test.rb +33 -0
- data/test/bytesize_validator_test.rb +53 -0
- data/test/data_loss_test.rb +66 -0
- data/test/database.yml +6 -0
- data/test/database_constraints_validator_test.rb +109 -0
- data/test/not_null_validator_test.rb +31 -0
- data/test/test_helper.rb +14 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dc6901e44580bc7760d63ab78aeb0575678dfba8
|
4
|
+
data.tar.gz: 6b056fa79931ef1db0080e718d34d071a212b164
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bfc973cd0579d7138c18e3403b214b9a546e61722937160e3342edcbd8a1610a513d9cec77a19b4ff77e0dc9a0a1c493b00ce9b7b7302cb6905f304bc02aab41
|
7
|
+
data.tar.gz: fe39aa4f6917b4a1de65edd48e566a348f1f7ac020a5c05eb2cd882204492f516f8a673ee43e16bf4bfca0b93d1c9a92e3e4a36025783a4c44a4f1cf2f7f99e8
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Willem van Bergen
|
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,77 @@
|
|
1
|
+
# ActiveRecord::DatabaseValidations
|
2
|
+
|
3
|
+
Add validations to your ActiveRecord models based on your database constraints.
|
4
|
+
|
5
|
+
This gem is primarily intended for MySQL databases not running in strict mode,
|
6
|
+
which can easily cause data loss. These problems are documented in
|
7
|
+
[DataLossTest](https://github.com/wvanbergen/activerecord-databasevalidations/blob/master/test/data_loss_test.rb)
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem 'activerecord-databasevalidations'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install activerecord-databasevalidations
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
You can use ActiveModel's `validates` method to define what fields you want
|
26
|
+
to validate based on the database constraints.
|
27
|
+
|
28
|
+
``` ruby
|
29
|
+
class Foo < ActiveRecord::Base
|
30
|
+
validates :boolean_field, database_constraints: :not_null
|
31
|
+
validates :string_field, database_constraints: [:size, :basic_multilingual_plane]
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
You can also use `validates_database_constraints_of`:
|
36
|
+
|
37
|
+
``` ruby
|
38
|
+
class Bar < ActiveRecord::Base
|
39
|
+
validates_database_constraints_of :my_field, with: :size
|
40
|
+
end
|
41
|
+
|
42
|
+
### Available validations
|
43
|
+
|
44
|
+
You have to specify what conatrints you want to validate for. Valid values are:
|
45
|
+
|
46
|
+
- `:size` to validate for the size of textual and binary columns. It will pick character
|
47
|
+
size or bytesize based on the column's type.
|
48
|
+
- `:not_null` to validate a NOT NULL contraint.
|
49
|
+
- `:basic_multilingual_plane` to validate that all characters for text fields are inside
|
50
|
+
the basic multilingual plane of unicode (unless you use the utf8mb4 character set).
|
51
|
+
|
52
|
+
The validations will only be created if it makes sense for the column, e.g. a `:not_null`
|
53
|
+
validation will only be added if the column has a NOT NULL constraint defined on it.
|
54
|
+
|
55
|
+
### Hand-rolling validations
|
56
|
+
|
57
|
+
You can also instantiate the validators yourself:
|
58
|
+
|
59
|
+
``` ruby
|
60
|
+
class Bar < ActiveRecord::Base
|
61
|
+
validates :string_field, bytesize: { maximum: 255}, basic_multilingual_plane: true
|
62
|
+
validates :string_field, not_null: true
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
Note that this will create validations without inspecting the column to see if it
|
67
|
+
actually makes sense.
|
68
|
+
|
69
|
+
```
|
70
|
+
|
71
|
+
## Contributing
|
72
|
+
|
73
|
+
1. Fork it (http://github.com/wvanbergen/activerecord-databasevalidations/fork)
|
74
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
75
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
76
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
77
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'active_record/database_validations/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "activerecord-databasevalidations"
|
8
|
+
spec.version = ActiveRecord::DatabaseValidations::VERSION
|
9
|
+
spec.authors = ["Willem van Bergen"]
|
10
|
+
spec.email = ["willem@railsdoctors.com"]
|
11
|
+
spec.summary = %q{Add validations to your ActiveRecord models based on MySQL database constraints.}
|
12
|
+
spec.description = %q{Opt-in validations for your ActiveRecord models based on your MySQL database constraints, including text field size, UTF-8 encoding issues, and NOT NULL constraints.}
|
13
|
+
spec.homepage = "https://github.com/wvanbergen/activerecord-database_validations"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency "activerecord", "~> 4"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "minitest", "~> 5"
|
26
|
+
spec.add_development_dependency "mysql2"
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActiveModel
|
2
|
+
module Validations
|
3
|
+
class BasicMultilingualPlaneValidator < ActiveModel::EachValidator
|
4
|
+
OUTSIDE_BMP = /[^\u{0}-\u{FFFF}]/
|
5
|
+
|
6
|
+
def validate_each(record, attribute, value)
|
7
|
+
return if value.nil?
|
8
|
+
if value.to_s.index(OUTSIDE_BMP)
|
9
|
+
errors_options = options.except(:characters_outside_basic_multilingual_plane)
|
10
|
+
default_message = options[:characters_outside_basic_multilingual_plane]
|
11
|
+
errors_options[:message] ||= default_message if default_message
|
12
|
+
record.errors.add(attribute, :characters_outside_basic_multilingual_plane, errors_options)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module HelperMethods
|
18
|
+
def validates_basic_multilingual_plane_of(*attr_names)
|
19
|
+
validates_with ActiveModel::Validations::BasicMultilingualPlaneValidator, _merge_attributes(attr_names)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ActiveModel
|
2
|
+
module Validations
|
3
|
+
class BytesizeValidator < ActiveModel::EachValidator
|
4
|
+
attr_reader :encoding
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
super
|
8
|
+
@encoding = Encoding.find(options[:encoding]) if options[:encoding]
|
9
|
+
end
|
10
|
+
|
11
|
+
def check_validity!
|
12
|
+
unless options[:maximum].is_a?(Integer) && options[:maximum] >= 0
|
13
|
+
raise ArgumentError, ":maximum must be set to a nonnegative Integer"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate_each(record, attribute, value)
|
18
|
+
string = value.to_s
|
19
|
+
string = string.encode(options[:encoding]) if requires_transcoding?(string)
|
20
|
+
if string.bytesize > options[:maximum]
|
21
|
+
errors_options = options.except(:too_many_bytes, :maximum)
|
22
|
+
default_message = options[:too_many_bytes]
|
23
|
+
errors_options[:count] = options[:maximum]
|
24
|
+
errors_options[:message] ||= default_message if default_message
|
25
|
+
record.errors.add(attribute, :too_many_bytes, errors_options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def requires_transcoding?(value)
|
30
|
+
encoding.present? && encoding != value.encoding
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module HelperMethods
|
35
|
+
def validates_bytesize_of(*attr_names)
|
36
|
+
validates_with ActiveModel::Validations::BytesizeValidator, _merge_attributes(attr_names)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ActiveModel
|
2
|
+
module Validations
|
3
|
+
class NotNullValidator < ActiveModel::EachValidator
|
4
|
+
def validate_each(record, attribute, value)
|
5
|
+
if value.nil?
|
6
|
+
errors_options = options.except(:must_be_set)
|
7
|
+
default_message = options[:must_be_set]
|
8
|
+
errors_options[:message] ||= default_message if default_message
|
9
|
+
record.errors.add(attribute, :must_be_set, errors_options)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module HelperMethods
|
15
|
+
def validates_not_null_of(*attr_names)
|
16
|
+
validates_with ActiveModel::Validations::NotNullValidator, _merge_attributes(attr_names)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
en:
|
2
|
+
errors:
|
3
|
+
# The values :model, :attribute and :value are always available for interpolation
|
4
|
+
# The value :count is available when applicable. Can be used for pluralization.
|
5
|
+
messages:
|
6
|
+
too_many_bytes: "is too long (maximum is %{count} bytes)"
|
7
|
+
characters_outside_basic_multilingual_plane: "contains characters outside Unicode's basic multilingual plane"
|
8
|
+
must_be_set: "must be set"
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'active_model/validations/bytesize'
|
2
|
+
require 'active_model/validations/not_null'
|
3
|
+
require 'active_model/validations/basic_multilingual_plane'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module Validations
|
7
|
+
class DatabaseConstraintsValidator < ActiveModel::EachValidator
|
8
|
+
TYPE_LIMITS = {
|
9
|
+
char: { validator: ActiveModel::Validations::LengthValidator },
|
10
|
+
varchar: { validator: ActiveModel::Validations::LengthValidator },
|
11
|
+
varbinary: { validator: ActiveModel::Validations::BytesizeValidator },
|
12
|
+
|
13
|
+
tinytext: { validator: ActiveModel::Validations::BytesizeValidator, default_maximum: 2 ** 8 - 1 },
|
14
|
+
text: { validator: ActiveModel::Validations::BytesizeValidator, default_maximum: 2 ** 16 - 1 },
|
15
|
+
mediumtext: { validator: ActiveModel::Validations::BytesizeValidator, default_maximum: 2 ** 24 - 1 },
|
16
|
+
longtext: { validator: ActiveModel::Validations::BytesizeValidator, default_maximum: 2 ** 32 - 1 },
|
17
|
+
|
18
|
+
tinyblob: { validator: ActiveModel::Validations::BytesizeValidator, default_maximum: 2 ** 8 - 1 },
|
19
|
+
blob: { validator: ActiveModel::Validations::BytesizeValidator, default_maximum: 2 ** 16 - 1 },
|
20
|
+
mediumblob: { validator: ActiveModel::Validations::BytesizeValidator, default_maximum: 2 ** 24 - 1 },
|
21
|
+
longblob: { validator: ActiveModel::Validations::BytesizeValidator, default_maximum: 2 ** 32 - 1 },
|
22
|
+
}
|
23
|
+
|
24
|
+
attr_reader :klass, :constraints
|
25
|
+
|
26
|
+
VALID_CONSTRAINTS = Set[:size, :basic_multilingual_plane, :not_null]
|
27
|
+
|
28
|
+
def initialize(options = {})
|
29
|
+
@klass = options[:class]
|
30
|
+
@constraints = Set.new(Array.wrap(options[:in]) + Array.wrap(options[:with]))
|
31
|
+
@constraint_validators = {}
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def check_validity!
|
36
|
+
invalid_constraints = constraints - VALID_CONSTRAINTS
|
37
|
+
|
38
|
+
raise ArgumentError, "You have to specify what constraints to validate for." if constraints.empty?
|
39
|
+
raise ArgumentError, "#{invalid_constraints.map(&:inspect).join(',')} is not a valid constraint." unless invalid_constraints.empty?
|
40
|
+
end
|
41
|
+
|
42
|
+
def not_null_validator(column)
|
43
|
+
return unless constraints.include?(:not_null)
|
44
|
+
return if column.null
|
45
|
+
|
46
|
+
ActiveModel::Validations::NotNullValidator.new(attributes: [column.name.to_sym], class: klass)
|
47
|
+
end
|
48
|
+
|
49
|
+
def size_validator(column)
|
50
|
+
return unless constraints.include?(:size)
|
51
|
+
return unless column.text? || column.binary?
|
52
|
+
|
53
|
+
column_type = column.sql_type.sub(/\(.*\z/, '').gsub(/\s/, '_').to_sym
|
54
|
+
type_limit = TYPE_LIMITS.fetch(column_type, {})
|
55
|
+
validator_class = type_limit[:validator]
|
56
|
+
maximum = column.limit || type_limit[:default_maximum]
|
57
|
+
encoding = column.text? ? determine_encoding(column) : nil
|
58
|
+
|
59
|
+
if validator_class && maximum
|
60
|
+
validator_class.new(attributes: [column.name.to_sym], class: klass, maximum: maximum, encoding: encoding)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def basic_multilingual_plane_validator(column)
|
65
|
+
return unless constraints.include?(:basic_multilingual_plane)
|
66
|
+
return unless column.text? && column.collation =~ /\Autf8(?:mb3)?_/
|
67
|
+
ActiveModel::Validations::BasicMultilingualPlaneValidator.new(attributes: [column.name.to_sym], class: klass)
|
68
|
+
end
|
69
|
+
|
70
|
+
def attribute_validators(attribute)
|
71
|
+
@constraint_validators[attribute] ||= begin
|
72
|
+
column = klass.columns_hash[attribute.to_s] or raise ArgumentError.new("Model #{self.class.name} does not have column #{column_name}!")
|
73
|
+
|
74
|
+
[
|
75
|
+
not_null_validator(column),
|
76
|
+
size_validator(column),
|
77
|
+
basic_multilingual_plane_validator(column),
|
78
|
+
].compact
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def validate_each(record, attribute, value)
|
83
|
+
attribute_validators(attribute).each do |validator|
|
84
|
+
validator.validate_each(record, attribute, value)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def determine_encoding(column)
|
91
|
+
case column.collation
|
92
|
+
when /\Autf8/; Encoding.find('utf-8')
|
93
|
+
else raise NotImplementedError, "Don't know how to determine the Ruby encoding for MySQL's #{column.collation} collation."
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
module ActiveModel
|
101
|
+
module Validations
|
102
|
+
module ClassMethods
|
103
|
+
def validates_database_constraints_of(*attr_names)
|
104
|
+
validates_with ActiveRecord::Validations::DatabaseConstraintsValidator, _merge_attributes(attr_names)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'active_record/database_validations'
|
@@ -0,0 +1 @@
|
|
1
|
+
require "active_record/database_validations"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
class BasicMultilingualPlaneValidatorTest < Minitest::Test
|
5
|
+
|
6
|
+
class Model
|
7
|
+
include ActiveModel::Validations
|
8
|
+
|
9
|
+
attr_accessor :unicode
|
10
|
+
validates :unicode, basic_multilingual_plane: true
|
11
|
+
end
|
12
|
+
|
13
|
+
def setup
|
14
|
+
@model = Model.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_basic_multilingual_plane_string
|
18
|
+
@model.unicode = 'basic multilingual ünicode'
|
19
|
+
assert @model.valid?
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_emoji
|
24
|
+
@model.unicode = '💩'
|
25
|
+
assert @model.invalid?
|
26
|
+
assert_equal ["contains characters outside Unicode's basic multilingual plane"], @model.errors[:unicode]
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_nil
|
30
|
+
@model.unicode = nil
|
31
|
+
assert @model.valid?
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
class BytesizeValidatorTest < Minitest::Test
|
5
|
+
|
6
|
+
class Model
|
7
|
+
include ActiveModel::Validations
|
8
|
+
|
9
|
+
attr_accessor :data
|
10
|
+
validates :data, bytesize: { maximum: 100, encoding: 'utf-8' }
|
11
|
+
end
|
12
|
+
|
13
|
+
def setup
|
14
|
+
@model = Model.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_nil_is_valid
|
18
|
+
@model.data = nil
|
19
|
+
assert @model.valid?
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_fitting_strings_are_valid
|
23
|
+
@model.data = ''
|
24
|
+
assert @model.valid?
|
25
|
+
|
26
|
+
@model.data = 'a' * 100
|
27
|
+
assert @model.valid?
|
28
|
+
|
29
|
+
@model.data = 'ü' * 50
|
30
|
+
assert @model.valid?
|
31
|
+
|
32
|
+
@model.data = "\0" * 100
|
33
|
+
assert @model.valid?
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_too_large_binary_values_are_invalid
|
37
|
+
@model.data = "\0" * 101
|
38
|
+
assert @model.invalid?
|
39
|
+
assert_equal ["is too long (maximum is 100 bytes)"], @model.errors[:data]
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_too_large_unicode_values_are_invalid
|
43
|
+
@model.data = '💩' * 26
|
44
|
+
assert @model.invalid?
|
45
|
+
assert_equal ["is too long (maximum is 100 bytes)"], @model.errors[:data]
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_transcoding
|
49
|
+
@model.data = ('ü' * 51).encode('ISO-8859-15')
|
50
|
+
assert @model.invalid?
|
51
|
+
assert_equal ["is too long (maximum is 100 bytes)"], @model.errors[:data]
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
ActiveRecord::Migration.suppress_messages do
|
6
|
+
ActiveRecord::Migration.create_table("unicorns", force: true, options: "CHARACTER SET utf8mb3") do |t|
|
7
|
+
t.string :string, limit: 40
|
8
|
+
t.text :tinytext, limit: 255
|
9
|
+
t.binary :blob
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Unicorn < ActiveRecord::Base
|
14
|
+
# no validations
|
15
|
+
end
|
16
|
+
|
17
|
+
class DataLossTest < Minitest::Test
|
18
|
+
def test_strict_mode_is_disabled
|
19
|
+
refute Unicorn.connection.show_variable(:sql_mode).include?('STRICT_ALL_TABLES')
|
20
|
+
refute Unicorn.connection.show_variable(:sql_mode).include?('STRICT_TRANS_TABLES')
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_bounded_string_fields_silently_loses_data_when_strict_mode_is_disabled
|
24
|
+
fitting_string = 'ü' * 40
|
25
|
+
assert Unicorn.columns_hash['string'].limit >= fitting_string.length
|
26
|
+
unicorn = Unicorn.create(string: fitting_string)
|
27
|
+
assert_equal fitting_string, unicorn.reload.string
|
28
|
+
|
29
|
+
overflowing_string = 'ü' * 41
|
30
|
+
assert Unicorn.columns_hash['string'].limit < overflowing_string.length
|
31
|
+
unicorn = Unicorn.create(string: overflowing_string)
|
32
|
+
assert unicorn.reload.string != overflowing_string
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_text_field_silently_loses_data_when_strict_mode_is_disabled
|
36
|
+
fitting_tinytext = '写' * 85
|
37
|
+
assert_equal 255, fitting_tinytext.bytesize
|
38
|
+
unicorn = Unicorn.create(tinytext: fitting_tinytext) # <= 255, the TINYTEXT limit
|
39
|
+
assert_equal fitting_tinytext, unicorn.reload.tinytext
|
40
|
+
|
41
|
+
overflowing_tinytext = 'ü' * 128
|
42
|
+
assert_equal 256, overflowing_tinytext.bytesize # > 255, the TINYTEXT limit
|
43
|
+
unicorn = Unicorn.create(tinytext: overflowing_tinytext)
|
44
|
+
assert unicorn.reload.tinytext != overflowing_tinytext
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_binary_field_silently_loses_data_when_strict_mode_is_disabled
|
48
|
+
fitting_blob = [].pack('x65535')
|
49
|
+
assert_equal 65535, fitting_blob.bytesize
|
50
|
+
unicorn = Unicorn.create(blob: fitting_blob) # <= 65535, the BLOB limit
|
51
|
+
assert_equal fitting_blob, unicorn.reload.blob
|
52
|
+
|
53
|
+
overflowing_blob = [].pack('x65536')
|
54
|
+
assert_equal 65536, overflowing_blob.bytesize # > 65535, the BLOB limit
|
55
|
+
unicorn = Unicorn.create(blob: overflowing_blob)
|
56
|
+
assert unicorn.reload.blob != overflowing_blob
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_unchecked_utf8mb3_field_silently_loses_data_when_strict_mode_is_disabled
|
60
|
+
emoji = '💩'
|
61
|
+
assert_equal 1, emoji.length
|
62
|
+
assert_equal 4, emoji.bytesize
|
63
|
+
unicorn = Unicorn.create(string: emoji)
|
64
|
+
assert unicorn.reload.string != emoji
|
65
|
+
end
|
66
|
+
end
|
data/test/database.yml
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
ActiveRecord::Migration.suppress_messages do
|
6
|
+
ActiveRecord::Migration.create_table("foos", force: true, options: "CHARACTER SET utf8mb3") do |t|
|
7
|
+
t.string :string, limit: 40
|
8
|
+
t.text :tinytext, limit: 255
|
9
|
+
t.binary :varbinary, limit: 255
|
10
|
+
t.binary :blob
|
11
|
+
t.text :not_null_text, null: false
|
12
|
+
t.integer :checked, null: false, default: 0
|
13
|
+
t.integer :unchecked, null: false
|
14
|
+
end
|
15
|
+
|
16
|
+
ActiveRecord::Migration.create_table("bars", force: true, options: "CHARACTER SET utf8mb4") do |t|
|
17
|
+
t.string :mb4_string
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Foo < ActiveRecord::Base
|
22
|
+
validates :string, :tinytext, :varbinary, :blob, database_constraints: :size
|
23
|
+
validates :checked, database_constraints: :not_null
|
24
|
+
validates :not_null_text, database_constraints: [:size, :basic_multilingual_plane]
|
25
|
+
end
|
26
|
+
|
27
|
+
class Bar < ActiveRecord::Base
|
28
|
+
validates :mb4_string, database_constraints: :basic_multilingual_plane
|
29
|
+
end
|
30
|
+
|
31
|
+
class DatabaseConstraintsValidatorTest < Minitest::Test
|
32
|
+
def test_argument_validation
|
33
|
+
assert_raises(ArgumentError) { Bar.validates(:mb4_string, database_constraints: []) }
|
34
|
+
assert_raises(ArgumentError) { Bar.validates(:mb4_string, database_constraints: true) }
|
35
|
+
assert_raises(ArgumentError) { Bar.validates(:mb4_string, database_constraints: :bogus) }
|
36
|
+
assert_raises(ArgumentError) { Bar.validates(:mb4_string, database_constraints: [:size, :bogus]) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_validators_are_defined
|
40
|
+
assert_kind_of ActiveRecord::Validations::DatabaseConstraintsValidator, Foo._validators[:string].first
|
41
|
+
assert_kind_of ActiveRecord::Validations::DatabaseConstraintsValidator, Foo._validators[:tinytext].first
|
42
|
+
assert_kind_of ActiveRecord::Validations::DatabaseConstraintsValidator, Foo._validators[:varbinary].first
|
43
|
+
assert_kind_of ActiveRecord::Validations::DatabaseConstraintsValidator, Foo._validators[:blob].first
|
44
|
+
assert_kind_of ActiveRecord::Validations::DatabaseConstraintsValidator, Foo._validators[:checked].first
|
45
|
+
assert_kind_of ActiveRecord::Validations::DatabaseConstraintsValidator, Foo._validators[:not_null_text].first
|
46
|
+
|
47
|
+
assert_equal [], Foo._validators[:unchecked]
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_not_null_field_defines_not_null_validator_if_requested
|
51
|
+
validator = Foo._validators[:checked].first
|
52
|
+
subvalidators = validator.attribute_validators(:checked)
|
53
|
+
assert_equal 1, subvalidators.length
|
54
|
+
assert_kind_of ActiveModel::Validations::NotNullValidator, subvalidators.first
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_string_field_defines_length_validator_by_default
|
58
|
+
validator = Foo._validators[:string].first
|
59
|
+
subvalidators = validator.attribute_validators(:string)
|
60
|
+
assert_equal 1, subvalidators.length
|
61
|
+
assert_kind_of ActiveModel::Validations::LengthValidator, subvalidators.first
|
62
|
+
assert_equal 40, subvalidators.first.options[:maximum]
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_blob_field_defines_bytesize_validator
|
66
|
+
validator = Foo._validators[:blob].first
|
67
|
+
subvalidators = validator.attribute_validators(:blob)
|
68
|
+
assert_equal 1, subvalidators.length
|
69
|
+
assert_kind_of ActiveModel::Validations::BytesizeValidator, subvalidators.first
|
70
|
+
assert_equal 65535, subvalidators.first.options[:maximum]
|
71
|
+
assert_equal nil, subvalidators.first.encoding
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_not_null_text_field_defines_requested_bytesize_validator_and_unicode_validator
|
75
|
+
validator = Foo._validators[:not_null_text].first
|
76
|
+
subvalidators = validator.attribute_validators(:not_null_text)
|
77
|
+
assert_equal 2, subvalidators.length
|
78
|
+
|
79
|
+
assert_kind_of ActiveModel::Validations::BytesizeValidator, subvalidators.first
|
80
|
+
assert_kind_of ActiveModel::Validations::BasicMultilingualPlaneValidator, subvalidators.second
|
81
|
+
assert_equal 65535, subvalidators.first.options[:maximum]
|
82
|
+
assert_equal Encoding.find('utf-8'), subvalidators.first.encoding
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_not_null_columns_with_a_default_value
|
86
|
+
assert Foo.new.valid?
|
87
|
+
assert Foo.new(checked: 1).valid?
|
88
|
+
refute Foo.new(checked: nil).valid?
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_should_not_create_a_validor_for_a_utf8mb4_field
|
92
|
+
assert Bar.new(mb4_string: '💩').valid?
|
93
|
+
Bar._validators[:mb4_string].first.attribute_validators(:mb4_string).empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_error_messages
|
97
|
+
foo = Foo.new(string: 'ü' * 41, checked: nil, not_null_text: '💩')
|
98
|
+
refute foo.save
|
99
|
+
|
100
|
+
assert_equal ["is too long (maximum is 40 characters)"], foo.errors[:string]
|
101
|
+
assert_equal ["must be set"], foo.errors[:checked]
|
102
|
+
assert_equal ["contains characters outside Unicode's basic multilingual plane"], foo.errors[:not_null_text]
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_encoding_craziness
|
106
|
+
foo = Foo.new(tinytext: ('ü' * 128).encode('ISO-8859-15'), string: ('ü' * 40).encode('ISO-8859-15'))
|
107
|
+
assert foo.invalid?
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class NotNullValidatorTest < Minitest::Test
|
4
|
+
|
5
|
+
class Model
|
6
|
+
include ActiveModel::Validations
|
7
|
+
|
8
|
+
attr_accessor :not_null_attribute
|
9
|
+
validates :not_null_attribute, not_null: true
|
10
|
+
end
|
11
|
+
|
12
|
+
def setup
|
13
|
+
@model = Model.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_nil
|
17
|
+
@model.not_null_attribute = nil
|
18
|
+
assert @model.invalid?
|
19
|
+
assert_equal ['must be set'], @model.errors[:not_null_attribute]
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_blank
|
23
|
+
@model.not_null_attribute = ''
|
24
|
+
assert @model.valid?
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_false
|
28
|
+
@model.not_null_attribute = false
|
29
|
+
assert @model.valid?
|
30
|
+
end
|
31
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require "minitest/autorun"
|
6
|
+
require "minitest/pride"
|
7
|
+
|
8
|
+
require "yaml"
|
9
|
+
|
10
|
+
require "active_record/database_validations"
|
11
|
+
|
12
|
+
database_yml = YAML.load_file(File.expand_path('../database.yml', __FILE__))
|
13
|
+
ActiveRecord::Base.establish_connection(database_yml['test'])
|
14
|
+
I18n.enforce_available_locales = false
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-databasevalidations
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Willem van Bergen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4'
|
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.5'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.5'
|
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: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mysql2
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Opt-in validations for your ActiveRecord models based on your MySQL database
|
84
|
+
constraints, including text field size, UTF-8 encoding issues, and NOT NULL constraints.
|
85
|
+
email:
|
86
|
+
- willem@railsdoctors.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".travis.yml"
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- activerecord-databasevalidations.gemspec
|
98
|
+
- lib/active_model/validations/basic_multilingual_plane.rb
|
99
|
+
- lib/active_model/validations/bytesize.rb
|
100
|
+
- lib/active_model/validations/not_null.rb
|
101
|
+
- lib/active_record/database_validations.rb
|
102
|
+
- lib/active_record/database_validations/locale/en.yml
|
103
|
+
- lib/active_record/database_validations/version.rb
|
104
|
+
- lib/active_record/validations/database_constraints.rb
|
105
|
+
- lib/activerecord-databasevalidations.rb
|
106
|
+
- lib/activerecord/databasevalidations.rb
|
107
|
+
- test/basic_multilingual_plane_validator_test.rb
|
108
|
+
- test/bytesize_validator_test.rb
|
109
|
+
- test/data_loss_test.rb
|
110
|
+
- test/database.yml
|
111
|
+
- test/database_constraints_validator_test.rb
|
112
|
+
- test/not_null_validator_test.rb
|
113
|
+
- test/test_helper.rb
|
114
|
+
homepage: https://github.com/wvanbergen/activerecord-database_validations
|
115
|
+
licenses:
|
116
|
+
- MIT
|
117
|
+
metadata: {}
|
118
|
+
post_install_message:
|
119
|
+
rdoc_options: []
|
120
|
+
require_paths:
|
121
|
+
- lib
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 2.2.2
|
135
|
+
signing_key:
|
136
|
+
specification_version: 4
|
137
|
+
summary: Add validations to your ActiveRecord models based on MySQL database constraints.
|
138
|
+
test_files:
|
139
|
+
- test/basic_multilingual_plane_validator_test.rb
|
140
|
+
- test/bytesize_validator_test.rb
|
141
|
+
- test/data_loss_test.rb
|
142
|
+
- test/database.yml
|
143
|
+
- test/database_constraints_validator_test.rb
|
144
|
+
- test/not_null_validator_test.rb
|
145
|
+
- test/test_helper.rb
|