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 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