Ruby how to decrease number of validation checks? - ruby

I want to refactor validation checks on Date fields
include Virtus.model
attribute :created_from, Date
attribute :created_to, Date
attribute :updated_from, Date
attribute :updated_to, Date
validate :validate_created_from_is_a_date, if: :created_from
validate :validate_created_to_is_a_date, if: :created_to
validate :validate_updated_from_is_a_date, if: :updated_from
validate :validate_updated_to_is_a_date, if: :updated_to
def validate_created_from_is_a_date
errors.add(:created_from, "not a date") unless created_from.is_a?(Date)
end
def validate_created_to_is_a_date
errors.add(:created_to, "not a date") unless created_to.is_a?(Date)
end
def validate_updated_from_is_a_date
errors.add(:updated_from, "not a date") unless updated_from.is_a?(Date)
end
def validate_updated_to_is_a_date
errors.add(:updated_to, "not a date") unless updated_to.is_a?(Date)
end
as you can see I have 4 attributes which i need to validate for Date I've tried the following, but it's not working as it checks for all cases
validate :validate_date_attributes, if: :any_date_attributes_defined?
def any_date_attributes_defined?
created_from || created_to || updated_from || updated_to
end
def validate_date_attributes
%w(created_from created_to updated_from updated_to).each do |attribute|
errors.add(attribute.to_sym, "not a date") unless attribute.to_sym.is_a?(Date)
end
end

So found a good way to handle such cases, hope it would be useful for others:
validate :created_from, date_attribute: true, if: :created_from
validate :created_to, date_attribute: true, if: :created_to
validate :updated_from, date_attribute: true, if: :updated_from
validate :updated_to, date_attribute: true, if: :updated_to
which will look for DateAttributeValidator here it's
class DateAttributeValidator < ActiveModel::EachValidator
def validate_each(record, attr_name, value)
record.errors[attr_name] << "not a date" unless value.is_a?(Date)
end
end
I really liked it, pretty neat, it's clean and ruby/rails way

Related

dry-struct How to conditionally validate one attribute?

I'm using dry-types and dry-struct and I would like to have a conditional validation.
for the class:
class Tax < Dry::Struct
attribute :tax_type, Types::String.constrained(min_size: 2, max_size: 3, included_in: %w[IVA IS NS])
attribute :tax_country_region, Types::String.constrained(max_size: 5)
attribute :tax_code, Types::String.constrained(max_size: 10)
attribute :description, Types::String.constrained(max_size: 255)
attribute :tax_percentage, Types::Integer
attribute :tax_ammount, Types::Integer.optional
end
I want to validate tax_ammount as an Integer and mandatory if `tax_type == 'IS'.
dry-struct is really for basic type assertion and coercion.
If you want more complex validation then you probably want to implement dry-validation as well (as recommended by dry-rb)
See Validating data with dry-struct which states
Please don’t. Structs are meant to work with valid input, it cannot generate error messages good enough for displaying them for a user etc. Use dry-validation for validating incoming data and then pass its output to structs.
The conditional validation using dry-validation would be something like
TaxValidation = Dry::Validation.Schema do
# Could be:
# required(:tax_type).filled(:str?,
# size?: 2..3,
# included_in?: %w(IVA IS NS))
# but since we are validating against a list of Strings I figured the rest was implied
required(:tax_type).filled(included_in?: %w(IVA IS NS))
optional(:tax_amount).maybe(:int?)
# rule name is of your choosing and will be used
# as the errors key (i just chose `tax_amount` for consistency)
rule(tax_amount:[:tax_type, :tax_amount]) do |tax_type, tax_amount|
tax_type.eql?('IS').then(tax_amount.filled?)
end
end
This requires tax_type to be in the %w(IVA IS NS) list;
Allows tax_amount to be optional but if it is filled in it must be an Integer (int?) and;
If tax_type == 'IS' (eql?('IS')) then tax_amount must be filled in (which means it must be an Integer based on the rule above).
Obviously you can validate your other inputs as well but I left these out for the sake of brevity.
Examples:
TaxValidation.({}).success?
#=> false
TaxValidation.({}).errors
# => {:tax_type=>["is missing"]}
TaxValidation.({tax_type: 'NO'}).errors
#=> {:tax_type=>["must be one of: IVA, IS, NS"]}
TaxValidation.({tax_type: 'NS'}).errors
#=> {}
TaxValidation.({tax_type: 'IS'}).errors
#=> {:tax_amount=>["must be filled"]}
TaxValidation.({tax_type: 'IS',tax_amount:'NO'}).errors
#=> {:tax_amount=>["must be an integer"]}
TaxValidation.({tax_type: 'NS',tax_amount:12}).errors
#=> {}
TaxValidation.({tax_type: 'NS',tax_amount:12}).success?
#=> true
There is an alternative solution - without validation rules that essentially duplicate the struct attributes.
module TaxChore
class BaseTax < Dry::Struct
attribute :type, Types::String.enum('IVA','NS')
# ...
attribute? :amount, Types::Integer.default(0)
end
class MandatoryTax < BaseTax
attribute :type, Types::String.enum('IS')
attribute :amount, Types::Integer
end
Tax = BaseTax | MandatoryTax
def self.run
tax = Tax.(type: 'IVA')
p tax
tax = Tax.(type: 'IVA', amount: 21)
p tax
tax = Tax.(type: 'IS', amount: 42)
p tax
begin
tax = Tax.(type: 'IS')
p tax
rescue Dry::Struct::Error => e
puts e
end
end
run
end
Outputs:
#<TaxChore::BaseTax type="IVA" amount=0>
#<TaxChore::BaseTax type="NS" amount=21>
#<TaxChore::MandatoryTax type="IS" amount=42>
[TaxChore::MandatoryTax.new] :amount is missing in Hash input
(N.B: I've removed the redundant tax_ prefix, since tax.type is just as clear but shorter.)

complex validation on object update?

I have a model "WorkDetail" and db-attrs related to problem are 'name', 'status' and 'approved_status' with datatype all integer and the model class definition is as:-
class WorkDetail < ActiveRecord::Base
enum name: [:smartcheck, :install, :verify]
enum status: [:pending, :inprogress, :complete]
belongs_to :work_order
has_many :cabinets
after_save :work_order_status_update
private
def work_order_status_update
work_detail_count = self.work_order.work_details.count
status_array = self.work_order.work_details.where(status: 2).count
if status_array == work_detail_count
if work_detail_count == 0
self.work_order.update({status: "pending"})
else
self.work_order.update({status: "complete"})
end
else
self.work_order.update({status: "inprogress"})
end
end
end
Now, I would like to add custom validation to be applied for below problem :-
validation should only apply for update process.
If object's name == "smartcheck" and status == "complete" then only the approved_status boolean attribute should be updated to true (the
default is false on migration), else should give error if not
smartchecked and the status is not complete on trying to update
approved_status attr.
Hoping the question makes sense and Thanks !!! in advance guys, Happy Coding.
1) Validation should only apply for update process
after_save :work_order_status_update, :on => :update
2) If object's name == "smartcheck" and status == "complete" then only
the approved_status boolean attribute should be updated to true (the
default is false on migration), else should give error if not
smartchecked and the status is not complete on trying to update
approved_status attr.
def test_status_and_name
if self.name == "smartcheck" and self.status == "complete"
self.update_attributes(approved_status: true)
else
errors[:base] << "smartchecked and the status is not complete on trying to update approved_status attr."
end
end

Ruby If statement

I am trying to do a post and run some if statement. What I want to do is:
check all fields are filled
if all fields are filled move on to next step, or else reload page
check if already in data base
add if not already in data base
post "/movies/new" do
title = params[:title]
year = params[:year]
gross = params[:gross]
poster = params[:poster]
trailer = params[:trailer]
if title && year && gross && poster && trailer
movie = Movie.find_by(title: title, year: year, gross: gross)
if movie
redirect "/movies/#{movie.id}"
else
movie = Movie.new(title: title, year: year, gross: gross, poster: poster, trailer: trailer)
if movie.save
redirect "/movies/#{movie.id}"
else
erb :'movies/new'
end
end
else
erb :'movies/new'
end
end
I don't think my if statement is correct. It works even if all my fields are not filled
Your code is doing a lot of work in one single method. I would suggest to restructure it into smaller chunks to make it easier to manage. I mostly code for Rails, so apologies if parts of these do not apply to your framework.
post "/movies/new" do
movie = find_movie || create_movie
if movie
redirect "/movies/#{movie.id}"
else
erb :'movies/new'
end
end
def find_movie
# guard condition to ensure that the required parameters are there
required_params = [:title, :year, :gross]
return nil unless params_present?(required_params)
Movie.find_by(params_from_keys(required_params))
end
def create_movie
required_params = [:title, :year, :gross, :poster, :trailer]
return nil unless params_present?(required_params)
movie = Movie.new(params_from_keys(required_params))
movie.save ? movie : nil # only return the movie if it is successfully saved
end
# utility method to check whether all provided params are present
def params_present?(keys)
keys.each {|key| return false if params[key].blank? }
true
end
# utility method to convert params into the hash format required to create / find a record
def params_from_keys(keys)
paras = {}
keys.each { |key| paras.merge!(key: params[key]) }
paras
end
Even if you type nothing in the HTML fields, they will still be submitted as empty strings.
You can avoid having empty parameters by, for example, filtering them:
post '/movies/new' do
params.reject! { |key, value| value.empty? }
# rest of your code
end
Also I would rather post to /movies rather than to /movies/new, that's more REST-wise.
Try if condition to check fields are blank like below -
unless [title, year, gross, poster, trailer].any?(&:blank?)
This will check any of the field should not be nil or blank("").

A good way to check for inclusion of several items in an array

I have an options hash and a method to update it - but the options hash could change and if it does I want my tests to fail. What's a good way to write this?
raise RuntimeError, msg unless options.keys.include?(
:external_uid,
:display_name,
:country_code
)
If options.keys doesn't include exactly those three items, an error should be raised.
solution that i almost used (thanks bjhaid):
def ensure_correct_options!(options)
msg = "Only 'external_uid', 'display_name' and 'country_code' can be "
msg += "updated. Given attributes: #{options.keys.inspect}"
raise RuntimeError, msg unless options.keys == [
:external_uid,
:display_name,
:country_code
]
end
The options probably have a value, so I would write:
unless options[:external_uid] && options[:display_name] && options[:country_code]
raise ArgumentError, ":external_uid, :display_name and :country_code required"
end
(I've replaced RuntimeError with ArgumentError because this seems to be about arguments)
If there were more than three values to test for inclusion as keys in the hash, you might do it t like this:
unless ([:external_uid, :display_name,...] - options.keys).empty? \
raise ArgumentError, ":external_uid, :display_Nam,... are required"

Better way to assert that all user.name in an array of user start with a prefix using rspec?

Here is what I have. And that kind of work.
it "should filter by name" do
users = users.search(:name => "s")
users.each {|u|
u.name.should be_starts_with("s")
}
end
However, the error message returned by rspec is really poor...
expected starts_with?("s") to return true, got false
Is there a way to get a more precise message, showing the element that failed, or at least its index?
In a binary test like this, I would create two users, one that starts with an s, the other without. I would then check that only the expected element was returned.
like
set up a user(:name => "Sam") and user(:name => "Fred")
filtered_users.map(&:name).should =~ ["Sam"]
In the case of failure, you will see something like
expected ["Sam"]
got ["Fred", "Sam"]
This is much more explicit about what you are doing
The reason you are only getting expected true but got false is because the starts_with methods returns true or false and not the actual value.
I'm not sure that this is the best way, but you can output it yourself.
users.each {|u|
p u.name if !u.name.starts_with?("s")
u.name.should be_starts_with("s")
}
Here is the way I used few times in cases like this:
describe 'user' do
before :each do
#users = users.search(:name => "s")
end
#users.each do |u|
it "should filter user with name '#{u.name}'" do
u.name.should be_starts_with("s")
end
end
end
You will have failed user name in you example description.
I found here an interesting extension to the matchers from Rspec concerning each :
http://xtargets.com/2011/08/12/rspec-meta-expectations-over-collections
So I sticked that helper into my spec_helper
RSpec::Matchers.define :each do |meta|
match do |actual|
actual.each_with_index do |i, j|
#elem = j
i.should meta
end
end
failure_message_for_should do |actual|
"at[#{#elem}] #{meta.failure_message_for_should}"
end
that allows me to write
users.should each satisfy {|u| u.name.should be_starts_with 's'}
and the error message is then :
at[1] expected #User to satisfy block
which give me the first index of failure.
With some addition to the error message, I'm sure I could output the details of that object that didn't match, and that seem a pretty good solution.
Any thoughts? I'm not a rubyist, just getting started with rails. Would be nice to get more input from
This should provide you with far better failure messages
it "should filter by name" do
users = users.search(:name => "s")
users.each do |u|
u.name.should match /^s/
end
end
I agree with Corey that calling "be_starts_with" is rough. RSpec expectations are intended to be read fluidly as a sentence. They don't all have to use "be".

Resources