ActiveRecord: Extending validation
Recall the Ruby Extending Hash post for inspecting a hash. Hash is a core Ruby class and we extending via a MonkeyPatch. With ActiveRecord we can take a more robust path by extending ActiveSupport::Concern.
Wait. Time out. Hold the bus. Why can’t we get what we need from regular old ActiveRecord?
Ok lets discuss Model Validation. Frequently invalid Model objects wreak havoc on applications and can even manifest itself as a 500 error. Typically this involves the following:
find the culprit we scour logs and find User with id 14 is our problem.
:001 > user = User.find(14)
=> #<User id: 14, email: nil, encrypted_password: "$2a$10$y.YqQ7PxjOOEoMD5buWKBetqcZOhiY6kzQ0QweUlozo...", job_function_id: 7, reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: "2016-11-09 22:53:34", current_sign_in_ip: nil, last_sign_in_ip: "124.5.92.99", confirmation_token: nil, confirmed_at: "2017-01-06 19:01:10", confirmation_sent_at: nil, unconfirmed_email: nil, first_name: nil, last_name: nil, title: "Senior Implementation Officer", company_id: 3, phone: "1-302-245-4427", account_confirmation_link: nil, password_reset_link: nil, account_confirmation_link_date: nil, password_reset_link_date: nil, active: true, inactive_date: nil, created_at: "2016-09-16 00:00:00", updated_at: "2017-01-06 19:01:29", failed_attempts: 0, unlock_token: nil, locked_at: nil>
Now lets check validity (it should not be valid):
:002 > user.valid?
=> false
Finally lets git to the heart of the matter - check the error messages:
:003 > user.errors.messages
=> {:email=>["is required."], :first_name=>["is required."], :last_name=>["is required."]}
Ok now lets go behind the curtain, extend ActiveRecord with some useful methods - we will explain later (promise!) and try this process in a different manner: like a clear presentation of the error messages, effected attributes and for grins lets look at all the object attributes:
:004 > user.issues
========= validation errors: =========
{
"email": "is required.",
"first_name": "is required.",
"last_name": "is required."
}
========= attributes: =========
{
"account_confirmation_link": null,
"account_confirmation_link_date": null,
"active": true,
"company_id": 3,
"confirmation_sent_at": null,
"confirmation_token": null,
"confirmed_at": "2017-01-06T19:01:10.935Z",
"created_at": "2016-09-16T00:00:00.000Z",
"current_sign_in_at": null,
"current_sign_in_ip": null,
"email": null,
"encrypted_password": "$2a$10$y.YqQ7PxjOOEoMD5buWKBetqcZOhiY6kzQ0QweUlozoItORxOF54W",
"failed_attempts": 0,
"first_name": null,
"id": 14,
"inactive_date": null,
"job_function_id": 7,
"last_name": null,
"last_sign_in_at": "2016-11-09T22:53:34.000Z",
"last_sign_in_ip": "124.5.92.99",
"locked_at": null,
"password_reset_link": null,
"password_reset_link_date": null,
"phone": "1-302-245-4427",
"remember_created_at": null,
"reset_password_sent_at": null,
"reset_password_token": null,
"sign_in_count": 0,
"title": "Senior Implementation Officer",
"unconfirmed_email": null,
"unlock_token": null,
"updated_at": "2017-01-06T19:01:29.572Z"
}
=> "=========== for display only ============="
Got everything we need with just user.issues. Clear error messages and an alpha listing of attributes. This can be useful with your boss breathing down you neck for the bug fix he promised yesterday. Ask for a raise. You deserve it. Concluding…here is the ActiveRecord extension that would go off the application root in the lib directory:
cmodule ActiveRecordExtension
extend ActiveSupport::Concern
# add your instance methods here
def issues
unless self.valid?
message_text = "\n ========= validation errors: =========\n"
msg = {}
self.errors.messages.each do |key, value|
msg[key.to_sym] = value.first.to_s
end
message_text += JSON.pretty_generate(JSON.parse(msg.sort.to_h.to_json))
message_text += "\n ========= attributes: =========\n"
message_text += "#{JSON.pretty_generate(JSON.parse(self.attributes.sort.to_h.to_json))}\n"
puts message_text
"=========== for display only ============="
end
end
def _issues
unless self.valid?
message_text = "\n ========= validation errors: =========\n"
msg = {}
self.errors.messages.each do |key, value|
msg[key.to_sym] = value.first.to_s
end
message_text += JSON.pretty_generate(JSON.parse(msg.sort.to_h.to_json))
message_text += "\n ========= attributes: =========\n"
message_text += "#{JSON.pretty_generate(JSON.parse(self.attributes.sort.to_h.to_json))}\n"
end
end
# add your static(class) methods here
module ClassMethods
end
end
# include the extension
ActiveRecord::Base.send(:include, ActiveRecordExtension)