Code Monkey home page Code Monkey logo

valhammer's Introduction

Valhammer

Gem Version Build Status Dependency Status Code Climate Coverage Status

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 of presence which doesn't work correctly for booleans
  • :uniqueness — added to match unique keys
  • :numericality — added to integer/decimal columns with the only_integer option set appropriately
  • :length — added to string 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.

valhammer's People

Contributors

smangelsdorf avatar bradleybeddoes avatar rianniello avatar rcaught avatar matthew-puku avatar

Stargazers

Keyth M Citizen  avatar Walter Lee Davis avatar  avatar James Gemmell avatar Toomaj Boloorian avatar

Watchers

Ritchie Young avatar Terry Smith avatar James Cloos avatar Jun avatar Jack avatar Toomaj Boloorian avatar  avatar  avatar  avatar

Forkers

bradleybeddoes

valhammer's Issues

Carrierwave attachments are "too long"

I am getting a validation error for the column that Carrierwave uses to store the filename. It appears as though the file contents are being used as the "text". I have tried messing with the order of inclusion of Valhammer and Carrierwave, but that doesn't help. I would like to be able to validate the filename length as a string, but once Carrierwave is included in the model, whatever the filename column is named becomes an alias for an entire object full of data.

Opt-out for columns

Maybe with , except: [...] and , only: [...] options.

Add the ability for selective "hammering". This allows more configurable custom validations on some fields.

Enum support

Valhammer is not able to handle validations of enums correctly, and will throw a false positive that {{EnumName}} is not a number. This is caused by Rails returning a string representation of the integer backed enum value.

Since any value that's not in the predefined set will raise an argument error in Rails, no validation is necessary. If desired, automatically adding this validation may be possible with some use of read_attribute_before_type_case.

valhammer not behaving with allows_nil

If valhammer is enabled in a class, the allows_nil constraint is ignored:

Before:

class Subject < ActiveRecord::Base
  ...
  validates :shared_token, uniqueness: true, allow_nil: true
  valhammer

end


    context 'shared_token uniqueness' do
      before { create(:subject, shared_token: nil) }
      subject { build(:subject, shared_token: nil) }
      it 'allows many nil shared_tokens' do
        expect(subject).to be_valid
      end
    end

    errors: Shared token has already been taken


After:

class Subject < ActiveRecord::Base
  ...
  validates :shared_token, uniqueness: { allows_nil: true }
  #valhammer

end

    context 'shared_token uniqueness' do
      before { create(:subject, shared_token: nil) }
      subject { build(:subject, shared_token: nil) }
      it 'allows many nil shared_tokens' do
        expect(subject).to be_valid
      end
    end

    1 example, 0 failures, 1 passed

Rspec seems to ignore these validations

I am trying to validate string length on fields, with the following unit test:

  let(:appt) { FactoryGirl.create(:appointment, candidate: clinical_candidate) }

  describe Appointment do

    it "validates string length" do
      # this is a 255-character column we are over-filling
      appt.reason_for_appt = Faker::Lorem.characters(400)
      expect(appt).not_to be_valid
      expect(appt.errors.keys.include? :reason_for_appt).to be true
    end

When I inspect appt._validators inside the test, I see some of the Valhammer-added validators, but not the length validation for this field. The fields that I do see are those that had additional length arguments in their definition, not those that are just at their default length.

In Rails console, I can see that all of the expected validators are created if I call Appointment.new._validators in Pry:

SELECT cc.column_name FROM all_constraints c, all_cons_columns cc WHERE c.owner = 'CEAL' AND c.table_name = 'FOB_APPOINTMENTS' AND c.constraint_type = 'P' AND cc.owner = c.owner AND cc.constraint_name = c.constraint_name
Valhammer generating options for `fob_appointments`.`rank_code` (type=:string null=true limit=255)
Valhammer options for `fob_appointments`.`rank_code` are: {:length=>{:maximum=>255}}
Valhammer generating options for `fob_appointments`.`track_code` (type=:string null=true limit=255)
Valhammer options for `fob_appointments`.`track_code` are: {:length=>{:maximum=>255}}
Valhammer generating options for `fob_appointments`.`reason` (type=:integer null=true limit=38)
Valhammer options for `fob_appointments`.`reason` are: {:numericality=>{:only_integer=>true, :allow_nil=>true}}
Valhammer generating options for `fob_appointments`.`adverse_effect` (type=:string null=true limit=4000)
Valhammer options for `fob_appointments`.`adverse_effect` are: {:length=>{:maximum=>4000}}
Valhammer generating options for `fob_appointments`.`departure_date` (type=:datetime null=true limit=nil)
Valhammer options for `fob_appointments`.`departure_date` are: {}
Valhammer generating options for `fob_appointments`.`reason_for_appt` (type=:string null=true limit=255)
Valhammer options for `fob_appointments`.`reason_for_appt` are: {:length=>{:maximum=>255}}
Valhammer generating options for `fob_appointments`.`start_date` (type=:datetime null=true limit=nil)
Valhammer options for `fob_appointments`.`start_date` are: {}
Valhammer generating options for `fob_appointments`.`person_replaced` (type=:string null=true limit=255)
Valhammer options for `fob_appointments`.`person_replaced` are: {:length=>{:maximum=>255}}
Valhammer generating options for `fob_appointments`.`full_job_title` (type=:string null=true limit=255)
Valhammer options for `fob_appointments`.`full_job_title` are: {:length=>{:maximum=>255}}
Valhammer generating options for `fob_appointments`.`updated_by` (type=:integer null=true limit=38)
Valhammer options for `fob_appointments`.`updated_by` are: {:numericality=>{:only_integer=>true, :allow_nil=>true}}
=> {:rank_code=>[#<ActiveModel::Validations::LengthValidator:0x002ba41a7552a8 @attributes=["rank_code"], @options={:maximum=>255}>],
 :track_code=>[#<ActiveModel::Validations::LengthValidator:0x002ba41a777948 @attributes=["track_code"], @options={:maximum=>255}>],
 :reason=>[#<ActiveModel::Validations::NumericalityValidator:0x002ba41a775a80 @attributes=["reason"], @options={:only_integer=>true, :allow_nil=>true}>],
 :adverse_effect=>[#<ActiveModel::Validations::LengthValidator:0x002ba41a775350 @attributes=["adverse_effect"], @options={:maximum=>4000}>],
 :reason_for_appt=>[#<ActiveModel::Validations::LengthValidator:0x002ba41a769550 @attributes=["reason_for_appt"], @options={:maximum=>255}>],
 :person_replaced=>[#<ActiveModel::Validations::LengthValidator:0x002ba41a78f4a8 @attributes=["person_replaced"], @options={:maximum=>255}>],
 :full_job_title=>[#<ActiveModel::Validations::LengthValidator:0x002ba41a78dd10 @attributes=["full_job_title"], @options={:maximum=>255}>],
 :updated_by=>[#<ActiveModel::Validations::NumericalityValidator:0x002ba41a77fdc8 @attributes=["updated_by"], @options={:only_integer=>true, :allow_nil=>true}>]}

To be clear, the validation does work when I exercise the application in a browser. I would just like to be able to write unit tests for these sorts of things, so I can ensure that it doesn't stop working at some point in the future. Is there something I need to add to my rails_helper or spec_helper to allow these validations to be created before tests run?

Thanks again,

Walter

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.