Duplicated Validation Across Form Objects and Models - ruby-on-rails

Where do the basic validators lie when dealing with Form objects and regular Rails models?
Following the concept of decoupling forms from the persistence layer in Rails. I've setup a Form Object Cage that creates two objects together... say Animal and Plant.
Following Form Object examples from http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ or https://github.com/solnic/virtus or https://github.com/makandra/active_type , each of these show the Form object itself has validations... no problem... part of the benefits include being able to validate objects in a more contextually aware way.
The issue:
class Animal < ActiveRecord::Base
validates :color, presence: true
validate :only_one_brown
private
def only_one_brown
if some_complex_thing
errors.add(:color, 'can not have more than one brown animal.')
end
end
end
class Plant < ActiveRecord::Base
validates :color, presence: true
end
class Cage
include Virtus.model # or ActiveType or whatever
include ActiveModel::Validations
attribute :bird_color, String
attribute :plant_color, String
validates :bird_color, presence: true
validates :plant_color, presence: true
def save
if valid?
animal.save!
plant.save!
true
else
false
end
end
def animal
#animal ||= Animal.new(color: bird_color)
end
def plant
#plant ||= Plant.new(color: plant_color)
end
end
How do I validate animal's "only one brown" rule without:
Too much duplication.
A lot of code to make Cage still act like an AR model
If we don't duplicate the validation code, when "only one brown" is false, Cage doesn't have an error for it... we'll raise, which requires the controller to catch and handle, which is bad.
If we do duplicate the code, and if there are several custom validations, we're duplicating a lot of code and each other form object that deals with Animal needs the duplicated validations now.
If we move the validation code out of Animal into Cage entirely, similar issue: all objects that interact with Animal need to know about the "only one brown" rule, which is just duplicating validators and opening up an easy way to forget to enforce it somewhere.
If we move Animal's error array up to Cage's, Animal's error is on :color, which is ambiguous to Cage, and shows an error on an attribute name the client never sent in. If you want to map Animal's error keys to Cage's, now you need to keep an map for each Form Object, feels stinky.
Are there any good patterns or ways to deal with this situation? I feel like it is very common when you start using Form Objects but all examples are quite trivial.
Thanks in advance!

At the end of point 3 on http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ the author says: "As a bonus, since validation logic is often contextual, it can be defined in the place exactly where it matters instead of needing to guard validations in the ActiveRecord itself." I'm agree with Bryan Helmkamp, puts the validation where it matters, you don't need to duplicate it.
edited:
If I were you, I'll put the validation only on the ActiveRecord model. And I'll update the Cage class:
def save
if valid?
ActiveRecord::Base.transaction do
animal.save!
plant.save!
end
true
else
false
end
rescue Exception => exception
raise if valid?
false
end
And I'll add an errors method that returns the errors of Cage, Plant and Animal instances.
edited:
I think you can redefine the valid? method, and then errors works fine:
class Cage
include ActiveModel::Model
def valid_with_mymodels?
valid_without_mymodels? && animal.valid? && plant.valid?
animal.errors.each do |attribute, error|
self.errors.add :"bird_#{attribute.to_s}", error
end
plant.errors.each do |attribute, error|
self.errors.add :"plant_#{attribute.to_s}", error
end
errors.empty?
end
alias_method_chain :valid?, :mymodels
...
end
Just, be careful with the name of your attrs.
I'm not sure how works Virtus, with Rails 4 you can use ActiveModel::Model, if using rails 3 I need research.
edited:
If you are using Rails 3.2, you can't use ActiveModel::Model, but you get the same with this:
class Cage
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
...
end

Related

How to pass around model errors between multiple classes?

What's an elegant way to handle passing around model errors through chains of classes?
Let's say I have a complex form with inputs foo and bar for which I want to display validation errors on the form. However, foo and bar themselves are not model inputs, they are just data passed to service classes:
class ComplexForm
attr_accessor :foo, :bar
def save
foo_processor.process(foo)
bar_processor.process(bar)
end
end
The processors then validate the inputs, validate the resulting model, and either save or return errors. The processors use ActiveModel::Model
class FooProcessor
include ActiveModel::Model
validate :foo_is_valid
def process(foo)
model_instance = MyModel.new(parse(foo))
if valid? && model_instance.valid?
model_instance.save
else
???
end
end
end
How do I pass this potential chain of errors up to ComplexForm and attribute them to the right input?

Rails custom model setter and querying

Suppose there is a Rails model with a custom setter/accessor and a uniqueness constraint on the name column:
class Person < ActiveRecord::Base
validates :name, presence: true, uniqueness: true
def name=(name)
# Example transformation only.
# Could be substituted for a more complex operation/transformation.
title_cased = name.titleize
self[:name] = title_cased
end
end
Now, consider the following:
Person.create! name: "John Citizen"
Person.find_or_create_by! name: "john citizen" # Error: validation fails
The find operation will not find any results, since there are no entries that match "john citizen". Then, the create! operation will throw an error as there is already an existing entry "John Citizen" (create! creates a new record and raises an exception if the validation fails).
How do you elegantly prevent such errors from occurring? For loose coupling and encapsulation purposes, is it possible to not transform names (to titlecase, in this case) before I perform operations like find_or_create_by! or other operations like find_by?
EDIT:
As #harimohanraj alludes to, the issue seems to be around equivalence. Should the model transparently deal with the understanding/translating input to its boiled-down, canonical state. Or should this be the responsibility of consumers of the class/model?
Also, is active record callbacks a recommended approach to this kind of scenario?
If you have defined a custom setter method, the implicit decision that you have made is: values for the name attribute, no matter what form they come in (eg. a user's input in a text field), should be handled in titleized form in your DB. If that's the case, then it makes sense that find_or_create_by! name: 'john citizen' fails! In other words, your custom setter method represents your decision that "John Citizen" and "john citizen" are one and the same.
If you find yourself wanting to store John Citizen and john citizen in your DB, then I would revisit your decision to create a custom setter method. One cool way to achieve "loose coupling" is to put all of the logic that sanitizes data (ex. data from a user filling out a form) into a separate Ruby object.
There isn't much context in the question, so here is a bit of an abstract example to demonstrate what I mean.
# A class to house the logic of sanitizing your parameters
class PersonParamsSanitizer
# It is initialized with dirty user parameters
def initialize(params)
#params = params
end
# It spits out neat, titleized params
def sanitized_params
{
name: #params[:name].titleize
}
end
end
class PersonController < ApplicationController
def create
# Use your sanitizer object to convert dirty user parameters into neat
# titleized params for your new perons
sanitized_params = UserParamsSanitizer.new(params).sanitized_params
person = Person.new(sanitized_params)
if person.save
redirect_to person
else
render :new
end
end
end
This way, you don't override the setter method in your User model, and are free to use find_or_create_by! fearlessly if you so choose!
You can set a validation to be case-insensitive by using:
class Person < ActiveRecord::Base
validates :name,
presence: true,
uniqueness: { case_sensitive: false }
end
However you also need a case-insensitive database index backing it since just using a validation in Rails will lead to race conditions. How to achieve that depends on the RBDMS.
Which leaves the issue of querying. The classic way of performing a intensive search is by WHERE LOWER(name) = LOWER(?). Although Postgres lets you use WHERE ILIKE name = ?.
If you want to encapsulate this into the model which is a good idea you would create a scope:
class Person
scope :find_by_name, lambda{ |name| where('LOWER(name) = LOWER(?)', name) }
end
However, you cannot use .find_or_create_by! in this case as the query not just a hash. Instead you would call .first_or_create.
Person.find_by_name("John Citizen").first_or_create(attrs)
see also
PostgreSQL: How to make "case-insensitive" query
The problem is the find_or_create_by and similar methods are already not tansforming the name... as you say there is no record "john citizen" but to work properly you'd need to titleize it for the find_or_create_by, find_or_create_by!, or find_by
(you don't need this solution for find as that only retrieves record by primary key)
so...
def self.find_or_create_by(options)
super(rectify_options(options))
end
def self.find_or_create_by!(options)
super(rectify_options(options))
end
def self.find_by(options)
super(rectify_options(options))
end
private
def self.rectify_options(options)
options[:name] = (new.name = options[:name]) if options[:name]
options
end

How can I programmatically copy ActiveModel validators from one model to another?

I'm writing a library that will require programmatically copying validations from one model to another, but I'm stumped on how to pull this off.
I have a model that is an ActiveModel::Model with some validation:
class User < ActiveRecord::Base
validates :name, presence: true
end
And another model that I'd like to have the same validations:
class UserForm
include ActiveModel::Model
attr_accessor :name
end
Now I'd like to give UserForm the same validations as User, and without modifying User. Copying the validators over doesn't work, because ActiveModel::Validations hooks into callbacks during the validation check:
UserForm._validators = User._validators
UserForm.new.valid?
# => true # We wanted to see `false` here, but no validations
# are actually running because the :validate callback
# is empty.
Unfortunately, there doesn't seem to be an easy way that I can see to programmatically give one model another's validation callbacks and still have it work. I think my best bet is if I can ask Rails to regenerate the validation callbacks based on the validators that are present at a given moment in time.
Is that possible? If not, is there a better way to do this?
Checking into the code of activerecord/lib/active_record/validations/presence.rb reveals how this can be achieved:
# File activerecord/lib/active_record/validations/presence.rb, line 60
def validates_presence_of(*attr_names)
validates_with PresenceValidator, _merge_attributes(attr_names)
end
So I guess I would try to hook into validates_with with an alias_method
alias_method :orig_validates_with :validates_with
Now you have a chance to get ahold of the values passed, so you can store them somewhere and retrieve them when you need to recreate the validation on UserForm
alias_method :orig_validates_with, :validates_with
def validates_with(*args)
# save the stuff you need, so you can recreate this method call on UserForm
orig_validates_with(*args)
end
Then you should be able to just call UserForm.validates_with(*saved_attrs). Sorry this is not something you can just copy/paste, but this should get you started. HTH

Creating 2 objects with has_one association in Rails

I need to get some info about creating new objects in Rails with validation. For example, there is the following code:
def create
#user = User.new(params[:user])
if #user.save
# some actions: redirect, render, etc
else
render 'new'
end
end
But if there is 2 models with has_one association, for example Club and Place. I need to create both this objects from params in the same 'create' action, because I've got the same form for inputing data for it(params[:club] and params[:club][:place]). I don't know how I should save this objects, because for building a place (#club.build_place(params[:club][:place])) I should save #club in database. Please, give me example of the code for my problem. Thanks in advance.
If you're creating multiple objects from a single form you'd probably be best off putting this logic into a "Form Object"... See the article "7 Patterns to Refactor Fat ActiveRecord Models" from the CodeClimate blog found here (look for Section #3 on extracting Form Objects): http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models.
Railscasts also has a good episode on form objects, though it is a "Pro Episode" (i.e. requires subscription). http://railscasts.com/episodes/416-form-objects
In short, you create a custom model including some of the necessary ActiveModel modules then create a custom save method, e.g. (this is directly from the article which has a lot of great advice).
class Signup
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :user
attr_reader :company
attribute :name, String
attribute :company_name, String
attribute :email, String
validates :email, presence: true
# … more validations …
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
#company = Company.create!(name: company_name)
#user = #company.users.create!(name: name, email: email)
end
end
This gives you much more control and a much cleaner interface.

Rails 3 Extremely Large Model Validation

I am currently working on a Rails 3 application that has a form that takes one parent object with a couple of pre-defined attributes selected from a fairly large array. Each parent object has several child objects that depending on which pre-defined attributes you have selected in the parent, can have WILDLY different min-max values, each with their own separate often unique limitations.
I am quickly realizing that validating these objects is going to result in an extremely large model file. I was wondering if there was a proper way to remove this type of large scale validation from the model(or at least have the model point to somewhere else).
Also a more higher level question, is it normal to have a say... 1000 lines of code to validate the integrity of the data of an object?
1) You can create a custom validator class, which involves inheirting from ActiveModel::Validator and implementing a validate method that takes the records to validate:
class Report > ActiveRecord::Base
validates with MyValidator
end
class MyValidator < ActiveModel::Validator
def validate(record)
record.errors[:base] = << "Error" unless is_valid(record)
end
end
2) In rails 3, there are validation macros as wel, which means extending ActiveModel::EachValidator:
class Report < ActiveRecord::Base
validates :name :report_like => true
end
class ReportLikeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value["Report"]
record.errors.add attribute, "Does not appear to be a ..."
end
end

Resources