├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── errors └── unsupported_type.rb ├── lib ├── arguments.rb └── validates_type.rb ├── locale ├── en.yml └── es.yml ├── spec ├── active_model │ ├── base_spec.rb │ ├── modifiers_spec.rb │ └── types_spec.rb ├── active_model_helper.rb ├── active_record_helper.rb ├── activerecord │ ├── base_spec.rb │ ├── modifiers_spec.rb │ └── types_spec.rb ├── arguments_spec.rb └── spec_helper.rb ├── validates_type.gemspec └── version.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .ruby* 2 | *.gem 3 | .bundle 4 | .idea 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | - 2.1.0 5 | - 2.2.1 6 | - 2.3.0 7 | script: bundle exec rspec spec 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | validates_type (4.0.0) 5 | activemodel (~> 7.0) 6 | ruby-boolean (~> 1.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | activemodel (7.0.4) 12 | activesupport (= 7.0.4) 13 | activerecord (7.0.4) 14 | activemodel (= 7.0.4) 15 | activesupport (= 7.0.4) 16 | activesupport (7.0.4) 17 | concurrent-ruby (~> 1.0, >= 1.0.2) 18 | i18n (>= 1.6, < 2) 19 | minitest (>= 5.1) 20 | tzinfo (~> 2.0) 21 | concurrent-ruby (1.1.10) 22 | diff-lcs (1.5.0) 23 | i18n (1.12.0) 24 | concurrent-ruby (~> 1.0) 25 | mini_portile2 (2.8.0) 26 | minitest (5.16.3) 27 | rake (13.0.6) 28 | rspec (3.4.0) 29 | rspec-core (~> 3.4.0) 30 | rspec-expectations (~> 3.4.0) 31 | rspec-mocks (~> 3.4.0) 32 | rspec-core (3.4.4) 33 | rspec-support (~> 3.4.0) 34 | rspec-expectations (3.4.0) 35 | diff-lcs (>= 1.2.0, < 2.0) 36 | rspec-support (~> 3.4.0) 37 | rspec-mocks (3.4.1) 38 | diff-lcs (>= 1.2.0, < 2.0) 39 | rspec-support (~> 3.4.0) 40 | rspec-support (3.4.1) 41 | ruby-boolean (1.3.4) 42 | sqlite3 (1.5.2) 43 | mini_portile2 (~> 2.8.0) 44 | tzinfo (2.0.5) 45 | concurrent-ruby (~> 1.0) 46 | 47 | PLATFORMS 48 | ruby 49 | 50 | DEPENDENCIES 51 | activerecord (>= 3.0.0) 52 | bundler (~> 2.0) 53 | rake 54 | rspec (= 3.4) 55 | sqlite3 (~> 1.3) 56 | validates_type! 57 | 58 | BUNDLED WITH 59 | 2.1.4 60 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Jake Yesbeck 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. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## validates_type 2 | 3 | [![Build Status](https://travis-ci.org/yez/validates_type.svg?branch=master)](https://travis-ci.org/yez/validates_type) 4 | [![Open Source Helpers](https://www.codetriage.com/yez/validates_type/badges/users.svg)](https://www.codetriage.com/yez/validates_type) 5 | 6 | ### Rails type validation 7 | 8 | #### Purpose 9 | 10 | Most Rails applications will have types coerced by their ORM connection adapters (like the `pg` gem or `mysql2`). However, this only useful for applications with very well defined schemas. If your application has a legacy storage layer that you can no longer modify or a lot of `store_accessor` columns, this solution is a nice middle ground to ensure your data is robust. 11 | 12 | This also prevents your data from being coerced into values that you did not intend by ActiveRecord adapters. 13 | 14 | #### Usage 15 | 16 | ##### With ActiveRecord 17 | 18 | ```ruby 19 | class Foo < ActiveRecord::Base 20 | 21 | # validate that attribute :bar is a String 22 | validates_type :bar, :string 23 | 24 | # validate that attribute :baz is an Integer with a custom error message 25 | validates_type :baz, :integer, message: 'Baz must be an Integer' 26 | 27 | # validate that attribute :qux is an Array, allow blank 28 | validates_type :qux, :array, allow_blank: true 29 | 30 | # validate that attribute :whatever is a Boolean 31 | validates_type :whatever, :boolean 32 | 33 | # validate that attribute :thing is a Float or nil 34 | validates_type :thing, :float, allow_nil: true 35 | 36 | # validate that attribute :when is a Time 37 | validates_type :when, :time 38 | 39 | # validate that attribute :birthday is a Date or blank 40 | validates_type :birthday, :date, allow_blank: true 41 | 42 | # validate that attribute :fox is of type Custom 43 | validates_type :fox, Custom 44 | end 45 | ``` 46 | 47 | ##### With ActiveModel 48 | 49 | ```ruby 50 | class Bar 51 | include ActiveModel::Validations 52 | 53 | attr_accessor :foo, :qux 54 | 55 | validates_type :foo, :string 56 | 57 | # Custom error message support 58 | validates_type :qux, :boolean, message: 'Attribute qux must be a boolean!' 59 | end 60 | ``` 61 | 62 | ##### With shortcut syntax 63 | 64 | ```ruby 65 | class Banana < ActiveRecord::Base 66 | 67 | # The banana's peel attribute must be a string 68 | validates :peel, type: { type: :string } 69 | 70 | # Custom error message for ripeness of banana 71 | validates :ripe, type: { type: :boolean, message: 'Only ripe bananas allowed' } 72 | end 73 | ``` 74 | 75 | ##### With multiple modifiers 76 | 77 | ```ruby 78 | class Foo < ActiveRecord::Base 79 | # validate that attribute :baz is an Integer with a custom error message 80 | # only if :conditional_method evaluates to true 81 | validates_type :baz, :integer, message: 'Baz must be an Integer', if: :conditional_method 82 | 83 | def conditional_method 84 | # some kind of logic that is important to pass 85 | end 86 | 87 | # validate that attribute :baz is a Float and is included in a specific array 88 | validates_type :foo, :float, in: [1.0, 2.5, 3.0] 89 | end 90 | ``` 91 | 92 | #### Supported types 93 | 94 | - `:array` 95 | - `:big_decimal` 96 | - `:boolean` 97 | - `:date` 98 | - `:float` 99 | - `:hash` 100 | - `:integer` 101 | - `:string` 102 | - `:symbol` 103 | - `:time` 104 | 105 | #### Any class name, possibly something custom defined in your app, is also game. 106 | 107 | #### Contributing 108 | 109 | Please feel free to submit pull requests to address issues that you may find with this software. One commit per pull request is preferred please and thank you. 110 | -------------------------------------------------------------------------------- /errors/unsupported_type.rb: -------------------------------------------------------------------------------- 1 | # Error class to raise if unsupported type given to validates_url 2 | module ValidatesType 3 | class UnsupportedType < StandardError; end 4 | end 5 | -------------------------------------------------------------------------------- /lib/arguments.rb: -------------------------------------------------------------------------------- 1 | module ValidatesType 2 | # Wrapper class for arguments consumed by validates_with 3 | class Arguments 4 | # @initialize 5 | # param: attribute_name - name of attribute that will be validated 6 | # param: attribute_type - type for which to validate the attribute against 7 | # param: options - extra options to pass along to the validator 8 | # i.e. allow_nil: true, message: 'my custom message' 9 | # return: nil 10 | def initialize(attribute_name, attribute_type, options) 11 | @attribute_name = attribute_name 12 | @attribute_type = attribute_type 13 | @options = options.is_a?(Hash) ? options : {} 14 | end 15 | 16 | # format expected by _merge_attributes 17 | # 18 | # @to_validation_attributes 19 | # return: - cardinality of 2 20 | def to_validation_attributes 21 | [attribute_name, merged_options] 22 | end 23 | 24 | private 25 | 26 | attr_reader :attribute_name, :attribute_type, :options 27 | 28 | # helper method to compact all the options together along 29 | # with the type for validation 30 | # 31 | # @merged_options 32 | # return: 33 | def merged_options 34 | type.merge(options) 35 | end 36 | 37 | # helper method to impose the type for validation into an option 38 | # that will be merged later 39 | # 40 | # @type 41 | # return: 42 | def type 43 | { type: attribute_type } 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/validates_type.rb: -------------------------------------------------------------------------------- 1 | require 'ruby-boolean' 2 | require 'active_model' 3 | require 'active_support/i18n' 4 | 5 | I18n.load_path << File.dirname(__FILE__) + '/../locale/en.yml' 6 | 7 | require_relative '../errors/unsupported_type' 8 | require_relative './arguments' 9 | 10 | module ActiveModel 11 | module Validations 12 | class TypeValidator < ActiveModel::EachValidator 13 | # Set default message for failure here 14 | # 15 | # @initialize 16 | # param: options - options hash of how to validate this attribute 17 | # including custom messaging due to failures, specifying 18 | # the type of the attribute to validate against, etc. 19 | # return: result of ActiveModel::Validations::EachValidator initialize 20 | def initialize(options) 21 | merged_options = { message: :type }.merge(options) 22 | 23 | super(merged_options) 24 | end 25 | 26 | # Validate that the value is the type we expect 27 | # 28 | # @validate_each 29 | # param: record - subject containing attribute to validate 30 | # param: attribute - name of attribute to validate 31 | # param: value - value of attribute to validate 32 | # return: nil 33 | def validate_each(record, attribute, value) 34 | value_to_test = type_before_coercion(record, attribute, value) 35 | expected_type = type_class(options[:type]) 36 | 37 | add_errors_or_raise(options, record, attribute) unless value_to_test.is_a?(expected_type) 38 | end 39 | 40 | private 41 | 42 | # Helper method to either add messages to the errors object 43 | # or raise an exception in :strict mode 44 | # 45 | # @add_errors_or_raise 46 | # param: options - options hash with strict flag or class 47 | # param: record - subject containg attribute to validate 48 | # param: attribute - name of attribute under validation 49 | # return: nil 50 | def add_errors_or_raise(options, record, attribute) 51 | error = options_error(options[:strict]) 52 | 53 | raise error unless error.nil? 54 | 55 | options = { type: type_class(options[:type]) }.merge(message: options[:message]) 56 | record.errors.add(attribute, :type, **options) 57 | end 58 | 59 | # Helper method to return the base expected error: 60 | # ActiveModel::StrictValidationFailed, a custom error, or nil 61 | # 62 | # @options_error 63 | # param: strict_error - either the flag 64 | # to raise an error or the actual error to raise 65 | # return: custom error, ActiveModel::StrictValidationFailed, or nil 66 | def options_error(strict_error) 67 | return if strict_error.nil? 68 | 69 | if strict_error == true 70 | ActiveModel::StrictValidationFailed 71 | elsif strict_error.ancestors.include?(Exception) 72 | strict_error 73 | end 74 | end 75 | 76 | # Helper method to get back a class constant from the given type option 77 | # If the type option is a class, it will be returned as is. 78 | # If it is not a class, the method will try to convert it to a class constant. 79 | # 80 | # ex: 81 | # type_class(:string) -> String 82 | # type_class(String) -> String 83 | # type_class(Custom) -> Custom 84 | # @type_class 85 | # param: type 86 | # return: class constant 87 | def type_class(type) 88 | @type_class ||= type.is_a?(Class) ? type : symbol_class(type) 89 | end 90 | 91 | # Helper method to convert a symbol into a class constant 92 | # 93 | # ex: 94 | # symbol_class(:string) -> String 95 | # symbol_class(:boolean) -> Boolean 96 | # symbol_class(:hash) -> Hash 97 | # 98 | # @symbol_class 99 | # param: symbol - symbol to turn into a class constant 100 | # return: class constant of supported types or raises UnsupportedType 101 | def symbol_class(symbol) 102 | { 103 | array: Array, 104 | boolean: Boolean, 105 | float: Float, 106 | hash: Hash, 107 | integer: Integer, 108 | string: String, 109 | symbol: Symbol, 110 | time: Time, 111 | date: Date, 112 | big_decimal: BigDecimal, 113 | }[symbol] || fail(ValidatesType::UnsupportedType, 114 | "Unsupported type #{ symbol.to_s.camelize } given for validates_type.") 115 | end 116 | 117 | # Helper method to circumvent active record's coercion 118 | # 119 | # @type_before_coercion 120 | # param: record - subject of validation 121 | # param: value - current value of attribute 122 | # return: the value of the attribute before active record's coercion 123 | # or the current value 124 | def type_before_coercion(record, attribute, value) 125 | record.try(:"#{ attribute }_before_type_cast") || value 126 | end 127 | end 128 | 129 | module ClassMethods 130 | # Validates the type of an attribute with supported types: 131 | # - :array 132 | # - :boolean 133 | # - :float 134 | # - :hash 135 | # - :integer 136 | # - :string 137 | # - :symbol 138 | # - :time 139 | # - :date 140 | # - :big_decimal 141 | # 142 | # Also validates the type of an attribute given a custom type class constant. 143 | # 144 | # class Foo 145 | # include ActiveModel::Validations 146 | # 147 | # attr_accessor :thing, :something, :custom_thing 148 | # 149 | # validates_type :thing, :boolean 150 | # validates_type :something, :array 151 | # validates_type :custom_thing, Custom 152 | # end 153 | # 154 | # @validates_type 155 | # param: attribute_name - name of attribute to validate 156 | # param: attribute_type - type of attribute to validate against 157 | # param: options - other common options to validate methods calls 158 | # i.e. message: 'my custom error message' 159 | # return: nil 160 | def validates_type(attribute_name, attribute_type, options = {}) 161 | args = ValidatesType::Arguments.new(attribute_name, attribute_type, options) 162 | validates_with TypeValidator, _merge_attributes(args.to_validation_attributes) 163 | end 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /locale/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | messages: 4 | type: is expected to be a %{type} and is not 5 | -------------------------------------------------------------------------------- /locale/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | errors: 3 | messages: 4 | type: se espera sea un %{type} y no lo es 5 | -------------------------------------------------------------------------------- /spec/active_model/base_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe 'ValidatesType' do 4 | context 'validates_type :attribute' do 5 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:string) } 6 | 7 | it 'adds a validator to the subject' do 8 | klass = subject.class 9 | expect(klass.validators).to_not be_empty 10 | expect(klass.validators).to include(ActiveModel::Validations::TypeValidator) 11 | end 12 | 13 | it 'adds the correct validator to the subject' do 14 | validator = subject.class.validators.find { |v| v.is_a?(ActiveModel::Validations::TypeValidator) } 15 | expect(validator.options[:type]).to eq(:string) 16 | end 17 | end 18 | 19 | context 'validates :attribute, type: { type: type }.merge(other_options)' do 20 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_validator(:string) } 21 | 22 | it 'adds a validator to the subject' do 23 | klass = subject.class 24 | expect(klass.validators).to_not be_empty 25 | expect(klass.validators).to include(ActiveModel::Validations::TypeValidator) 26 | end 27 | 28 | it 'adds the correct validator to the subject' do 29 | validator = subject.class.validators.find { |v| v.is_a?(ActiveModel::Validations::TypeValidator) } 30 | expect(validator.options[:type]).to eq(:string) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/active_model/modifiers_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe 'ValidatesType' do 4 | [:set_accessor_and_long_validator, 5 | :set_accessor_and_validator].each do |validate_version| 6 | context "#{ validate_version }" do 7 | context 'custom modifiers' do 8 | before do 9 | subject.attribute = value 10 | end 11 | 12 | describe 'message' do 13 | subject do 14 | ActiveModel::TypeValidationTestClass.send(validate_version, 15 | :string, message: 'is not a String!') 16 | end 17 | 18 | context 'when validation fails' do 19 | let(:value) { 1 } 20 | 21 | specify do 22 | subject.validate 23 | expect(subject.errors.messages[:attribute][0]).to match(/is not a String!/) 24 | end 25 | end 26 | end 27 | 28 | describe 'allow_nil' do 29 | subject do 30 | ActiveModel::TypeValidationTestClass.send(validate_version, 31 | :string, allow_nil: true) 32 | end 33 | 34 | context 'field value is nil' do 35 | let(:value) { nil } 36 | 37 | specify do 38 | expect(subject).to be_valid 39 | end 40 | end 41 | 42 | context 'field value is not nil' do 43 | context 'field value is the specified type' do 44 | let(:value) { 'I am a string'} 45 | 46 | specify do 47 | expect(subject).to be_valid 48 | end 49 | end 50 | 51 | context 'field value is not specified type' do 52 | let(:value) { -1 } 53 | 54 | specify do 55 | expect(subject).to_not be_valid 56 | end 57 | end 58 | end 59 | end 60 | 61 | describe 'allow_blank' do 62 | subject do 63 | ActiveModel::TypeValidationTestClass.send(validate_version, 64 | :integer, allow_blank: true) 65 | end 66 | 67 | context 'field value is nil' do 68 | let(:value) { nil } 69 | 70 | specify do 71 | expect(subject).to be_valid 72 | end 73 | end 74 | 75 | context 'field value is an empty string' do 76 | let(:value) { '' } 77 | 78 | specify do 79 | expect(subject).to be_valid 80 | end 81 | end 82 | 83 | context 'field value is not nil' do 84 | context 'field value is the specified type' do 85 | let(:value) { 1 } 86 | 87 | specify do 88 | expect(subject).to be_valid 89 | end 90 | end 91 | 92 | context 'field value is not specified type' do 93 | let(:value) { { foo: :bar } } 94 | 95 | specify do 96 | expect(subject).to_not be_valid 97 | end 98 | end 99 | end 100 | end 101 | 102 | describe 'strict' do 103 | subject do 104 | ActiveModel::TypeValidationTestClass.send(validate_version, 105 | :string, strict: true) 106 | end 107 | 108 | context 'field value is expected type' do 109 | let(:value) { 'I am a string' } 110 | 111 | specify do 112 | expect do 113 | subject.valid? 114 | end.to_not raise_error 115 | end 116 | end 117 | 118 | context 'field value is not expected type' do 119 | let(:value) { -1 } 120 | 121 | specify do 122 | expect do 123 | subject.valid? 124 | end.to raise_error(ActiveModel::StrictValidationFailed) 125 | end 126 | 127 | context 'with a custom error class' do 128 | class UhOhSpaghettios < StandardError; end 129 | subject do 130 | ActiveModel::TypeValidationTestClass.send(validate_version, 131 | :string, strict: UhOhSpaghettios) 132 | end 133 | 134 | specify do 135 | expect do 136 | subject.valid? 137 | end.to raise_error(UhOhSpaghettios) 138 | end 139 | end 140 | end 141 | end 142 | 143 | describe 'if' do 144 | subject do 145 | ActiveModel::TypeValidationTestClass.send(validate_version, 146 | :string, if: condition) 147 | end 148 | 149 | context 'field value is expected type' do 150 | let(:value) { 'I am a string' } 151 | 152 | context 'condition is true' do 153 | let(:condition) { ->{ true } } 154 | 155 | specify do 156 | expect(subject).to be_valid 157 | end 158 | end 159 | 160 | context 'condition is false' do 161 | let(:condition) { ->{ false } } 162 | 163 | specify do 164 | expect(subject).to be_valid 165 | end 166 | end 167 | end 168 | 169 | context 'field value is not expected type' do 170 | let(:value) { -1 } 171 | 172 | context 'condition is true' do 173 | let(:condition) { ->{ true } } 174 | 175 | specify do 176 | expect(subject).to_not be_valid 177 | end 178 | end 179 | 180 | context 'condition is false' do 181 | let(:condition) { ->{ false } } 182 | 183 | specify do 184 | expect(subject).to be_valid 185 | end 186 | end 187 | end 188 | end 189 | 190 | describe 'unless' do 191 | subject do 192 | ActiveModel::TypeValidationTestClass.send(validate_version, 193 | :string, unless: condition) 194 | end 195 | 196 | context 'field value is expected type' do 197 | let(:value) { 'I am a string' } 198 | 199 | context 'condition is true' do 200 | let(:condition) { ->{ true } } 201 | 202 | specify do 203 | expect(subject).to be_valid 204 | end 205 | end 206 | 207 | context 'condition is false' do 208 | let(:condition) { ->{ false } } 209 | 210 | specify do 211 | expect(subject).to be_valid 212 | end 213 | end 214 | end 215 | 216 | context 'field value is not expected type' do 217 | let(:value) { -1 } 218 | 219 | context 'condition is true' do 220 | let(:condition) { ->{ true } } 221 | 222 | specify do 223 | expect(subject).to be_valid 224 | end 225 | end 226 | 227 | context 'condition is false' do 228 | let(:condition) { ->{ false } } 229 | 230 | specify do 231 | expect(subject).to_not be_valid 232 | end 233 | end 234 | end 235 | end 236 | 237 | describe 'on' do 238 | let(:value) { nil } 239 | subject do 240 | ActiveModel::TypeValidationTestClass.send(validate_version, 241 | :string, on: :some_test_method) 242 | end 243 | 244 | before do 245 | allow(subject).to receive(:some_test_method) { subject.validate } 246 | end 247 | 248 | context 'on: criteria is met' do 249 | specify do 250 | expect(subject).to receive(:validate) 251 | subject.some_test_method 252 | end 253 | end 254 | 255 | context 'on: criteria is not met' do 256 | specify do 257 | expect(subject).to_not receive(:validate) 258 | subject.valid? 259 | end 260 | end 261 | end 262 | end 263 | 264 | context 'multiple custom modifiers' do 265 | before do 266 | subject.attribute = value 267 | end 268 | 269 | describe 'message: with if:' do 270 | subject do 271 | ActiveModel::TypeValidationTestClass.send(validate_version, 272 | :string, message: 'This is bad!', if: condition) 273 | end 274 | 275 | context 'condition is true' do 276 | let(:condition) { -> { true } } 277 | 278 | context 'field value is the expected type' do 279 | let(:value) { 'I am a string' } 280 | 281 | specify do 282 | expect(subject).to be_valid 283 | end 284 | end 285 | 286 | context 'field value is not the expected type' do 287 | let(:value) { -1 } 288 | 289 | specify do 290 | expect(subject).to_not be_valid 291 | expect(subject.errors.messages[:attribute][0]).to match(/This is bad!/) 292 | end 293 | end 294 | end 295 | 296 | context 'condition is false' do 297 | let(:condition) { -> { false } } 298 | 299 | context 'field value is the expected type' do 300 | let(:value) { 'I am a string' } 301 | 302 | specify do 303 | expect(subject).to be_valid 304 | end 305 | end 306 | 307 | context 'field value is not the expected type' do 308 | let(:value) { -1 } 309 | 310 | specify do 311 | expect(subject).to be_valid 312 | end 313 | end 314 | end 315 | end 316 | end 317 | end 318 | end 319 | end 320 | -------------------------------------------------------------------------------- /spec/active_model/types_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe 'ValidatesType' do 4 | context 'supported types' do 5 | before do 6 | subject.attribute = value 7 | end 8 | 9 | describe 'String' do 10 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:string) } 11 | 12 | context 'field value is a String' do 13 | let(:value) { 'some string' } 14 | 15 | specify do 16 | expect(subject).to be_valid 17 | end 18 | end 19 | 20 | context 'field value is not a String' do 21 | let(:value) { -1 } 22 | specify do 23 | expect(subject).to_not be_valid 24 | end 25 | 26 | specify do 27 | subject.validate 28 | expect(subject.errors).to_not be_empty 29 | expect(subject.errors.messages[:attribute][0]).to match(/is expected to be a String and is not/) 30 | end 31 | end 32 | end 33 | 34 | describe 'Integer' do 35 | 36 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:integer) } 37 | 38 | context 'field value is an Integer' do 39 | let(:value) { 20 } 40 | 41 | specify do 42 | expect(subject).to be_valid 43 | end 44 | end 45 | 46 | context 'field value is not an Integer' do 47 | let(:value) { {} } 48 | 49 | specify do 50 | expect(subject).to_not be_valid 51 | end 52 | 53 | specify do 54 | subject.validate 55 | expect(subject.errors).to_not be_empty 56 | expect(subject.errors.messages[:attribute][0]).to match(/is expected to be a Integer and is not/) 57 | end 58 | end 59 | end 60 | 61 | describe 'Float' do 62 | 63 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:float) } 64 | 65 | context 'field value is a Float' do 66 | let(:value) { 1.2 } 67 | 68 | specify do 69 | expect(subject).to be_valid 70 | end 71 | end 72 | 73 | context 'field value is not a Float' do 74 | let(:value) { 10 } 75 | specify do 76 | expect(subject).to_not be_valid 77 | end 78 | 79 | specify do 80 | subject.validate 81 | expect(subject.errors).to_not be_empty 82 | expect(subject.errors.messages[:attribute][0]).to match(/is expected to be a Float and is not/) 83 | end 84 | end 85 | end 86 | 87 | describe 'Boolean' do 88 | 89 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:boolean) } 90 | 91 | context 'field value is a Boolean' do 92 | let(:value) { true } 93 | 94 | specify do 95 | expect(subject).to be_valid 96 | end 97 | end 98 | 99 | context 'field value is not a Boolean' do 100 | let(:value) { 'true' } 101 | specify do 102 | expect(subject).to_not be_valid 103 | end 104 | 105 | specify do 106 | subject.validate 107 | expect(subject.errors).to_not be_empty 108 | expect(subject.errors.messages[:attribute][0]).to match(/is expected to be a Boolean and is not/) 109 | end 110 | end 111 | end 112 | 113 | describe 'Hash' do 114 | 115 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:hash) } 116 | 117 | context 'field value is a Hash' do 118 | let(:value) { {} } 119 | 120 | specify do 121 | expect(subject).to be_valid 122 | end 123 | end 124 | 125 | context 'field value is not a Hash' do 126 | let(:value) { [] } 127 | specify do 128 | expect(subject).to_not be_valid 129 | end 130 | 131 | specify do 132 | subject.validate 133 | expect(subject.errors).to_not be_empty 134 | expect(subject.errors.messages[:attribute][0]).to match(/is expected to be a Hash and is not/) 135 | end 136 | end 137 | end 138 | 139 | describe 'Array' do 140 | 141 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:array) } 142 | 143 | context 'field value is an Array' do 144 | let(:value) { [] } 145 | 146 | specify do 147 | expect(subject).to be_valid 148 | end 149 | end 150 | 151 | context 'field value is not an Array' do 152 | let(:value) { true } 153 | specify do 154 | expect(subject).to_not be_valid 155 | end 156 | 157 | specify do 158 | subject.validate 159 | expect(subject.errors).to_not be_empty 160 | expect(subject.errors.messages[:attribute][0]).to match(/is expected to be a Array and is not/) 161 | end 162 | end 163 | end 164 | 165 | describe 'Symbol' do 166 | 167 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:symbol) } 168 | 169 | context 'field value is a Symbol' do 170 | let(:value) { :foo } 171 | 172 | specify do 173 | expect(subject).to be_valid 174 | end 175 | end 176 | 177 | context 'field value is not a Symbol' do 178 | let(:value) { [] } 179 | specify do 180 | expect(subject).to_not be_valid 181 | end 182 | 183 | specify do 184 | subject.validate 185 | expect(subject.errors).to_not be_empty 186 | expect(subject.errors.messages[:attribute][0]).to match(/is expected to be a Symbol and is not/) 187 | end 188 | end 189 | end 190 | 191 | describe 'Date' do 192 | 193 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:date) } 194 | 195 | context 'field value is a Date' do 196 | let(:value) { Date.new } 197 | 198 | specify do 199 | expect(subject).to be_valid 200 | end 201 | end 202 | 203 | context 'field value is not a Date' do 204 | let(:value) { :foo } 205 | specify do 206 | expect(subject).to_not be_valid 207 | end 208 | 209 | specify do 210 | subject.validate 211 | expect(subject.errors).to_not be_empty 212 | expect(subject.errors.messages[:attribute][0]).to match(/is expected to be a Date and is not/) 213 | end 214 | end 215 | end 216 | 217 | describe 'Time' do 218 | 219 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:time) } 220 | 221 | context 'field value is a Time' do 222 | let(:value) { Time.new } 223 | 224 | specify do 225 | expect(subject).to be_valid 226 | end 227 | end 228 | 229 | context 'field value is not a Time' do 230 | let(:value) { 123456 } 231 | specify do 232 | expect(subject).to_not be_valid 233 | end 234 | 235 | specify do 236 | subject.validate 237 | expect(subject.errors).to_not be_empty 238 | expect(subject.errors.messages[:attribute][0]).to match(/is expected to be a Time and is not/) 239 | end 240 | end 241 | end 242 | 243 | describe 'BigDecimal' do 244 | 245 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:big_decimal) } 246 | 247 | context 'field value is a BigDecimal' do 248 | let(:value) { BigDecimal(1) } 249 | 250 | specify do 251 | expect(subject).to be_valid 252 | end 253 | end 254 | 255 | context 'field value is not a BigDecimal' do 256 | let(:value) { 123456 } 257 | specify do 258 | expect(subject).to_not be_valid 259 | end 260 | 261 | specify do 262 | subject.validate 263 | expect(subject.errors).to_not be_empty 264 | expect(subject.errors.messages[:attribute][0]).to match(/is expected to be a BigDecimal and is not/) 265 | end 266 | end 267 | end 268 | 269 | context 'passing in a custom message' do 270 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:string, message: 'is not a String!') } 271 | 272 | context 'when validation fails' do 273 | let(:value) { 1 } 274 | 275 | specify do 276 | subject.validate 277 | expect(subject.errors.messages[:attribute][0]).to match(/is not a String!/) 278 | end 279 | end 280 | end 281 | end 282 | 283 | context 'unsupported types' do 284 | describe 'Foo' do 285 | specify do 286 | expect do 287 | subject = ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(:foo) 288 | subject.valid? 289 | end.to raise_error( 290 | ValidatesType::UnsupportedType, 291 | "Unsupported type Foo given for validates_type.") 292 | end 293 | end 294 | end 295 | 296 | context 'custom class' do 297 | class Custom; end 298 | subject { ActiveModel::TypeValidationTestClass.set_accessor_and_long_validator(Custom) } 299 | 300 | before do 301 | subject.attribute = value 302 | end 303 | 304 | context 'field value is of type Custom' do 305 | let(:value) { Custom.new } 306 | 307 | specify do 308 | expect(subject).to be_valid 309 | end 310 | end 311 | 312 | context 'field value is not of type Custom' do 313 | let(:value) { -1 } 314 | 315 | specify do 316 | expect(subject).to_not be_valid 317 | end 318 | 319 | specify do 320 | subject.validate 321 | expect(subject.errors).to_not be_empty 322 | expect(subject.errors.messages[:attribute][0]).to match(/is expected to be a Custom and is not/) 323 | end 324 | end 325 | end 326 | end 327 | -------------------------------------------------------------------------------- /spec/active_model_helper.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | class TypeValidationTestClass 3 | include Validations 4 | 5 | # Creates and returns a new instance of TypeValidationTestClass with 6 | # a "long" style validator: 7 | # 8 | # class TypeValidationTestClass 9 | # attr_accessor :attribute 10 | # 11 | # validates_type :attribute, :string 12 | # end 13 | # 14 | # @set_accessor_and_long_validator 15 | # param: type - type which to validate against 16 | # param: options - extra modifiers/custom messaging 17 | # return: TypeValidationTestClass instance with set validator 18 | def self.set_accessor_and_long_validator(type, options = {}) 19 | self.new.tap do |test_class| 20 | test_class.class_eval { attr_accessor :attribute } 21 | test_class.class_eval { validates_type :attribute, type, options } 22 | end 23 | end 24 | 25 | # Creates and returns a new instance of TypeValidationTestClass with 26 | # a "short" style validator: 27 | # 28 | # class TypeValidationTestClass 29 | # attr_accessor :attribute 30 | # 31 | # validates :attribute, type: { type: :string } 32 | # end 33 | # 34 | # @set_accessor_and_long_validator 35 | # param: type - type which to validate against 36 | # param: options - extra modifiers/custom messaging 37 | # return: TypeValidationTestClass instance with set validator 38 | def self.set_accessor_and_validator(type, options = {}) 39 | self.new.tap do |test_class| 40 | test_class.class_eval { attr_accessor :attribute } 41 | test_class.class_eval { validates :attribute, type: { type: type }.merge(options) } 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/active_record_helper.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Base.establish_connection( 2 | "adapter" => "sqlite3", 3 | "database" => ":memory:" 4 | ) 5 | 6 | ActiveRecord::Migration.verbose = false 7 | ActiveRecord::Schema.define do 8 | create_table :type_validation_tests do |t| 9 | t.string :test_attribute 10 | end 11 | end 12 | 13 | def drop_and_create_column_with_type(column_type) 14 | ActiveRecord::Schema.define do 15 | change_column :type_validation_tests, :test_attribute, column_type.to_sym 16 | end 17 | end 18 | 19 | class TypeValidationTest < ActiveRecord::Base 20 | def self.set_accessor_and_long_validator(type, options = {}) 21 | self.new.tap do |test_class| 22 | test_class.class_eval { validates_type :test_attribute, type, options } 23 | end 24 | end 25 | 26 | def self.set_accessor_and_validator(type, options = {}) 27 | self.new.tap do |test_class| 28 | test_class.class_eval { validates :test_attribute, type: { type: type }.merge(options) } 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/activerecord/base_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe 'ValidatesType' do 4 | context 'validates_type :attribute' do 5 | subject { TypeValidationTest.set_accessor_and_long_validator(:string) } 6 | 7 | it 'adds a validator to the subject' do 8 | klass = subject.class 9 | expect(klass.validators).to_not be_empty 10 | expect(klass.validators).to include(ActiveModel::Validations::TypeValidator) 11 | end 12 | 13 | it 'adds the correct validator to the subject' do 14 | validator = subject.class.validators.find { |v| v.is_a?(ActiveModel::Validations::TypeValidator) } 15 | expect(validator.options[:type]).to eq(:string) 16 | end 17 | end 18 | 19 | context 'validates :attribute, type: { type: type }.merge(other_options)' do 20 | subject { TypeValidationTest.set_accessor_and_validator(:string) } 21 | 22 | it 'adds a validator to the subject' do 23 | klass = subject.class 24 | expect(klass.validators).to_not be_empty 25 | expect(klass.validators).to include(ActiveModel::Validations::TypeValidator) 26 | end 27 | 28 | it 'adds the correct validator to the subject' do 29 | validator = subject.class.validators.find { |v| v.is_a?(ActiveModel::Validations::TypeValidator) } 30 | expect(validator.options[:type]).to eq(:string) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/activerecord/modifiers_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe 'ValidatesType' do 4 | [:set_accessor_and_long_validator, 5 | :set_accessor_and_validator].each do |validate_version| 6 | context "#{ validate_version }" do 7 | context 'custom modifiers' do 8 | before do 9 | subject.test_attribute = value 10 | end 11 | 12 | describe 'message' do 13 | subject do 14 | TypeValidationTest.send(validate_version, 15 | :string, message: 'is not a String!') 16 | end 17 | 18 | context 'when validation fails' do 19 | let(:value) { 1 } 20 | 21 | specify do 22 | subject.validate 23 | expect(subject.errors.messages[:test_attribute][0]).to match(/is not a String!/) 24 | end 25 | end 26 | end 27 | 28 | describe 'allow_nil' do 29 | subject do 30 | TypeValidationTest.send(validate_version, 31 | :string, allow_nil: true) 32 | end 33 | 34 | context 'field value is nil' do 35 | let(:value) { nil } 36 | 37 | specify do 38 | expect(subject).to be_valid 39 | end 40 | end 41 | 42 | context 'field value is not nil' do 43 | context 'field value is the specified type' do 44 | let(:value) { 'I am a string'} 45 | 46 | specify do 47 | expect(subject).to be_valid 48 | end 49 | end 50 | 51 | context 'field value is not specified type' do 52 | let(:value) { -1 } 53 | 54 | specify do 55 | expect(subject).to_not be_valid 56 | end 57 | end 58 | end 59 | end 60 | 61 | describe 'allow_blank' do 62 | subject do 63 | TypeValidationTest.send(validate_version, 64 | :integer, allow_blank: true) 65 | end 66 | 67 | context 'field value is nil' do 68 | let(:value) { nil } 69 | 70 | specify do 71 | expect(subject).to be_valid 72 | end 73 | end 74 | 75 | context 'field value is an empty string' do 76 | let(:value) { '' } 77 | 78 | specify do 79 | expect(subject).to be_valid 80 | end 81 | end 82 | 83 | context 'field value is not nil' do 84 | context 'field value is the specified type' do 85 | let(:value) { 1 } 86 | 87 | specify do 88 | expect(subject).to be_valid 89 | end 90 | end 91 | 92 | context 'field value is not specified type' do 93 | let(:value) { { foo: :bar } } 94 | 95 | specify do 96 | expect(subject).to_not be_valid 97 | end 98 | end 99 | end 100 | end 101 | 102 | describe 'strict' do 103 | subject do 104 | TypeValidationTest.send(validate_version, 105 | :string, strict: true) 106 | end 107 | 108 | context 'field value is expected type' do 109 | let(:value) { 'I am a string' } 110 | 111 | specify do 112 | expect do 113 | subject.valid? 114 | end.to_not raise_error 115 | end 116 | end 117 | 118 | context 'field value is not expected type' do 119 | let(:value) { -1 } 120 | 121 | specify do 122 | expect do 123 | subject.valid? 124 | end.to raise_error(ActiveModel::StrictValidationFailed) 125 | end 126 | 127 | context 'with a custom error class' do 128 | class UhOhSpaghettios < StandardError; end 129 | subject do 130 | TypeValidationTest.send(validate_version, 131 | :string, strict: UhOhSpaghettios) 132 | end 133 | 134 | specify do 135 | expect do 136 | subject.valid? 137 | end.to raise_error(UhOhSpaghettios) 138 | end 139 | end 140 | end 141 | end 142 | 143 | describe 'if' do 144 | subject do 145 | TypeValidationTest.send(validate_version, 146 | :string, if: condition) 147 | end 148 | 149 | context 'field value is expected type' do 150 | let(:value) { 'I am a string' } 151 | 152 | context 'condition is true' do 153 | let(:condition) { ->{ true } } 154 | 155 | specify do 156 | expect(subject).to be_valid 157 | end 158 | end 159 | 160 | context 'condition is false' do 161 | let(:condition) { ->{ false } } 162 | 163 | specify do 164 | expect(subject).to be_valid 165 | end 166 | end 167 | end 168 | 169 | context 'field value is not expected type' do 170 | let(:value) { -1 } 171 | 172 | context 'condition is true' do 173 | let(:condition) { ->{ true } } 174 | 175 | specify do 176 | expect(subject).to_not be_valid 177 | end 178 | end 179 | 180 | context 'condition is false' do 181 | let(:condition) { ->{ false } } 182 | 183 | specify do 184 | expect(subject).to be_valid 185 | end 186 | end 187 | end 188 | end 189 | 190 | describe 'unless' do 191 | subject do 192 | TypeValidationTest.send(validate_version, 193 | :string, unless: condition) 194 | end 195 | 196 | context 'field value is expected type' do 197 | let(:value) { 'I am a string' } 198 | 199 | context 'condition is true' do 200 | let(:condition) { ->{ true } } 201 | 202 | specify do 203 | expect(subject).to be_valid 204 | end 205 | end 206 | 207 | context 'condition is false' do 208 | let(:condition) { ->{ false } } 209 | 210 | specify do 211 | expect(subject).to be_valid 212 | end 213 | end 214 | end 215 | 216 | context 'field value is not expected type' do 217 | let(:value) { -1 } 218 | 219 | context 'condition is true' do 220 | let(:condition) { ->{ true } } 221 | 222 | specify do 223 | expect(subject).to be_valid 224 | end 225 | end 226 | 227 | context 'condition is false' do 228 | let(:condition) { ->{ false } } 229 | 230 | specify do 231 | expect(subject).to_not be_valid 232 | end 233 | end 234 | end 235 | end 236 | 237 | describe 'on' do 238 | let(:value) { nil } 239 | subject do 240 | TypeValidationTest.send(validate_version, 241 | :string, on: :some_test_method) 242 | end 243 | 244 | before do 245 | allow(subject).to receive(:some_test_method) { subject.validate } 246 | end 247 | 248 | context 'on: criteria is met' do 249 | specify do 250 | expect(subject).to receive(:validate) 251 | subject.some_test_method 252 | end 253 | end 254 | 255 | context 'on: criteria is not met' do 256 | specify do 257 | expect(subject).to_not receive(:validate) 258 | subject.valid? 259 | end 260 | end 261 | end 262 | end 263 | 264 | context 'multiple custom modifiers' do 265 | before do 266 | subject.test_attribute = value 267 | end 268 | 269 | describe 'message: with if:' do 270 | subject do 271 | TypeValidationTest.send(validate_version, 272 | :string, message: 'This is bad!', if: condition) 273 | end 274 | 275 | context 'condition is true' do 276 | let(:condition) { -> { true } } 277 | 278 | context 'field value is the expected type' do 279 | let(:value) { 'I am a string' } 280 | 281 | specify do 282 | expect(subject).to be_valid 283 | end 284 | end 285 | 286 | context 'field value is not the expected type' do 287 | let(:value) { -1 } 288 | 289 | specify do 290 | expect(subject).to_not be_valid 291 | expect(subject.errors.messages[:test_attribute][0]).to match(/This is bad!/) 292 | end 293 | end 294 | end 295 | 296 | context 'condition is false' do 297 | let(:condition) { -> { false } } 298 | 299 | context 'field value is the expected type' do 300 | let(:value) { 'I am a string' } 301 | 302 | specify do 303 | expect(subject).to be_valid 304 | end 305 | end 306 | 307 | context 'field value is not the expected type' do 308 | let(:value) { -1 } 309 | 310 | specify do 311 | expect(subject).to be_valid 312 | end 313 | end 314 | end 315 | end 316 | end 317 | end 318 | end 319 | end 320 | -------------------------------------------------------------------------------- /spec/activerecord/types_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe 'ValidatesType' do 4 | context 'supported types' do 5 | before do 6 | subject.test_attribute = value 7 | end 8 | 9 | describe 'Array' do 10 | drop_and_create_column_with_type(:string) 11 | 12 | subject { TypeValidationTest.set_accessor_and_long_validator(:array) } 13 | 14 | context 'field value is an Array' do 15 | let(:value) { [] } 16 | 17 | specify do 18 | expect(subject).to be_valid 19 | end 20 | end 21 | 22 | context 'field value is not an Array' do 23 | let(:value) { -1 } 24 | 25 | specify do 26 | expect(subject).to_not be_valid 27 | end 28 | 29 | specify do 30 | subject.validate 31 | expect(subject.errors).to_not be_empty 32 | expect(subject.errors.messages[:test_attribute][0]).to match(/is expected to be a Array and is not/) 33 | end 34 | end 35 | 36 | context 'passing in a custom message' do 37 | subject { TypeValidationTest.set_accessor_and_long_validator(:array, message: 'is not an Array!') } 38 | 39 | context 'when validation fails' do 40 | let(:value) { 1 } 41 | 42 | specify do 43 | subject.validate 44 | expect(subject.errors.messages[:test_attribute][0]).to match(/is not an Array!/) 45 | end 46 | end 47 | end 48 | end 49 | 50 | describe 'Boolean' do 51 | drop_and_create_column_with_type(:boolean) 52 | 53 | subject { TypeValidationTest.set_accessor_and_long_validator(:boolean) } 54 | 55 | context 'field value is a Boolean' do 56 | let(:value) { true } 57 | 58 | specify do 59 | expect(subject).to be_valid 60 | end 61 | end 62 | 63 | context 'field value is not a Boolean' do 64 | let(:value) { 'false' } 65 | 66 | specify do 67 | expect(subject).to_not be_valid 68 | end 69 | 70 | specify do 71 | subject.validate 72 | expect(subject.errors).to_not be_empty 73 | expect(subject.errors.messages[:test_attribute][0]).to match(/is expected to be a Boolean and is not/) 74 | end 75 | end 76 | 77 | context 'passing in a custom message' do 78 | subject { TypeValidationTest.set_accessor_and_long_validator(:boolean, message: 'is not a Boolean!') } 79 | 80 | context 'when validation fails' do 81 | let(:value) { 1 } 82 | 83 | specify do 84 | subject.validate 85 | expect(subject.errors.messages[:test_attribute][0]).to match(/is not a Boolean!/) 86 | end 87 | end 88 | end 89 | end 90 | 91 | describe 'Float' do 92 | drop_and_create_column_with_type(:float) 93 | 94 | subject { TypeValidationTest.set_accessor_and_long_validator(:float) } 95 | 96 | context 'field value is a Float' do 97 | let(:value) { 1.0 } 98 | 99 | specify do 100 | expect(subject).to be_valid 101 | end 102 | end 103 | 104 | context 'field value is not a Float' do 105 | let(:value) { 'banana' } 106 | 107 | specify do 108 | expect(subject).to_not be_valid 109 | end 110 | 111 | specify do 112 | subject.validate 113 | expect(subject.errors).to_not be_empty 114 | expect(subject.errors.messages[:test_attribute][0]).to match(/is expected to be a Float and is not/) 115 | end 116 | end 117 | 118 | context 'passing in a custom message' do 119 | subject { TypeValidationTest.set_accessor_and_long_validator(:float, message: 'is not a Float!') } 120 | 121 | context 'when validation fails' do 122 | let(:value) { 1 } 123 | 124 | specify do 125 | subject.validate 126 | expect(subject.errors.messages[:test_attribute][0]).to match(/is not a Float!/) 127 | end 128 | end 129 | end 130 | end 131 | 132 | describe 'Hash' do 133 | drop_and_create_column_with_type(:string) 134 | 135 | subject { TypeValidationTest.set_accessor_and_long_validator(:hash) } 136 | 137 | context 'field value is a Hash' do 138 | let(:value) { { this: :here } } 139 | 140 | specify do 141 | expect(subject).to be_valid 142 | end 143 | end 144 | 145 | context 'field value is not a Hash' do 146 | let(:value) { 'banana' } 147 | 148 | specify do 149 | expect(subject).to_not be_valid 150 | end 151 | 152 | specify do 153 | subject.validate 154 | expect(subject.errors).to_not be_empty 155 | expect(subject.errors.messages[:test_attribute][0]).to match(/is expected to be a Hash and is not/) 156 | end 157 | end 158 | 159 | context 'passing in a custom message' do 160 | subject { TypeValidationTest.set_accessor_and_long_validator(:hash, message: 'is not a Hash!') } 161 | 162 | context 'when validation fails' do 163 | let(:value) { [] } 164 | 165 | specify do 166 | subject.validate 167 | expect(subject.errors.messages[:test_attribute][0]).to match(/is not a Hash!/) 168 | end 169 | end 170 | end 171 | end 172 | 173 | describe 'Integer' do 174 | drop_and_create_column_with_type(:integer) 175 | 176 | subject { TypeValidationTest.set_accessor_and_long_validator(:integer) } 177 | 178 | context 'field value is a Integer' do 179 | let(:value) { 1 } 180 | 181 | specify do 182 | expect(subject).to be_valid 183 | end 184 | end 185 | 186 | context 'field value is not a Integer' do 187 | let(:value) { 'banana' } 188 | 189 | specify do 190 | expect(subject).to_not be_valid 191 | end 192 | 193 | specify do 194 | subject.validate 195 | expect(subject.errors).to_not be_empty 196 | expect(subject.errors.messages[:test_attribute][0]).to match(/is expected to be a Integer and is not/) 197 | end 198 | end 199 | 200 | context 'passing in a custom message' do 201 | subject { TypeValidationTest.set_accessor_and_long_validator(:integer, message: 'is not a Integer!') } 202 | 203 | context 'when validation fails' do 204 | let(:value) { [] } 205 | 206 | specify do 207 | subject.validate 208 | expect(subject.errors.messages[:test_attribute][0]).to match(/is not a Integer!/) 209 | end 210 | end 211 | end 212 | end 213 | 214 | describe 'String' do 215 | drop_and_create_column_with_type(:string) 216 | 217 | subject { TypeValidationTest.set_accessor_and_long_validator(:string) } 218 | 219 | context 'field value is a String' do 220 | let(:value) { 'some string' } 221 | 222 | specify do 223 | expect(subject).to be_valid 224 | end 225 | end 226 | 227 | context 'field value is not a String' do 228 | let(:value) { -1 } 229 | specify do 230 | expect(subject).to_not be_valid 231 | end 232 | 233 | specify do 234 | subject.validate 235 | expect(subject.errors).to_not be_empty 236 | expect(subject.errors.messages[:test_attribute][0]).to match(/is expected to be a String and is not/) 237 | end 238 | end 239 | 240 | context 'passing in a custom message' do 241 | subject { TypeValidationTest.set_accessor_and_long_validator(:string, message: 'is not a String!') } 242 | 243 | context 'when validation fails' do 244 | let(:value) { 1 } 245 | 246 | specify do 247 | subject.validate 248 | expect(subject.errors.messages[:test_attribute][0]).to match(/is not a String!/) 249 | end 250 | end 251 | end 252 | end 253 | 254 | describe 'Symbol' do 255 | drop_and_create_column_with_type(:string) 256 | 257 | subject { TypeValidationTest.set_accessor_and_long_validator(:symbol) } 258 | 259 | context 'field value is a Symbol' do 260 | let(:value) { :something } 261 | 262 | specify do 263 | expect(subject).to be_valid 264 | end 265 | end 266 | 267 | context 'field value is not a Symbol' do 268 | let(:value) { -1 } 269 | specify do 270 | expect(subject).to_not be_valid 271 | end 272 | 273 | specify do 274 | subject.validate 275 | expect(subject.errors).to_not be_empty 276 | expect(subject.errors.messages[:test_attribute][0]).to match(/is expected to be a Symbol and is not/) 277 | end 278 | end 279 | 280 | context 'passing in a custom message' do 281 | subject { TypeValidationTest.set_accessor_and_long_validator(:symbol, message: 'is not a Symbol!') } 282 | 283 | context 'when validation fails' do 284 | let(:value) { 1 } 285 | 286 | specify do 287 | subject.validate 288 | expect(subject.errors.messages[:test_attribute][0]).to match(/is not a Symbol!/) 289 | end 290 | end 291 | end 292 | end 293 | 294 | describe 'Date' do 295 | drop_and_create_column_with_type(:string) 296 | 297 | subject { TypeValidationTest.set_accessor_and_long_validator(:date) } 298 | 299 | context 'field value is a Date' do 300 | let(:value) { Date.new } 301 | 302 | specify do 303 | expect(subject).to be_valid 304 | end 305 | end 306 | 307 | context 'field value is not a Date' do 308 | let(:value) { :foo } 309 | specify do 310 | expect(subject).to_not be_valid 311 | end 312 | 313 | specify do 314 | subject.validate 315 | expect(subject.errors).to_not be_empty 316 | expect(subject.errors.messages[:test_attribute][0]).to match(/is expected to be a Date and is not/) 317 | end 318 | end 319 | end 320 | 321 | describe 'Time' do 322 | drop_and_create_column_with_type(:string) 323 | 324 | subject { TypeValidationTest.set_accessor_and_long_validator(:time) } 325 | 326 | context 'field value is a Time' do 327 | let(:value) { Time.new } 328 | 329 | specify do 330 | expect(subject).to be_valid 331 | end 332 | end 333 | 334 | context 'field value is not a Time' do 335 | let(:value) { 123456 } 336 | specify do 337 | expect(subject).to_not be_valid 338 | end 339 | 340 | specify do 341 | subject.validate 342 | expect(subject.errors).to_not be_empty 343 | expect(subject.errors.messages[:test_attribute][0]).to match(/is expected to be a Time and is not/) 344 | end 345 | end 346 | end 347 | 348 | describe 'BigDecimal' do 349 | drop_and_create_column_with_type(:string) 350 | 351 | subject { TypeValidationTest.set_accessor_and_long_validator(:big_decimal) } 352 | 353 | context 'field value is a BigDecimal' do 354 | let(:value) { BigDecimal(1) } 355 | 356 | specify do 357 | expect(subject).to be_valid 358 | end 359 | end 360 | 361 | context 'field value is not a BigDecimal' do 362 | let(:value) { 123456 } 363 | specify do 364 | expect(subject).to_not be_valid 365 | end 366 | 367 | specify do 368 | subject.validate 369 | expect(subject.errors).to_not be_empty 370 | expect(subject.errors.messages[:test_attribute][0]).to match(/is expected to be a BigDecimal and is not/) 371 | end 372 | end 373 | end 374 | end 375 | 376 | context 'unsupported types' do 377 | describe 'Foo' do 378 | specify do 379 | expect do 380 | subject = TypeValidationTest.set_accessor_and_long_validator(:foo) 381 | subject.valid? 382 | end.to raise_error( 383 | ValidatesType::UnsupportedType, 384 | "Unsupported type Foo given for validates_type.") 385 | end 386 | end 387 | end 388 | 389 | context 'custom class' do 390 | drop_and_create_column_with_type(:string) 391 | class Custom; end 392 | subject { TypeValidationTest.set_accessor_and_long_validator(Custom) } 393 | 394 | before do 395 | subject.test_attribute = value 396 | end 397 | 398 | context 'field value is of type Custom' do 399 | let(:value) { Custom.new } 400 | 401 | specify do 402 | expect(subject).to be_valid 403 | end 404 | end 405 | 406 | context 'field value is not of type Custom' do 407 | let(:value) { -1 } 408 | 409 | specify do 410 | expect(subject).to_not be_valid 411 | end 412 | 413 | specify do 414 | subject.validate 415 | expect(subject.errors).to_not be_empty 416 | expect(subject.errors.messages[:test_attribute][0]).to match(/is expected to be a Custom and is not/) 417 | end 418 | end 419 | 420 | context 'passing in a custom message' do 421 | subject { TypeValidationTest.set_accessor_and_long_validator(Custom, message: 'is not Custom!') } 422 | 423 | context 'when validation fails' do 424 | let(:value) { 1 } 425 | 426 | specify do 427 | subject.validate 428 | expect(subject.errors.messages[:test_attribute][0]).to match(/is not Custom!/) 429 | end 430 | end 431 | end 432 | end 433 | end 434 | -------------------------------------------------------------------------------- /spec/arguments_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative '../lib/arguments' 3 | 4 | describe ValidatesType::Arguments do 5 | 6 | let(:attribute_name) { :foo } 7 | let(:attribute_type) { :string } 8 | let(:options) { { extra: :options } } 9 | 10 | subject { described_class.new(attribute_name, attribute_type, options) } 11 | 12 | describe '#to_validation_attributes' do 13 | context 'all pertinent attributes are present' do 14 | specify do 15 | expect(subject.to_validation_attributes).to eq([attribute_name, { type: attribute_type }.merge(options)]) 16 | end 17 | end 18 | 19 | context 'attribute_type is nil' do 20 | let(:attribute_type) { nil } 21 | 22 | specify do 23 | expect(subject.to_validation_attributes).to eq([attribute_name, { type: attribute_type }.merge(options)]) 24 | end 25 | end 26 | 27 | context 'attribute_name is nil' do 28 | let(:attribute_name) { nil } 29 | 30 | specify do 31 | expect(subject.to_validation_attributes).to eq([attribute_name, { type: attribute_type }.merge(options)]) 32 | end 33 | end 34 | 35 | context 'options is nil' do 36 | let(:options) { nil } 37 | 38 | specify do 39 | expect(subject.to_validation_attributes).to eq([attribute_name, { type: attribute_type }]) 40 | end 41 | end 42 | end 43 | 44 | describe '#type' do 45 | context 'type is given' do 46 | specify do 47 | expect(subject.send(:type)).to eq({ type: attribute_type }) 48 | end 49 | end 50 | 51 | context 'type is nil' do 52 | let(:attribute_type) { nil } 53 | 54 | specify do 55 | expect(subject.send(:type)).to eq({ type: nil }) 56 | end 57 | end 58 | end 59 | 60 | describe '#merged_options' do 61 | context 'options are present' do 62 | specify do 63 | expect(subject.send(:merged_options)).to eq({ type: attribute_type }.merge(options)) 64 | end 65 | end 66 | 67 | context 'options is nil' do 68 | let(:options) { nil } 69 | 70 | specify do 71 | expect(subject.send(:merged_options)).to eq({ type: attribute_type }) 72 | end 73 | end 74 | 75 | context 'options is a blank hash' do 76 | let(:options) { {} } 77 | 78 | specify do 79 | expect(subject.send(:merged_options)).to eq({ type: attribute_type }) 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require_relative '../lib/validates_type' 3 | require_relative './active_model_helper' 4 | require_relative './active_record_helper' 5 | 6 | 7 | -------------------------------------------------------------------------------- /validates_type.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib/', __FILE__) 2 | $:.unshift lib unless $:.include?(lib) 3 | require './version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'validates_type' 7 | s.version = ValidatesType::VERSION 8 | s.summary = %q{Type validation for ActiveModel/ActiveRecord attributes} 9 | s.description = %q{This library helps validate attributes to specific types in the same way that ActiveModel valiations work. Able to chain additional modifiers to each validation.} 10 | s.authors = ['Jake Yesbeck'] 11 | s.email = 'yesbeckjs@gmail.com' 12 | s.homepage = 'http://rubygems.org/gems/validates_type' 13 | s.license = 'MIT' 14 | 15 | s.require_paths = ['lib'] 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = s.files.grep(/^spec\//) 18 | 19 | s.required_ruby_version = '>= 3.1.0' 20 | 21 | s.add_dependency 'ruby-boolean', '~> 1.0' 22 | s.add_dependency 'activemodel', '~> 7.0' 23 | 24 | s.add_development_dependency 'bundler', '~> 2.0' 25 | s.add_development_dependency 'rake' 26 | s.add_development_dependency 'rspec', '3.4' 27 | s.add_development_dependency 'activerecord', '>= 3.0.0' 28 | s.add_development_dependency 'sqlite3', '~> 1.3' 29 | end 30 | -------------------------------------------------------------------------------- /version.rb: -------------------------------------------------------------------------------- 1 | module ValidatesType 2 | MAJOR = 4 3 | MINOR = 0 4 | TINY = 0 5 | 6 | VERSION = [MAJOR, MINOR, TINY].join('.').freeze 7 | end 8 | --------------------------------------------------------------------------------