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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95a3e699ef49bdc2b00988cce281ee448bfc05020b5ce9388889a928ae74fe57
4
- data.tar.gz: 544048310b55c334b2e287feb99e48f17258d3e9082137dfa9d5b633c8b4afa6
3
+ metadata.gz: a44382b4c751d51c6056b9eae1daed6f8eedfe8040383fe38ce717beafc2c84d
4
+ data.tar.gz: 553ffe11b1a3c83f6c2f1126954d691553465492f7f3820f6530b14a48675971
5
5
  SHA512:
6
- metadata.gz: 024cbf1790625db028dc7d351ff8d20797e762f31c2d91651da61ee6b533e73999d67aa871a3649db94140dc9ae86ceee3d09b311cf9a324c5cd075ce19a38ac
7
- data.tar.gz: 8f4c69c1a0b59d300c7553f92f6056db6d6ffa50fd1b46f6bd65b0a7cd8e0b8baaaac0bc58fcdb0f3498613b87c0544397963f6107310e2aa5665af2505fd77a
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/travis/joshwlewis/validates_by_schema.svg?style=flat)](https://travis-ci.org/joshwlewis/validates_by_schema)
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 "dropping"
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 "creating"
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
- if !ActiveRecord::Base.belongs_to_required_by_default && !column.null
14
- klass.validates association.name, presence: true
15
- end
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
- options = to_hash
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
@@ -1,3 +1,3 @@
1
1
  module ValidatesBySchema
2
- VERSION = '0.4.0'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -6,6 +6,9 @@ module ValidatesBySchema
6
6
 
7
7
  extend ActiveSupport::Concern
8
8
 
9
+ mattr_accessor :validate_uniqueness
10
+ self.validate_uniqueness = true
11
+
9
12
  module ClassMethods
10
13
 
11
14
  def validates_by_schema(options = {})
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '../..'
4
+
5
+ gem 'simplecov', require: false
6
+ gem 'simplecov-lcov', require: false
7
+
8
+ gem 'activerecord', '~> 5.0.0'
9
+
10
+ gem 'sqlite3', '~> 1.3.7'
@@ -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,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '../..'
4
+
5
+ gem 'simplecov', require: false
6
+ gem 'simplecov-lcov', require: false
7
+
8
+ gem 'activerecord', '~> 5.2.0'
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '../..'
4
+
5
+ gem 'simplecov', require: false
6
+ gem 'simplecov-lcov', require: false
7
+
8
+ gem 'activerecord', '~> 6.0.0'
@@ -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
@@ -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,7 @@
1
+ require 'widget'
2
+
3
+ class Contraption < Widget
4
+ enum kind: %w(one other)
5
+
6
+ validates_by_schema
7
+ end
@@ -0,0 +1,5 @@
1
+ require 'widget'
2
+
3
+ class Gadget < Widget
4
+ validates_by_schema except: :wheels
5
+ end
@@ -0,0 +1,7 @@
1
+ require 'widget'
2
+
3
+ class Gizmo < Widget
4
+ load_schema # who knows, probably loaded by somebody else before validates_by_schema
5
+
6
+ validates_by_schema only: [:name, :wheels, :cost]
7
+ end
@@ -0,0 +1,4 @@
1
+ require 'contraption'
2
+
3
+ class SubContraption < Contraption
4
+ end
@@ -0,0 +1,6 @@
1
+ class Widget < ActiveRecord::Base
2
+
3
+ belongs_to :parent, class_name: 'Widget'
4
+ belongs_to :other, class_name: 'Widget', optional: true
5
+
6
+ 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.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: 2019-10-31 00:00:00.000000000 Z
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: rake
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: rspec-rails
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: shoulda-matchers
57
+ name: rake
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - "<"
60
+ - - ">="
61
61
  - !ruby/object:Gem::Version
62
- version: '3.0'
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: '3.0'
69
+ version: '0'
70
70
  - !ruby/object:Gem::Dependency
71
- name: sqlite3
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: pg
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: mysql2
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, and inclusion of ActiveRecord
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
- homepage: http://github.com/joshwlewis/validates_by_schema
129
- licenses: []
130
- metadata: {}
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
- rubyforge_project:
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