validates_by_schema 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +22 -2
- data/Rakefile +7 -8
- data/lib/validates_by_schema/validation_option.rb +43 -5
- data/lib/validates_by_schema/version.rb +1 -1
- data/lib/validates_by_schema.rb +3 -0
- data/spec/ci/rails50.gemfile +10 -0
- data/spec/ci/rails50.gemfile.lock +118 -0
- data/spec/ci/rails52.gemfile +8 -0
- data/spec/ci/rails60.gemfile +8 -0
- data/spec/config/database.yml +31 -0
- data/spec/config/schema.rb +44 -0
- data/spec/db/test.sqlite3 +0 -0
- data/spec/spec_helper.rb +75 -0
- data/spec/support/models/contraption.rb +7 -0
- data/spec/support/models/gadget.rb +5 -0
- data/spec/support/models/gizmo.rb +7 -0
- data/spec/support/models/sub_contraption.rb +4 -0
- data/spec/support/models/widget.rb +6 -0
- data/spec/validations_spec.rb +340 -0
- metadata +51 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a44382b4c751d51c6056b9eae1daed6f8eedfe8040383fe38ce717beafc2c84d
|
4
|
+
data.tar.gz: 553ffe11b1a3c83f6c2f1126954d691553465492f7f3820f6530b14a48675971
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b404ea525a570681cc76d936755f6f6fbc89a310e8409d0480161a2a9aad11e1c803445c30b0cf03ce951d02ae63f614715e48c3a9bfde80455e5cec4b293c71
|
7
|
+
data.tar.gz: a7b24d0fe512674063b0c055f155b5cd608661fcf284634399354c6d63a3acfbf7fba6e9de335ea5e55e25765cedc8bda60505879aa7d9a09f96a3e828ebadba
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# Validates By Schema (validates_by_schema)
|
2
|
+
|
2
3
|
[![Gem Version](http://img.shields.io/gem/v/validates_by_schema.svg?style=flat)](https://rubygems.org/gems/validates_by_schema)
|
3
|
-
[![Build Status](http://img.shields.io/
|
4
|
+
[![Build Status](http://img.shields.io/github/workflow/status/joshwlewis/validates_by_schema/Build?style=flat)](https://github.com/joshwlewis/validates_by_schema/actions/workflows/build.yml)
|
4
5
|
[![Coverage Status](http://img.shields.io/coveralls/joshwlewis/validates_by_schema.svg?style=flat)](https://coveralls.io/r/joshwlewis/validates_by_schema)
|
5
6
|
[![Code Climate](http://img.shields.io/codeclimate/maintainability/joshwlewis/validates_by_schema.svg?style=flat)](https://codeclimate.com/github/joshwlewis/validates_by_schema)
|
6
7
|
|
7
|
-
|
8
8
|
Automatic validation based on your database schema column types and limits. Keep your code DRY by inferring column validations from table properties!
|
9
9
|
|
10
10
|
## Example
|
@@ -18,6 +18,7 @@ create_table "widgets", force: true do |t|
|
|
18
18
|
t.string "color", null: false
|
19
19
|
t.boolean "flagged", null: false, default: false
|
20
20
|
t.integer "other_id", null: false
|
21
|
+
t.index ["other_id", "color"], unique: true
|
21
22
|
end
|
22
23
|
```
|
23
24
|
|
@@ -35,6 +36,11 @@ validates :thickness, numericality: { allow_nil: true,
|
|
35
36
|
validates :color, presence: true, length: { allow_nil: true, maximum: 255 }
|
36
37
|
validates :flagged, inclusion: { in: [true, false], allow_nil: false }
|
37
38
|
validates :other, presence: true
|
39
|
+
validates :other_id,
|
40
|
+
uniqueness: {
|
41
|
+
scope: :color,
|
42
|
+
if: -> { |model| model.other_id_changed? || model.color_changed? }
|
43
|
+
}
|
38
44
|
```
|
39
45
|
|
40
46
|
## Installation
|
@@ -69,8 +75,22 @@ validates_by_schema except: [:name, :title]
|
|
69
75
|
|
70
76
|
The primary key and timestamp columns are not validated.
|
71
77
|
|
78
|
+
If you want to opt out of automatic uniqueness validations globally, add the following line to an initializer:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
ValidatesBySchema.validate_uniqueness = false
|
82
|
+
```
|
83
|
+
|
72
84
|
## Notes
|
73
85
|
|
74
86
|
Column properties are inferred by your database adapter (like pg, mysql2, sqlite3), and does not depend on migration files or schema.rb. As such, you could use this on projects where the database where Rails is not in control of the database configuration.
|
75
87
|
|
76
88
|
This has been tested with mysql, postgresql, and sqlite3. It should work with any other database that has reliable adapter.
|
89
|
+
|
90
|
+
## Contributing
|
91
|
+
|
92
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/joshwlewis/validates_by_schema.
|
93
|
+
|
94
|
+
## License
|
95
|
+
|
96
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -31,25 +31,24 @@ end
|
|
31
31
|
|
32
32
|
task default: :spec
|
33
33
|
|
34
|
-
|
35
34
|
namespace :db do
|
36
35
|
task :drop do
|
37
|
-
puts
|
36
|
+
puts 'dropping'
|
38
37
|
case ENV['DB']
|
39
38
|
when 'postgresql'
|
40
|
-
exec "psql -c 'drop database if exists validates_by_schema_test;' -U postgres"
|
39
|
+
exec "psql -c 'drop database if exists validates_by_schema_test;' -U postgres -h localhost"
|
41
40
|
when 'mysql'
|
42
|
-
exec "mysql -e 'drop database if exists validates_by_schema_test;'"
|
41
|
+
exec "mysql -e 'drop database if exists validates_by_schema_test;' -u root -h 127.0.0.1"
|
43
42
|
end
|
44
43
|
end
|
45
44
|
|
46
45
|
task :create do
|
47
|
-
puts
|
46
|
+
puts 'creating'
|
48
47
|
case ENV['DB']
|
49
48
|
when 'postgresql'
|
50
|
-
exec "psql -c 'create database validates_by_schema_test;' -U postgres"
|
49
|
+
exec "psql -c 'create database validates_by_schema_test;' -U postgres -h localhost"
|
51
50
|
when 'mysql'
|
52
|
-
exec "mysql -e 'create database validates_by_schema_test;'"
|
51
|
+
exec "mysql -e 'create database validates_by_schema_test;' -u root -h 127.0.0.1"
|
53
52
|
end
|
54
53
|
end
|
55
|
-
end
|
54
|
+
end
|
@@ -10,17 +10,31 @@ class ValidatesBySchema::ValidationOption
|
|
10
10
|
|
11
11
|
def define!
|
12
12
|
if association
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
# Only presence and uniqueness are handled for associations.
|
14
|
+
# presence on the association name, uniqueness on the column name.
|
15
|
+
define_belongs_to_presence_validation
|
16
16
|
else
|
17
|
-
|
18
|
-
klass.validates column.name, options if options.present?
|
17
|
+
define_validations(to_hash)
|
19
18
|
end
|
19
|
+
define_uniqueness_validations if ValidatesBySchema.validate_uniqueness
|
20
20
|
end
|
21
21
|
|
22
22
|
private
|
23
23
|
|
24
|
+
def define_belongs_to_presence_validation
|
25
|
+
klass.validates association.name, presence: true if !ActiveRecord::Base.belongs_to_required_by_default && presence
|
26
|
+
end
|
27
|
+
|
28
|
+
def define_uniqueness_validations
|
29
|
+
uniqueness.each do |options|
|
30
|
+
define_validations(uniqueness: options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def define_validations(options)
|
35
|
+
klass.validates column.name, options if options.present?
|
36
|
+
end
|
37
|
+
|
24
38
|
def presence?
|
25
39
|
presence && column.type != :boolean
|
26
40
|
end
|
@@ -53,6 +67,30 @@ class ValidatesBySchema::ValidationOption
|
|
53
67
|
numericality
|
54
68
|
end
|
55
69
|
|
70
|
+
def uniqueness
|
71
|
+
unique_indexes.map do |index|
|
72
|
+
{
|
73
|
+
scope: index.columns.reject { |col| col == column.name },
|
74
|
+
conditions: -> { where(index.where) },
|
75
|
+
allow_nil: column.null,
|
76
|
+
case_sensitive: case_sensitive?,
|
77
|
+
if: ->(model) { index.columns.any? { |c| model.send("#{c}_changed?") } }
|
78
|
+
}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def unique_indexes
|
83
|
+
klass
|
84
|
+
.connection
|
85
|
+
.indexes(klass.table_name)
|
86
|
+
.select { |index| index.unique && index.columns.first == column.name }
|
87
|
+
end
|
88
|
+
|
89
|
+
def case_sensitive?
|
90
|
+
!klass.connection.respond_to?(:collation) ||
|
91
|
+
!klass.connection.collation.end_with?('_ci')
|
92
|
+
end
|
93
|
+
|
56
94
|
def array?
|
57
95
|
column.respond_to?(:array) && column.array
|
58
96
|
end
|
data/lib/validates_by_schema.rb
CHANGED
@@ -0,0 +1,118 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ../..
|
3
|
+
specs:
|
4
|
+
validates_by_schema (0.4.0)
|
5
|
+
activerecord (>= 5.0.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actionpack (5.0.7.2)
|
11
|
+
actionview (= 5.0.7.2)
|
12
|
+
activesupport (= 5.0.7.2)
|
13
|
+
rack (~> 2.0)
|
14
|
+
rack-test (~> 0.6.3)
|
15
|
+
rails-dom-testing (~> 2.0)
|
16
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
17
|
+
actionview (5.0.7.2)
|
18
|
+
activesupport (= 5.0.7.2)
|
19
|
+
builder (~> 3.1)
|
20
|
+
erubis (~> 2.7.0)
|
21
|
+
rails-dom-testing (~> 2.0)
|
22
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
23
|
+
activemodel (5.0.7.2)
|
24
|
+
activesupport (= 5.0.7.2)
|
25
|
+
activerecord (5.0.7.2)
|
26
|
+
activemodel (= 5.0.7.2)
|
27
|
+
activesupport (= 5.0.7.2)
|
28
|
+
arel (~> 7.0)
|
29
|
+
activesupport (5.0.7.2)
|
30
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
31
|
+
i18n (>= 0.7, < 2)
|
32
|
+
minitest (~> 5.1)
|
33
|
+
tzinfo (~> 1.1)
|
34
|
+
arel (7.1.4)
|
35
|
+
builder (3.2.4)
|
36
|
+
concurrent-ruby (1.1.9)
|
37
|
+
crass (1.0.6)
|
38
|
+
diff-lcs (1.4.4)
|
39
|
+
docile (1.4.0)
|
40
|
+
erubis (2.7.0)
|
41
|
+
i18n (1.8.11)
|
42
|
+
concurrent-ruby (~> 1.0)
|
43
|
+
loofah (2.12.0)
|
44
|
+
crass (~> 1.0.2)
|
45
|
+
nokogiri (>= 1.5.9)
|
46
|
+
method_source (1.0.0)
|
47
|
+
mini_portile2 (2.6.1)
|
48
|
+
minitest (5.14.4)
|
49
|
+
mysql2 (0.5.3)
|
50
|
+
nokogiri (1.12.5)
|
51
|
+
mini_portile2 (~> 2.6.1)
|
52
|
+
racc (~> 1.4)
|
53
|
+
pg (1.2.3)
|
54
|
+
racc (1.6.0)
|
55
|
+
rack (2.2.3)
|
56
|
+
rack-test (0.6.3)
|
57
|
+
rack (>= 1.0)
|
58
|
+
rails-dom-testing (2.0.3)
|
59
|
+
activesupport (>= 4.2.0)
|
60
|
+
nokogiri (>= 1.6)
|
61
|
+
rails-html-sanitizer (1.4.2)
|
62
|
+
loofah (~> 2.3)
|
63
|
+
railties (5.0.7.2)
|
64
|
+
actionpack (= 5.0.7.2)
|
65
|
+
activesupport (= 5.0.7.2)
|
66
|
+
method_source
|
67
|
+
rake (>= 0.8.7)
|
68
|
+
thor (>= 0.18.1, < 2.0)
|
69
|
+
rake (13.0.6)
|
70
|
+
rspec-core (3.10.1)
|
71
|
+
rspec-support (~> 3.10.0)
|
72
|
+
rspec-expectations (3.10.1)
|
73
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
74
|
+
rspec-support (~> 3.10.0)
|
75
|
+
rspec-mocks (3.10.2)
|
76
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
77
|
+
rspec-support (~> 3.10.0)
|
78
|
+
rspec-rails (4.1.2)
|
79
|
+
actionpack (>= 4.2)
|
80
|
+
activesupport (>= 4.2)
|
81
|
+
railties (>= 4.2)
|
82
|
+
rspec-core (~> 3.10)
|
83
|
+
rspec-expectations (~> 3.10)
|
84
|
+
rspec-mocks (~> 3.10)
|
85
|
+
rspec-support (~> 3.10)
|
86
|
+
rspec-support (3.10.3)
|
87
|
+
shoulda-matchers (4.5.1)
|
88
|
+
activesupport (>= 4.2.0)
|
89
|
+
simplecov (0.21.2)
|
90
|
+
docile (~> 1.1)
|
91
|
+
simplecov-html (~> 0.11)
|
92
|
+
simplecov_json_formatter (~> 0.1)
|
93
|
+
simplecov-html (0.12.3)
|
94
|
+
simplecov-lcov (0.8.0)
|
95
|
+
simplecov_json_formatter (0.1.3)
|
96
|
+
sqlite3 (1.3.13)
|
97
|
+
thor (1.1.0)
|
98
|
+
thread_safe (0.3.6)
|
99
|
+
tzinfo (1.2.9)
|
100
|
+
thread_safe (~> 0.1)
|
101
|
+
|
102
|
+
PLATFORMS
|
103
|
+
ruby
|
104
|
+
|
105
|
+
DEPENDENCIES
|
106
|
+
activerecord (~> 5.0.0)
|
107
|
+
mysql2
|
108
|
+
pg
|
109
|
+
rake
|
110
|
+
rspec-rails
|
111
|
+
shoulda-matchers
|
112
|
+
simplecov
|
113
|
+
simplecov-lcov
|
114
|
+
sqlite3 (~> 1.3.7)
|
115
|
+
validates_by_schema!
|
116
|
+
|
117
|
+
BUNDLED WITH
|
118
|
+
2.1.4
|
@@ -0,0 +1,31 @@
|
|
1
|
+
sqlite: &sqlite
|
2
|
+
adapter: sqlite3
|
3
|
+
database: spec/db/<%= ENV["RAILS_ENV"] %>.sqlite3
|
4
|
+
|
5
|
+
mysql: &mysql
|
6
|
+
adapter: mysql2
|
7
|
+
username: root
|
8
|
+
password:
|
9
|
+
database: validates_by_schema_<%= ENV["RAILS_ENV"] %>
|
10
|
+
|
11
|
+
postgresql: &postgresql
|
12
|
+
adapter: postgresql
|
13
|
+
username: postgres
|
14
|
+
password: postgres
|
15
|
+
database: validates_by_schema_<%= ENV["RAILS_ENV"] %>
|
16
|
+
min_messages: ERROR
|
17
|
+
|
18
|
+
defaults: &defaults
|
19
|
+
pool: 5
|
20
|
+
timeout: 5000
|
21
|
+
host: '127.0.0.1'
|
22
|
+
<<: *<%= ENV['DB'] || "sqlite" %>
|
23
|
+
|
24
|
+
development:
|
25
|
+
<<: *defaults
|
26
|
+
|
27
|
+
test:
|
28
|
+
<<: *defaults
|
29
|
+
|
30
|
+
production:
|
31
|
+
<<: *defaults
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# This file is auto-generated from the current state of the database. Instead
|
3
|
+
# of editing this file, please use the migrations feature of Active Record to
|
4
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
5
|
+
#
|
6
|
+
# Note that this schema.rb definition is the authoritative source for your
|
7
|
+
# database schema. If you need to create the application database on another
|
8
|
+
# system, you should be using db:schema:load, not running all the migrations
|
9
|
+
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
10
|
+
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
11
|
+
#
|
12
|
+
# It's strongly recommended to check this file into your version control system.
|
13
|
+
|
14
|
+
ActiveRecord::Schema.define(version: 20_121_210_034_140) do
|
15
|
+
create_table 'widgets', force: true do |t|
|
16
|
+
t.string 'name', limit: 50
|
17
|
+
t.string 'model', null: false
|
18
|
+
t.text 'description'
|
19
|
+
t.integer 'wheels', null: false
|
20
|
+
t.integer 'doors', limit: 2
|
21
|
+
t.decimal 'price', precision: 6, scale: 2
|
22
|
+
t.decimal 'cost', precision: 4, scale: 2, null: false
|
23
|
+
t.decimal 'completion'
|
24
|
+
t.float 'rating'
|
25
|
+
t.float 'score', null: false
|
26
|
+
t.datetime 'published_at'
|
27
|
+
t.date 'invented_on'
|
28
|
+
t.time 'startup_time'
|
29
|
+
t.time 'shutdown_time', null: false
|
30
|
+
t.boolean 'enabled'
|
31
|
+
t.binary 'data'
|
32
|
+
t.integer 'parent_id', null: false
|
33
|
+
t.integer 'other_id'
|
34
|
+
t.integer 'kind', null: false
|
35
|
+
t.string 'list', array: true, limit: 3
|
36
|
+
|
37
|
+
t.index 'parent_id'
|
38
|
+
t.index ['name', 'wheels'], unique: true
|
39
|
+
t.index 'other_id', unique: true
|
40
|
+
t.index ['doors'], unique: true, where: 'enabled = true'
|
41
|
+
t.index ['model', 'price'], unique: true
|
42
|
+
t.index ['model', 'cost'], unique: true
|
43
|
+
end
|
44
|
+
end
|
Binary file
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
ENV['RAILS_ENV'] ||= 'test'
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'active_record'
|
5
|
+
require 'shoulda-matchers'
|
6
|
+
require 'simplecov'
|
7
|
+
|
8
|
+
SimpleCov.start do
|
9
|
+
if ENV['CI']
|
10
|
+
require 'simplecov-lcov'
|
11
|
+
|
12
|
+
SimpleCov::Formatter::LcovFormatter.config do |c|
|
13
|
+
c.report_with_single_file = true
|
14
|
+
c.single_report_path = 'coverage/lcov.info'
|
15
|
+
end
|
16
|
+
|
17
|
+
formatter SimpleCov::Formatter::LcovFormatter
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Load up our code
|
22
|
+
require 'validates_by_schema'
|
23
|
+
|
24
|
+
# Setup the database
|
25
|
+
conf = YAML.load(ERB.new(File.read(File.join(File.dirname(__FILE__), 'config', 'database.yml'))).result)
|
26
|
+
ActiveRecord::Base.establish_connection(conf['test'])
|
27
|
+
|
28
|
+
ActiveRecord::Base.belongs_to_required_by_default = false
|
29
|
+
|
30
|
+
load(File.join(File.dirname(__FILE__), 'config', 'schema.rb'))
|
31
|
+
|
32
|
+
# Add support test models to the load path.
|
33
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'support', 'models'))
|
34
|
+
|
35
|
+
# Require all support files
|
36
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
37
|
+
|
38
|
+
RSpec.configure do |config|
|
39
|
+
config.around do |example|
|
40
|
+
ActiveRecord::Base.transaction do
|
41
|
+
example.run
|
42
|
+
raise ActiveRecord::Rollback
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Run specs in random order to surface order dependencies. If you find an
|
47
|
+
# order dependency and want to debug it, you can fix the order by providing
|
48
|
+
# the seed, which is printed after each run.
|
49
|
+
# --seed 1234
|
50
|
+
config.order = 'random'
|
51
|
+
|
52
|
+
config.include(Shoulda::Matchers::ActiveModel)
|
53
|
+
config.include(Shoulda::Matchers::ActiveRecord)
|
54
|
+
end
|
55
|
+
|
56
|
+
Shoulda::Matchers.configure do |config|
|
57
|
+
config.integrate do |with|
|
58
|
+
with.test_framework :rspec
|
59
|
+
with.library :active_record
|
60
|
+
with.library :active_model
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Shoulda
|
65
|
+
module Matchers
|
66
|
+
module ActiveModel
|
67
|
+
class ValidatePresenceOfMatcher < ValidationMatcher
|
68
|
+
# monkey patch to use `include?` instead of `in?`
|
69
|
+
def collection_association?
|
70
|
+
association? && %i[has_many has_and_belongs_to_many].include?(association_reflection.macro)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,340 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'validates by schema' do
|
4
|
+
let(:attributes) do
|
5
|
+
{
|
6
|
+
name: 'Secret',
|
7
|
+
model: 'secret-42',
|
8
|
+
description: 'Life, the Universe, Everything',
|
9
|
+
wheels: 4,
|
10
|
+
doors: 2,
|
11
|
+
price: 4242.42,
|
12
|
+
cost: 42.42,
|
13
|
+
completion: 0.424,
|
14
|
+
rating: 42.4242,
|
15
|
+
score: 4242.42,
|
16
|
+
published_at: Time.now.to_datetime,
|
17
|
+
invented_on: Time.now.to_date,
|
18
|
+
startup_time: 1.hour.ago,
|
19
|
+
shutdown_time: 4.hours.from_now,
|
20
|
+
enabled: true,
|
21
|
+
data: 'the question'.unpack('b*').to_s,
|
22
|
+
parent: Widget.new,
|
23
|
+
kind: 'one',
|
24
|
+
list: ['abc']
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'plain' do
|
29
|
+
subject { Contraption.new attributes }
|
30
|
+
|
31
|
+
context 'validates columns of type' do
|
32
|
+
context :string do
|
33
|
+
it { should_not validate_presence_of(:name) }
|
34
|
+
it { should validate_length_of(:name).is_at_most(50) }
|
35
|
+
it { should validate_presence_of(:model) }
|
36
|
+
if ENV['DB'] == 'mysql'
|
37
|
+
it { should validate_length_of(:model).is_at_most(255) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context :text do
|
42
|
+
it { should_not validate_presence_of(:description) }
|
43
|
+
it { should_not validate_length_of(:description).is_at_most(10_000_000) }
|
44
|
+
end
|
45
|
+
|
46
|
+
context :primary_key do
|
47
|
+
it { should_not validate_presence_of(:id) }
|
48
|
+
end
|
49
|
+
|
50
|
+
context :integer do
|
51
|
+
it { should validate_presence_of(:wheels) }
|
52
|
+
it { should validate_numericality_of(:wheels).only_integer }
|
53
|
+
it { should allow_value(242_424).for(:wheels) }
|
54
|
+
it { should allow_value(-242_424).for(:wheels) }
|
55
|
+
if ENV['DB'] != 'mysql'
|
56
|
+
it { should allow_value(2_147_483_647).for(:wheels) }
|
57
|
+
it { should allow_value(-2_147_483_647).for(:wheels) }
|
58
|
+
if ENV['DB'] != 'postgresql' && ActiveRecord.version.to_s >= '5.1'
|
59
|
+
it { should allow_value(10**100).for(:wheels) }
|
60
|
+
it { should allow_value(-10**100).for(:wheels) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it { should_not validate_presence_of(:doors) }
|
65
|
+
it { should validate_numericality_of(:doors).only_integer }
|
66
|
+
it { should allow_value(32767).for(:doors) }
|
67
|
+
it { should allow_value(-32767).for(:doors) }
|
68
|
+
it { should_not allow_value(32768).for(:doors) }
|
69
|
+
it { should_not allow_value(-32768).for(:doors) }
|
70
|
+
end
|
71
|
+
|
72
|
+
context :decimal do
|
73
|
+
it { should_not validate_presence_of(:price) }
|
74
|
+
it { should validate_numericality_of(:price) }
|
75
|
+
it { should allow_value(-9_999.99).for(:price) }
|
76
|
+
it { should allow_value(9_999.99).for(:price) }
|
77
|
+
it { should_not allow_value(10_000).for(:price) }
|
78
|
+
it { should_not allow_value(-10_000).for(:price) }
|
79
|
+
|
80
|
+
it { should validate_presence_of(:cost) }
|
81
|
+
it { should validate_numericality_of(:cost) }
|
82
|
+
it { should allow_value(99.99).for(:cost) }
|
83
|
+
it { should allow_value(-99.99).for(:cost) }
|
84
|
+
it { should_not allow_value(100).for(:cost) }
|
85
|
+
it { should_not allow_value(-100).for(:cost) }
|
86
|
+
end
|
87
|
+
|
88
|
+
context :float do
|
89
|
+
it { should_not validate_presence_of(:rating) }
|
90
|
+
it { should validate_numericality_of(:rating) }
|
91
|
+
it { should allow_value(242_424.242424).for(:rating) }
|
92
|
+
it { should allow_value(-5).for(:rating) }
|
93
|
+
|
94
|
+
it { should validate_presence_of(:score) }
|
95
|
+
it { should validate_numericality_of(:score) }
|
96
|
+
it { should allow_value(242_424.242424).for(:score) }
|
97
|
+
it { should allow_value(-5).for(:score) }
|
98
|
+
end
|
99
|
+
|
100
|
+
context :datetime do
|
101
|
+
it { should_not validate_presence_of(:published_at) }
|
102
|
+
end
|
103
|
+
|
104
|
+
context :date do
|
105
|
+
it { should_not validate_presence_of(:invented_on) }
|
106
|
+
end
|
107
|
+
|
108
|
+
context :time do
|
109
|
+
it { should_not validate_presence_of(:startup_time) }
|
110
|
+
it { should validate_presence_of(:shutdown_time) }
|
111
|
+
end
|
112
|
+
|
113
|
+
context :belongs_to do
|
114
|
+
if !ActiveRecord::Base.belongs_to_required_by_default
|
115
|
+
# belongs_to_required_by_default produces message 'must exist' instead of 'can't be blank'
|
116
|
+
it { should validate_presence_of(:parent) }
|
117
|
+
end
|
118
|
+
|
119
|
+
it { should allow_value(Widget.new).for(:parent) }
|
120
|
+
it { should_not allow_value(nil).for(:parent) }
|
121
|
+
end
|
122
|
+
|
123
|
+
context :enum do
|
124
|
+
it { should validate_presence_of(:kind) }
|
125
|
+
it { should allow_value('other').for(:kind) }
|
126
|
+
end
|
127
|
+
|
128
|
+
if ENV['DB'] == 'postgresql'
|
129
|
+
context :array do
|
130
|
+
it { should allow_value(['abc', 'def', 'ghi', 'jkl']).for(:list) }
|
131
|
+
# this value will be rejected by the db, but the validation would require a custom
|
132
|
+
# validator.
|
133
|
+
it { should allow_value(['abcdef']).for(:list) }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'validates uniqueness' do
|
139
|
+
let(:valid_attributes) do
|
140
|
+
attrs = attributes.dup
|
141
|
+
|
142
|
+
attrs[:list] = nil
|
143
|
+
attrs[:parent_id] = 23
|
144
|
+
attrs[:other_id] = 42
|
145
|
+
attrs[:wheels] = 3
|
146
|
+
attrs[:name] = 'Geheimnis'
|
147
|
+
attrs[:model] = 'secret-23'
|
148
|
+
attrs[:cost] = 9.99
|
149
|
+
attrs[:price] = 20.01
|
150
|
+
|
151
|
+
attrs
|
152
|
+
end
|
153
|
+
let(:existing_widget) { Widget.new(valid_attributes) }
|
154
|
+
before do
|
155
|
+
subject.list = nil
|
156
|
+
existing_widget.save!
|
157
|
+
end
|
158
|
+
|
159
|
+
context :simple_non_unique_index do
|
160
|
+
context 'has assumptions' do
|
161
|
+
it { expect(existing_widget.parent_id).to eq 23 }
|
162
|
+
end
|
163
|
+
|
164
|
+
it { should_not validate_uniqueness_of(:parent_id) }
|
165
|
+
it { should allow_value(23).for(:parent_id) }
|
166
|
+
end
|
167
|
+
|
168
|
+
context :simple_unique_index do
|
169
|
+
context 'has assumptions' do
|
170
|
+
it { expect(existing_widget.other_id).to eq 42 }
|
171
|
+
end
|
172
|
+
|
173
|
+
it { should validate_uniqueness_of(:other_id) }
|
174
|
+
it { should_not allow_value(42).for(:other_id) }
|
175
|
+
it { should allow_value(43).for(:other_id) }
|
176
|
+
end
|
177
|
+
|
178
|
+
context :multi_column_unique_index do
|
179
|
+
before { existing_widget.update!(wheels: 4) }
|
180
|
+
|
181
|
+
context 'has assumptions' do
|
182
|
+
it { expect(existing_widget.name).to eq 'Geheimnis' }
|
183
|
+
it { expect(existing_widget.wheels).to eq 4 }
|
184
|
+
it { expect(subject.wheels).to eq 4 }
|
185
|
+
end
|
186
|
+
|
187
|
+
it { should validate_uniqueness_of(:name).scoped_to(:wheels).ignoring_case_sensitivity }
|
188
|
+
it { should_not allow_value('Geheimnis').for(:name) }
|
189
|
+
it do
|
190
|
+
subject.wheels = 3
|
191
|
+
should allow_value('Geheimnis').for(:name)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context :multiple_multi_column_unique_index do
|
196
|
+
before { existing_widget.update!(cost: 10.0, price: 20.0) }
|
197
|
+
|
198
|
+
context 'has assumptions' do
|
199
|
+
it { expect(existing_widget.model).to eq 'secret-23' }
|
200
|
+
it { expect(existing_widget.cost).to eq 10.0 }
|
201
|
+
it { expect(existing_widget.price).to eq 20.0 }
|
202
|
+
it { expect(subject.cost).to eq 42.42 }
|
203
|
+
it { expect(subject.price).to eq 4242.42 }
|
204
|
+
end
|
205
|
+
|
206
|
+
it { should allow_value('secret-23').for(:model) }
|
207
|
+
it do
|
208
|
+
subject.cost = 10.0
|
209
|
+
should_not allow_value('secret-23').for(:model)
|
210
|
+
end
|
211
|
+
it do
|
212
|
+
subject.cost = 10.0
|
213
|
+
subject.price = 20.0
|
214
|
+
should_not allow_value('secret-23').for(:model)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
if ENV['DB'] == 'postgresql'
|
219
|
+
context :partial_unique_index do
|
220
|
+
context 'has assumptions' do
|
221
|
+
it { expect(existing_widget.doors).to eq 2 }
|
222
|
+
it { expect(existing_widget).to be_enabled }
|
223
|
+
it { should be_enabled }
|
224
|
+
end
|
225
|
+
|
226
|
+
it { should validate_uniqueness_of(:doors) }
|
227
|
+
it { should_not allow_value(2).for(:doors) }
|
228
|
+
it do
|
229
|
+
existing_widget.update(enabled: false)
|
230
|
+
should allow_value(2).for(:doors)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
context 'with except' do
|
238
|
+
subject { Gadget.new attributes }
|
239
|
+
|
240
|
+
context 'does not validate passed columns' do
|
241
|
+
context :string do
|
242
|
+
it { should_not validate_presence_of(:name) }
|
243
|
+
it { should validate_length_of(:name).is_at_most(50) }
|
244
|
+
end
|
245
|
+
|
246
|
+
context :integer do
|
247
|
+
it { should_not validate_presence_of(:wheels) }
|
248
|
+
it { should_not validate_numericality_of(:wheels) }
|
249
|
+
it { should allow_value(242_424).for(:wheels) }
|
250
|
+
it { should allow_value(-42_424).for(:wheels) }
|
251
|
+
if ENV['DB'] == 'postgresql' && ActiveRecord.version.to_s >= '5.1'
|
252
|
+
# Indexed int colums produce ActiveModel::RangeError in Rails 5
|
253
|
+
it { should allow_value(10**100).for(:wheels) }
|
254
|
+
it { should allow_value(-10**100).for(:wheels) }
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
context :decimal do
|
259
|
+
it { should_not validate_presence_of(:price) }
|
260
|
+
it { should validate_numericality_of(:price) }
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
context 'with only' do
|
266
|
+
subject { Gizmo.new attributes }
|
267
|
+
|
268
|
+
context 'validate just passed columns' do
|
269
|
+
context :string do
|
270
|
+
it { should_not validate_presence_of(:name) }
|
271
|
+
it { should validate_length_of(:name).is_at_most(50) }
|
272
|
+
end
|
273
|
+
|
274
|
+
context :integer do
|
275
|
+
it { should validate_presence_of(:wheels) }
|
276
|
+
it { should validate_numericality_of(:wheels).only_integer }
|
277
|
+
it { should allow_value(242_424).for(:wheels) }
|
278
|
+
it { should allow_value(-42_424).for(:wheels) }
|
279
|
+
if ENV['DB'] != 'mysql'
|
280
|
+
it { should allow_value(2_147_483_647).for(:wheels) }
|
281
|
+
it { should allow_value(-2_147_483_647).for(:wheels) }
|
282
|
+
if ENV['DB'] != 'postgresql' && ActiveRecord.version.to_s >= '5.1'
|
283
|
+
it { should allow_value(10**100).for(:wheels) }
|
284
|
+
it { should allow_value(-10**100).for(:wheels) }
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
context :decimal do
|
290
|
+
it { should_not validate_presence_of(:price) }
|
291
|
+
it { should_not validate_numericality_of(:price) }
|
292
|
+
it { should allow_value('1000000').for(:price) }
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
context 'subclass' do
|
298
|
+
subject { SubContraption.new attributes }
|
299
|
+
|
300
|
+
context 'performs validations as well' do
|
301
|
+
context :string do
|
302
|
+
it { should_not validate_presence_of(:name) }
|
303
|
+
it { should validate_length_of(:name).is_at_most(50) }
|
304
|
+
it { should validate_presence_of(:model) }
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
context 'multiple threads' do
|
310
|
+
subject { Contraption.new attributes }
|
311
|
+
|
312
|
+
it 'defines validations only once' do
|
313
|
+
subject.list = nil
|
314
|
+
subject.model = nil
|
315
|
+
5.times do
|
316
|
+
Thread.new do
|
317
|
+
Contraption.new.valid?
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
expect(subject).not_to be_valid
|
322
|
+
expect(subject.errors.full_messages).to eq(["Model can't be blank"])
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
context 'reset_column_information' do
|
327
|
+
subject { Contraption.new attributes }
|
328
|
+
|
329
|
+
it 'does not duplicate validations' do
|
330
|
+
subject.list = nil
|
331
|
+
expect(subject).to be_valid
|
332
|
+
|
333
|
+
Contraption.reset_column_information
|
334
|
+
|
335
|
+
subject.model = nil
|
336
|
+
expect(subject).not_to be_valid
|
337
|
+
expect(subject.errors.full_messages).to eq(["Model can't be blank"])
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: validates_by_schema
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Lewis
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2021-11-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -26,7 +26,7 @@ dependencies:
|
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: 5.0.0
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
|
-
name:
|
29
|
+
name: mysql2
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
32
|
- - ">="
|
@@ -40,7 +40,7 @@ dependencies:
|
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: '0'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
|
-
name:
|
43
|
+
name: pg
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
45
45
|
requirements:
|
46
46
|
- - ">="
|
@@ -54,21 +54,21 @@ dependencies:
|
|
54
54
|
- !ruby/object:Gem::Version
|
55
55
|
version: '0'
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
|
-
name:
|
57
|
+
name: rake
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
|
-
- - "
|
60
|
+
- - ">="
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
62
|
+
version: '0'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
|
-
- - "
|
67
|
+
- - ">="
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '
|
69
|
+
version: '0'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
|
-
name:
|
71
|
+
name: rspec-rails
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
73
73
|
requirements:
|
74
74
|
- - ">="
|
@@ -82,7 +82,7 @@ dependencies:
|
|
82
82
|
- !ruby/object:Gem::Version
|
83
83
|
version: '0'
|
84
84
|
- !ruby/object:Gem::Dependency
|
85
|
-
name:
|
85
|
+
name: shoulda-matchers
|
86
86
|
requirement: !ruby/object:Gem::Requirement
|
87
87
|
requirements:
|
88
88
|
- - ">="
|
@@ -96,7 +96,7 @@ dependencies:
|
|
96
96
|
- !ruby/object:Gem::Version
|
97
97
|
version: '0'
|
98
98
|
- !ruby/object:Gem::Dependency
|
99
|
-
name:
|
99
|
+
name: sqlite3
|
100
100
|
requirement: !ruby/object:Gem::Requirement
|
101
101
|
requirements:
|
102
102
|
- - ">="
|
@@ -110,8 +110,8 @@ dependencies:
|
|
110
110
|
- !ruby/object:Gem::Version
|
111
111
|
version: '0'
|
112
112
|
description: Keep your code DRY by inferring column validations from table properties!
|
113
|
-
Automagically validate presence, length, numericality,
|
114
|
-
backed columns.
|
113
|
+
Automagically validate presence, length, numericality, inclusion and uniqueness
|
114
|
+
of ActiveRecord backed columns.
|
115
115
|
email:
|
116
116
|
- josh.w.lewis@gmail.com
|
117
117
|
- pascal@codez.ch
|
@@ -125,9 +125,27 @@ files:
|
|
125
125
|
- lib/validates_by_schema.rb
|
126
126
|
- lib/validates_by_schema/validation_option.rb
|
127
127
|
- lib/validates_by_schema/version.rb
|
128
|
-
|
129
|
-
|
130
|
-
|
128
|
+
- spec/ci/rails50.gemfile
|
129
|
+
- spec/ci/rails50.gemfile.lock
|
130
|
+
- spec/ci/rails52.gemfile
|
131
|
+
- spec/ci/rails60.gemfile
|
132
|
+
- spec/config/database.yml
|
133
|
+
- spec/config/schema.rb
|
134
|
+
- spec/db/test.sqlite3
|
135
|
+
- spec/spec_helper.rb
|
136
|
+
- spec/support/models/contraption.rb
|
137
|
+
- spec/support/models/gadget.rb
|
138
|
+
- spec/support/models/gizmo.rb
|
139
|
+
- spec/support/models/sub_contraption.rb
|
140
|
+
- spec/support/models/widget.rb
|
141
|
+
- spec/validations_spec.rb
|
142
|
+
homepage: https://github.com/joshwlewis/validates_by_schema
|
143
|
+
licenses:
|
144
|
+
- MIT
|
145
|
+
metadata:
|
146
|
+
homepage_uri: https://github.com/joshwlewis/validates_by_schema
|
147
|
+
source_code_uri: https://github.com/joshwlewis/validates_by_schema
|
148
|
+
changelog_uri: https://github.com/joshwlewis/validates_by_schema/blob/master/CHANGELOG.md
|
131
149
|
post_install_message:
|
132
150
|
rdoc_options: []
|
133
151
|
require_paths:
|
@@ -143,9 +161,22 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
143
161
|
- !ruby/object:Gem::Version
|
144
162
|
version: '0'
|
145
163
|
requirements: []
|
146
|
-
|
147
|
-
rubygems_version: 2.7.8
|
164
|
+
rubygems_version: 3.1.4
|
148
165
|
signing_key:
|
149
166
|
specification_version: 4
|
150
167
|
summary: Automatic validation based on your database schema column types and limits.
|
151
|
-
test_files:
|
168
|
+
test_files:
|
169
|
+
- spec/spec_helper.rb
|
170
|
+
- spec/db/test.sqlite3
|
171
|
+
- spec/validations_spec.rb
|
172
|
+
- spec/support/models/widget.rb
|
173
|
+
- spec/support/models/contraption.rb
|
174
|
+
- spec/support/models/sub_contraption.rb
|
175
|
+
- spec/support/models/gadget.rb
|
176
|
+
- spec/support/models/gizmo.rb
|
177
|
+
- spec/config/schema.rb
|
178
|
+
- spec/config/database.yml
|
179
|
+
- spec/ci/rails52.gemfile
|
180
|
+
- spec/ci/rails50.gemfile
|
181
|
+
- spec/ci/rails60.gemfile
|
182
|
+
- spec/ci/rails50.gemfile.lock
|