Valhammer
Automatically validate ActiveRecord models based on the database schema.
Copyright 2015-2016, Australian Access Federation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Installation
Add this line to your application's Gemfile:
gem 'valhammer'
Use Bundler to install the dependency:
bundle install
In Rails, Valhammer is automatically added to ActiveRecord::Base
. If you're
using ActiveRecord outside of Rails, you may have to do this yourself:
ActiveRecord::Base.extend(Valhammer::Validations)
Usage
Call the valhammer
method inside your model class, after any belongs_to
relationships are defined:
class Widget < ActiveRecord::Base
belongs_to :supplier
valhammer
end
Generated validations are:
-
:presence
— added to non-nullable, non-boolean columns -
:inclusion
— added to boolean columns to emulate the functionality ofpresence
which doesn't work correctly for booleans -
:uniqueness
— added to match unique keys -
:numericality
— added tointeger
/decimal
columns with theonly_integer
option set appropriately -
:length
— added tostring
columns to ensure the value fits in the column
SQLite Note: In SQLite, a string
column has no default length restriction
(except for the hard limit on data size set at compile
time). Valhammer will not apply a length
validation unless the column was created with an explicit limit.
Disabling Validators
Passing a block to valhammer
allows some selective calls to disable
to
customise the validators which are applied to your model:
class Widget < ActiveRecord::Base
valhammer do
disable item_code: [:presence, :uniqueness]
end
end
Disabling an attribute instructs Valhammer not to apply any validators for that attribute:
class Widget < ActiveRecord::Base
valhammer do
disable :supplier_code
end
end
When disabling validations for an association, disable the validations on the association name, not the name of the foreign key column:
class Widget < ActiveRecord::Base
belongs_to :supplier
valhammer do
disable supplier: :presence
end
end
Composite Unique Keys
When Valhammer encounters a composite unique key, it inspects the columns
involved in the key and uses them to build a scope
. For example:
create_table(:widgets) do |t|
t.string :supplier_code, null: false, default: nil
t.string :item_code, null: false, default: nil
t.index [:supplier_code, :item_code], unique: true
end
When this table is examined by Valhammer, the uniqueness validation created will be the same as if you had written:
class Widget < ActiveRecord::Base
validates :item_code, uniqueness: { scope: :supplier_code }
end
That is, the last column in the key is the field which gets validated, and the
other columns form the scope
argument.
If any of the scope columns are nullable, the validation will be conditional on
the presence of the scope values. This avoids the situation where your
underlying database would accept a row (because it considers NULL
values not
equal to each other, which is true of SQLite,
PostgreSQL and MySQL/MariaDB at the
least.)
If the above example table had nullable columns, for example:
create_table(:widgets) do |t|
t.string :supplier_code, null: true, default: nil
t.string :item_code, null: true, default: nil
t.index [:supplier_code, :item_code], unique: true
end
This amended table structure causes Valhammer to create validations as though you had written:
class Widget < ActiveRecord::Base
validates :item_code, uniqueness: { scope: :supplier_code,
if: -> { supplier_code },
allow_nil: true }
end
Duplicate Unique Keys
Valhammer is able to handle the simple case when multiple unique keys reference the same field, as in the following contrived example:
create_table(:order_update) do |t|
t.belongs_to :order
t.string :state
t.string :identifier
t.index [:order_id, :state], unique: true
t.index [:order_id, :state, :identifier], unique: true
end
Uniqueness validations are created as though the model was defined using:
class OrderUpdate < ActiveRecord::Base
validates :state, uniqueness: { scope: :order_id }
validates :identifier, uniqueness: { scope: [:order_id, :state] }
end
In the case where multiple unique keys have the same column in the last position, Valhammer is unable to determine which is the "authoritative" scope for the validation. Take the following contrived example:
create_table(:order_enquiry) do |t|
t.belongs_to :order
t.belongs_to :customer
t.string :date
t.index [:order_id, :date], unique: true
t.index [:customer_id, :date], unique: true
end
Valhammer is unable to resolve which scope
to apply, so no uniqueness
validation is applied.
Unique Keys and Associations
In the case where a foreign key is the last column in a key, that key will not be given a uniqueness validation.
create_table(:order_payment) do |t|
t.belongs_to :customer
t.string :reference
t.boolean :complete
t.integer :amount
t.index [:reference, :customer_id], unique: true
end
To work around this, put associations first in your unique keys (often a good idea anyway, if it means your association queries benefit from the index).
Alternatively, apply the validation yourself using ActiveRecord.
Partial Unique Keys
When a unique key is partially applied to a relation, that key will not be given a uniqueness validation.
create_table(:widgets) do |t|
t.string :supplier_code, null: true, default: nil
t.string :item_code, null: true, default: nil
t.index [:supplier_code, :item_code], unique: true,
where: 'item_code LIKE "a%"'
end
In this case, it is not possible for valhammer to determine the behaviour of the
where
clause, so the validation must be manually created.
Logging
To make Valhammer tell you exactly what it's doing, turn on verbose mode:
Valhammer.config.verbose = true
Contributing
Refer to GitHub Flow for help contributing to this project.