validates_by_schema 0.2.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a44382b4c751d51c6056b9eae1daed6f8eedfe8040383fe38ce717beafc2c84d
4
+ data.tar.gz: 553ffe11b1a3c83f6c2f1126954d691553465492f7f3820f6530b14a48675971
5
+ SHA512:
6
+ metadata.gz: b404ea525a570681cc76d936755f6f6fbc89a310e8409d0480161a2a9aad11e1c803445c30b0cf03ce951d02ae63f614715e48c3a9bfde80455e5cec4b293c71
7
+ data.tar.gz: a7b24d0fe512674063b0c055f155b5cd608661fcf284634399354c6d63a3acfbf7fba6e9de335ea5e55e25765cedc8bda60505879aa7d9a09f96a3e828ebadba
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Validates By Schema (validates_by_schema)
2
- [![Build Status](https://secure.travis-ci.org/joshwlewis/validates_by_schema.png)](http://travis-ci.org/joshwlewis/validates_by_schema)
3
- [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/joshwlewis/validates_by_schema)
4
- [![Dependency Status](https://gemnasium.com/joshwlewis/validates_by_schema.png)](https://gemnasium.com/joshwlewis/validates_by_schema)
5
- [![Gem Version](https://badge.fury.io/rb/validates_by_schema.png)](http://badge.fury.io/rb/validates_by_schema)
2
+
3
+ [![Gem Version](http://img.shields.io/gem/v/validates_by_schema.svg?style=flat)](https://rubygems.org/gems/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)
5
+ [![Coverage Status](http://img.shields.io/coveralls/joshwlewis/validates_by_schema.svg?style=flat)](https://coveralls.io/r/joshwlewis/validates_by_schema)
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
  Automatic validation based on your database schema column types and limits. Keep your code DRY by inferring column validations from table properties!
8
9
 
@@ -11,21 +12,35 @@ Automatic validation based on your database schema column types and limits. Keep
11
12
  Say you had a table setup like this:
12
13
 
13
14
  ```ruby
14
- create_table "widgets", :force => true do |t|
15
- t.integer "quantity", :limit => 2
16
- t.decimal "thickness", :precision => 4, :scale => 4
17
- t.string "color", :null => false
15
+ create_table "widgets", force: true do |t|
16
+ t.integer "quantity", limit: 2, null: false
17
+ t.decimal "thickness", precision: 4, scale: 4
18
+ t.string "color", null: false
19
+ t.boolean "flagged", null: false, default: false
20
+ t.integer "other_id", null: false
21
+ t.index ["other_id", "color"], unique: true
18
22
  end
19
23
  ```
20
24
 
21
25
  Then these validations are inferred when you add `validates_by_schema` to your model:
22
26
 
23
27
  ```ruby
24
- validates :quantity, numericality: { allow_nil: true,
25
- greater_than: -32768, less_than: 32768}
26
- validates :thickness, numericality: {allow_nil: true,
27
- less_than_or_equal_to: 0.999, greater_than_or_equal_to: -0.999}
28
- validates :color, presence: true, length: {allow_nil: false, maximum: 255}
28
+ validates :quantity, presence: true,
29
+ numericality: { allow_nil: true,
30
+ only_integer: true,
31
+ greater_than: -32768,
32
+ less_than: 32768}
33
+ validates :thickness, numericality: { allow_nil: true,
34
+ less_than_or_equal_to: 0.999,
35
+ greater_than_or_equal_to: -0.999 }
36
+ validates :color, presence: true, length: { allow_nil: true, maximum: 255 }
37
+ validates :flagged, inclusion: { in: [true, false], allow_nil: false }
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
+ }
29
44
  ```
30
45
 
31
46
  ## Installation
@@ -58,8 +73,24 @@ validates_by_schema only: [:body, :description]
58
73
  validates_by_schema except: [:name, :title]
59
74
  ```
60
75
 
76
+ The primary key and timestamp columns are not validated.
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
+
61
84
  ## Notes
62
85
 
63
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.
64
87
 
65
- This has been tested with mysql, postgresql, and sqlite3. It should work with any other database that has reliable adapter.
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
@@ -20,19 +20,35 @@ RDoc::Task.new(:rdoc) do |rdoc|
20
20
  rdoc.rdoc_files.include('lib/**/*.rb')
21
21
  end
22
22
 
23
-
24
-
25
-
26
23
  Bundler::GemHelper.install_tasks
27
24
 
28
- require 'rake/testtask'
25
+ require 'rspec/core/rake_task'
29
26
 
30
- Rake::TestTask.new(:test) do |t|
31
- t.libs << 'lib'
32
- t.libs << 'test'
33
- t.pattern = 'test/**/*_test.rb'
34
- t.verbose = false
27
+ RSpec::Core::RakeTask.new(:spec) do |t|
28
+ t.pattern = Dir.glob('spec/**/*_spec.rb')
29
+ t.rspec_opts = ['--backtrace']
35
30
  end
36
31
 
37
-
38
- task :default => :test
32
+ task default: :spec
33
+
34
+ namespace :db do
35
+ task :drop do
36
+ puts 'dropping'
37
+ case ENV['DB']
38
+ when 'postgresql'
39
+ exec "psql -c 'drop database if exists validates_by_schema_test;' -U postgres -h localhost"
40
+ when 'mysql'
41
+ exec "mysql -e 'drop database if exists validates_by_schema_test;' -u root -h 127.0.0.1"
42
+ end
43
+ end
44
+
45
+ task :create do
46
+ puts 'creating'
47
+ case ENV['DB']
48
+ when 'postgresql'
49
+ exec "psql -c 'create database validates_by_schema_test;' -U postgres -h localhost"
50
+ when 'mysql'
51
+ exec "mysql -e 'create database validates_by_schema_test;' -u root -h 127.0.0.1"
52
+ end
53
+ end
54
+ end
@@ -1,12 +1,40 @@
1
1
  class ValidatesBySchema::ValidationOption
2
2
  # column here must be an ActiveRecord column
3
3
  # i.e. MyARModel.columns.first
4
- attr_accessor :column
4
+ attr_accessor :klass, :column
5
5
 
6
- def initialize(column)
6
+ def initialize(klass, column)
7
+ @klass = klass
7
8
  @column = column
8
9
  end
9
10
 
11
+ def define!
12
+ if association
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
+ else
17
+ define_validations(to_hash)
18
+ end
19
+ define_uniqueness_validations if ValidatesBySchema.validate_uniqueness
20
+ end
21
+
22
+ private
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
+
10
38
  def presence?
11
39
  presence && column.type != :boolean
12
40
  end
@@ -15,8 +43,12 @@ class ValidatesBySchema::ValidationOption
15
43
  !column.null
16
44
  end
17
45
 
46
+ def enum?
47
+ klass.respond_to?(:defined_enums) && klass.defined_enums.has_key?(column.name)
48
+ end
49
+
18
50
  def numericality?
19
- [:integer, :decimal, :float].include? column.type
51
+ [:integer, :decimal, :float].include?(column.type) && !enum?
20
52
  end
21
53
 
22
54
  def numericality
@@ -27,20 +59,48 @@ class ValidatesBySchema::ValidationOption
27
59
  numericality[:less_than] = integer_max
28
60
  numericality[:greater_than] = -integer_max
29
61
  end
30
- elsif column.type == :decimal
62
+ elsif column.type == :decimal && decimal_max
31
63
  numericality[:less_than_or_equal_to] = decimal_max
32
64
  numericality[:greater_than_or_equal_to] = -decimal_max
33
65
  end
34
- numericality[:allow_nil] = column.null
66
+ numericality[:allow_nil] = true
35
67
  numericality
36
68
  end
37
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
+
94
+ def array?
95
+ column.respond_to?(:array) && column.array
96
+ end
97
+
38
98
  def length?
39
- [:string, :text].include?(column.type) && column.limit
99
+ [:string, :text].include?(column.type) && column.limit && !array?
40
100
  end
41
101
 
42
102
  def length
43
- {:maximum => column.limit, :allow_nil => column.null }
103
+ { maximum: column.limit, allow_nil: true }
44
104
  end
45
105
 
46
106
  def inclusion?
@@ -48,23 +108,25 @@ class ValidatesBySchema::ValidationOption
48
108
  end
49
109
 
50
110
  def inclusion
51
- {:in => [true, false], :allow_nil => column.null}
111
+ { in: [true, false], allow_nil: column.null }
52
112
  end
53
113
 
54
114
  def integer_max
55
- (2 ** (8 * column.limit)) / 2 if column.limit
115
+ (2**(8 * column.limit)) / 2 if column.limit
56
116
  end
57
117
 
58
118
  def decimal_max
59
- 10.0**(column.precision-column.scale) - 10.0**(-column.scale)
119
+ 10.0**(column.precision - column.scale) - 10.0**(-column.scale) if column.precision && column.scale
60
120
  end
61
121
 
62
- def string_max
63
- column.limit
122
+ def association
123
+ @association ||= klass.reflect_on_all_associations(:belongs_to).find do |a|
124
+ a.foreign_key.to_s == column.name
125
+ end
64
126
  end
65
127
 
66
128
  def to_hash
67
- [:presence, :numericality, :length, :inclusion].inject({}) do |h,k|
129
+ [:presence, :numericality, :length, :inclusion].inject({}) do |h, k|
68
130
  send(:"#{k}?") ? h.merge(k => send(k)) : h
69
131
  end
70
132
  end
@@ -73,4 +135,4 @@ class ValidatesBySchema::ValidationOption
73
135
  to_hash.inspect
74
136
  end
75
137
 
76
- end
138
+ end
@@ -1,3 +1,3 @@
1
1
  module ValidatesBySchema
2
- VERSION = "0.2.2"
2
+ VERSION = '0.5.0'
3
3
  end
@@ -1,30 +1,62 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/lazy_load_hooks'
3
+
1
4
  module ValidatesBySchema
2
5
  autoload :ValidationOption, 'validates_by_schema/validation_option'
3
6
 
4
7
  extend ActiveSupport::Concern
5
8
 
9
+ mattr_accessor :validate_uniqueness
10
+ self.validate_uniqueness = true
11
+
6
12
  module ClassMethods
7
- def validates_by_schema options={}
8
- return unless table_exists?
9
- columns = schema_validateable_columns
10
13
 
11
- # Allow user to specify :only or :except options
12
- [:only => :select!, :except => :reject!].each do |k,v|
13
- columns.send(v){|c| options[k].include? c.name} if options[k]
14
+ def validates_by_schema(options = {})
15
+ @validates_by_schema_options = options
16
+ define_schema_validations if schema_loaded?
17
+ end
18
+
19
+ def load_schema!
20
+ super
21
+ # define schema validations lazy to avoid accessing the database
22
+ # at class load time.
23
+ define_schema_validations
24
+ end
25
+
26
+ private
27
+
28
+ def define_schema_validations
29
+ return unless @validates_by_schema_options
30
+
31
+ customized_schema_validatable_columns.each do |c|
32
+ ValidationOption.new(self, c).define!
14
33
  end
15
34
 
16
- columns.each do |c|
17
- vo = ValidationOption.new(c).to_hash
18
- validates c.name, vo if vo.present?
35
+ @validates_by_schema_options = nil
36
+ end
37
+
38
+ def customized_schema_validatable_columns
39
+ # Allow user to specify :only or :except options
40
+ schema_validatable_columns.tap do |columns|
41
+ { only: :select!, except: :reject! }.each do |k, v|
42
+ if @validates_by_schema_options[k]
43
+ attrs = Array(@validates_by_schema_options[k]).collect(&:to_s)
44
+ columns.send(v) { |c| attrs.include?(c.name) }
45
+ end
46
+ end
19
47
  end
20
48
  end
21
49
 
22
- def schema_validateable_columns
23
- # Don't auto validate primary keys or timestamps
24
- columns.reject do |c|
25
- c.primary || %w(updated_at created_at).include?(c.name)
50
+ def schema_validatable_columns
51
+ columns.reject do |c|
52
+ ignored_columns_for_validates_by_schema.include?(c.name)
26
53
  end
27
54
  end
55
+
56
+ def ignored_columns_for_validates_by_schema
57
+ [primary_key.to_s, 'created_at', 'updated_at', 'deleted_at']
58
+ end
59
+
28
60
  end
29
61
  end
30
62
 
@@ -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,151 +1,182 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: validates_by_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
5
- prerelease:
4
+ version: 0.5.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Josh Lewis
8
+ - Pascal Zumkehr
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-08 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
- name: rails
15
+ name: activerecord
16
16
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
17
  requirements:
19
- - - ! '>='
18
+ - - ">="
20
19
  - !ruby/object:Gem::Version
21
- version: 3.1.0
20
+ version: 5.0.0
22
21
  type: :runtime
23
22
  prerelease: false
24
23
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
24
  requirements:
27
- - - ! '>='
25
+ - - ">="
28
26
  - !ruby/object:Gem::Version
29
- version: 3.1.0
27
+ version: 5.0.0
30
28
  - !ruby/object:Gem::Dependency
31
- name: rspec-rails
29
+ name: mysql2
32
30
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
31
  requirements:
35
- - - ! '>='
32
+ - - ">="
36
33
  - !ruby/object:Gem::Version
37
34
  version: '0'
38
35
  type: :development
39
36
  prerelease: false
40
37
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
38
  requirements:
43
- - - ! '>='
39
+ - - ">="
44
40
  - !ruby/object:Gem::Version
45
41
  version: '0'
46
42
  - !ruby/object:Gem::Dependency
47
- name: shoulda-matchers
43
+ name: pg
48
44
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
45
  requirements:
51
- - - ! '>='
46
+ - - ">="
52
47
  - !ruby/object:Gem::Version
53
48
  version: '0'
54
49
  type: :development
55
50
  prerelease: false
56
51
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
52
  requirements:
59
- - - ! '>='
53
+ - - ">="
60
54
  - !ruby/object:Gem::Version
61
55
  version: '0'
62
56
  - !ruby/object:Gem::Dependency
63
- name: sqlite3
57
+ name: rake
64
58
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
59
  requirements:
67
- - - ! '>='
60
+ - - ">="
68
61
  - !ruby/object:Gem::Version
69
62
  version: '0'
70
63
  type: :development
71
64
  prerelease: false
72
65
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
66
  requirements:
75
- - - ! '>='
67
+ - - ">="
76
68
  - !ruby/object:Gem::Version
77
69
  version: '0'
78
70
  - !ruby/object:Gem::Dependency
79
- name: pg
71
+ name: rspec-rails
80
72
  requirement: !ruby/object:Gem::Requirement
81
- none: false
82
73
  requirements:
83
- - - ! '>='
74
+ - - ">="
84
75
  - !ruby/object:Gem::Version
85
76
  version: '0'
86
77
  type: :development
87
78
  prerelease: false
88
79
  version_requirements: !ruby/object:Gem::Requirement
89
- none: false
90
80
  requirements:
91
- - - ! '>='
81
+ - - ">="
92
82
  - !ruby/object:Gem::Version
93
83
  version: '0'
94
84
  - !ruby/object:Gem::Dependency
95
- name: mysql2
85
+ name: shoulda-matchers
96
86
  requirement: !ruby/object:Gem::Requirement
97
- none: false
98
87
  requirements:
99
- - - ! '>='
88
+ - - ">="
100
89
  - !ruby/object:Gem::Version
101
90
  version: '0'
102
91
  type: :development
103
92
  prerelease: false
104
93
  version_requirements: !ruby/object:Gem::Requirement
105
- none: false
106
94
  requirements:
107
- - - ! '>='
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: sqlite3
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
108
110
  - !ruby/object:Gem::Version
109
111
  version: '0'
110
112
  description: Keep your code DRY by inferring column validations from table properties!
111
- Automagically validate presence, length, numericality, and inclusion of ActiveRecord
112
- backed columns.
113
+ Automagically validate presence, length, numericality, inclusion and uniqueness
114
+ of ActiveRecord backed columns.
113
115
  email:
114
116
  - josh.w.lewis@gmail.com
117
+ - pascal@codez.ch
115
118
  executables: []
116
119
  extensions: []
117
120
  extra_rdoc_files: []
118
121
  files:
119
- - lib/tasks/validates_by_schema_tasks.rake
120
- - lib/validates_by_schema/validation_option.rb
121
- - lib/validates_by_schema/version.rb
122
- - lib/validates_by_schema.rb
123
122
  - MIT-LICENSE
124
- - Rakefile
125
123
  - README.md
126
- homepage: http://github.com/joshwlewis/validates_by_schema
127
- licenses: []
124
+ - Rakefile
125
+ - lib/validates_by_schema.rb
126
+ - lib/validates_by_schema/validation_option.rb
127
+ - lib/validates_by_schema/version.rb
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
128
149
  post_install_message:
129
150
  rdoc_options: []
130
151
  require_paths:
131
152
  - lib
132
153
  required_ruby_version: !ruby/object:Gem::Requirement
133
- none: false
134
154
  requirements:
135
- - - ! '>='
155
+ - - ">="
136
156
  - !ruby/object:Gem::Version
137
157
  version: '0'
138
158
  required_rubygems_version: !ruby/object:Gem::Requirement
139
- none: false
140
159
  requirements:
141
- - - ! '>='
160
+ - - ">="
142
161
  - !ruby/object:Gem::Version
143
162
  version: '0'
144
163
  requirements: []
145
- rubyforge_project:
146
- rubygems_version: 1.8.23
164
+ rubygems_version: 3.1.4
147
165
  signing_key:
148
- specification_version: 3
166
+ specification_version: 4
149
167
  summary: Automatic validation based on your database schema column types and limits.
150
- test_files: []
151
- has_rdoc:
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
@@ -1,4 +0,0 @@
1
- # desc "Explaining what the task does"
2
- # task :validates_by_schema do
3
- # # Task goes here
4
- # end