activerecord-databasevalidations 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.3
7
+
8
+ before_script:
9
+ - mysql -e 'create database database_validations;'
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ end
8
+
9
+ task :default => :test
@@ -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,7 @@
1
+ require 'active_record'
2
+ require 'active_support/i18n'
3
+ require 'active_record/database_validations/version'
4
+
5
+ require 'active_record/validations/database_constraints'
6
+
7
+ I18n.load_path << File.dirname(__FILE__) + '/database_validations/locale/en.yml'
@@ -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,5 @@
1
+ module ActiveRecord
2
+ module DatabaseValidations
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -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,6 @@
1
+ test:
2
+ adapter: mysql2
3
+ database: database_validations
4
+ username: travis
5
+ encoding: utf8
6
+ strict: false
@@ -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
@@ -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