I am trying to implement my own validations in Ruby for practice.
Here is a class Item that has 2 validations, which I need to implement in the BaseClass:
require_relative "base_class"
class Item < BaseClass
attr_accessor :price, :name
def initialize(attributes = {})
#price = attributes[:price]
#name = attributes[:name]
end
validates_presence_of :name
validates_numericality_of :price
end
My problem is: the validations validates_presence_of, and validates_numericality_of will be class methods. How can I access the instance object to validate the name, and price data within these class methods?
class BaseClass
attr_accessor :errors
def initialize
#errors = []
end
def valid?
#errors.empty?
end
class << self
def validates_presence_of(attribute)
begin
# HERE IS THE PROBLEM, self HERE IS THE CLASS NOT THE INSTANCE!
data = self.send(attribute)
if data.empty?
#errors << ["#{attribute} can't be blank"]
end
rescue
end
end
def validates_numericality_of(attribute)
begin
data = self.send(attribute)
if data.empty? || !data.integer?
#valid = false
#errors << ["#{attribute} must be number"]
end
rescue
end
end
end
end
Looking at ActiveModel, you can see that it doesn't do the actual validation when validate_presence_of is called. Reference: presence.rb.
It actually creates an instance of a Validator to a list of validators (which is a class variable _validators) via validates_with; this list of validators is then called during the record's instantiation via callbacks. Reference: with.rb and validations.rb.
I made a simplified version of the above, but it is similar to what ActiveModel does I believe. (Skipping callbacks and all that)
class PresenceValidator
attr_reader :attributes
def initialize(*attributes)
#attributes = attributes
end
def validate(record)
begin
#attributes.each do |attribute|
data = record.send(attribute)
if data.nil? || data.empty?
record.errors << ["#{attribute} can't be blank"]
end
end
rescue
end
end
end
class BaseClass
attr_accessor :errors
def initialize
#errors = []
end
end
EDIT: Like what SimpleLime pointed out, the list of validators will be shared across and if they are in the base class, it would cause all the items to share the attributes (which would obviously fail if the set of attributes are any different).
They can be extracted out into a separate module Validations and included but I've left them in in this answer.
require_relative "base_class"
class Item < BaseClass
attr_accessor :price, :name
##_validators = []
def initialize(attributes = {})
super()
#price = attributes[:price]
#name = attributes[:name]
end
def self.validates_presence_of(attribute)
##_validators << PresenceValidator.new(attribute)
end
validates_presence_of :name
def valid?
##_validators.each do |v|
v.validate(self)
end
#errors.empty?
end
end
p Item.new(name: 'asdf', price: 2).valid?
p Item.new(price: 2).valid?
References:
presence.rb
with.rb
validators.rb
class variable _validators
First, let's try to have validation baked into the model. We'll extract it once it's working.
Our starting point is Item without any kind of validation:
class Item
attr_accessor :name, :price
def initialize(name: nil, price: nil)
#name = name
#price = price
end
end
We'll add a single method Item#validate that'll return an array of strings representing errors messages. If a model is valid the array will be empty.
class Item
attr_accessor :name, :price
def initialize(name: nil, price: nil)
#name = name
#price = price
end
def validate
validators.flat_map do |validator|
validator.run(self)
end
end
private
def validators
[]
end
end
Validating a model means iterating over all associated validators, running them on the model and collecting results. Notice we provided a dummy implementation of Item#validators that returns an empty array.
A validator is an object that responds to #run and returns an array of errors (if any). Let's define NumberValidator that verifies whether a given attribute is an instance of Numeric. Each instance of this class is responsible for validating a single argument. We need to pass the attribute name to the validator's constructor to make it aware which attribute to validate:
class NumberValidator
def initialize(attribute)
#attribute = attribute
end
def run(model)
unless model.public_send(#attribute).is_a?(Numeric)
["#{#attribute} should be an instance of Numeric"]
end
end
end
If we return this validator from Item#validators and set price to "foo" it'll work as expected.
Let's extract validation-related methods to a module.
module Validation
def validate
validators.flat_map do |validator|
validator.run(self)
end
end
private
def validators
[NumberValidator.new(:price)]
end
end
class Item
include Validation
# ...
end
Validators should be defined on a per-model basis. In order to keep track of them, we'll define a class instance variable #validators on the model class. It'll simply by an array of validators specified for the given model. We need a bit of meta-programming to make this happen.
When we include any model into a class then included is called on the model and receives the class the model is included in as an argument. We can use this method to customize the class at inclusion time. We'll use #class_eval to do so:
module Validation
def self.included(klass)
klass.class_eval do
# Define a class instance variable on the model class.
#validators = [NumberValidator.new(:price)]
def self.validators
#validators
end
end
end
def validate
validators.flat_map do |validator|
validator.run(self)
end
end
def validators
# The validators are defined on the class so we need to delegate.
self.class.validators
end
end
We need a way to add validators to the model. Let's make Validation define add_validator on the model class:
module Validation
def self.included(klass)
klass.class_eval do
#validators = []
# ...
def self.add_validator(validator)
#validators << validator
end
end
end
# ...
end
Now, we can do the following:
class Item
include Validation
attr_accessor :name, :price
add_validator NumberValidator.new(:price)
def initialize(name: nil, price: nil)
#name = name
#price = price
end
end
This should be a good starting point. There're lots of further enhancements you can make:
More validators.
Configurable validators.
Conditional validators.
A DSL for validators (e.g. validate_presence_of).
Automatic validator discovery (e.g. if you define FooValidator you'll automatically be able to call validate_foo).
If your goal is to mimic ActiveRecord, the other answers have you covered. But if you really want to focus on a simple PORO, then you might reconsider the class methods:
class Item < BaseClass
attr_accessor :price, :name
def initialize(attributes = {})
#price = attributes[:price]
#name = attributes[:name]
end
# validators are defined in BaseClass and are expected to return
# an error message if the attribute is invalid
def valid?
errors = [
validates_presence_of(name),
validates_numericality_of(price)
]
errors.compact.none?
end
end
If you need access to the errors afterwards, you'll need to store them:
class Item < BaseClass
attr_reader :errors
# ...
def valid?
#errors = {
name: [validates_presence_of(name)].compact,
price: [validates_numericality_of(price)].compact
}
#errors.values.flatten.compact.any?
end
end
I don't understand the point to implement PORO validations in Ruby. I'd do that in Rails rather than in Ruby.
So let's assume you have a Rails project. In order to mimic the Active Record validations for your PORO, you need to have also 3 things:
Some kind of a save instance method within your PORO (to call the validation from).
A Rails controller handling CRUD on your PORO.
A Rails view with a scaffold flash messages area.
Provided all 3 these conditions I implemented the PORO validation (just for name for simplicity) this way:
require_relative "base_class"
class Item < BaseClass
attr_accessor :price, :name
include ActiveModel::Validations
class MyValidator
def initialize(attrs, record)
#attrs = attrs
#record = record
end
def validate!
if #attrs['name'].blank?
#record.errors[:name] << 'can\'t be blank.'
end
raise ActiveRecord::RecordInvalid.new(#record) unless #record.errors[:name].blank?
end
end
def initialize(attributes = {})
#price = attributes[:price]
#name = attributes[:name]
end
# your PORO save method
def update_attributes(attrs)
MyValidator.new(attrs, self).validate!
#...actual update code here
save
end
end
In your controller you have to manually process the exception (as your PORO is outside ActiveRecord):
class PorosController < ApplicationController
rescue_from ActiveRecord::RecordInvalid do |exception|
redirect_to :back, alert: exception.message
end
...
end
And in a view - just a common scaffold-generated code. Something like this (or similar):
<%= form_with(model: poro, local: true) do |form| %>
<% if poro.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(poro.errors.count, "error") %> prohibited this poro from being saved:</h2>
<ul>
<% poro.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name, id: :poro_name %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
That's it. Just keep it all simple.
Related
I want to have two input fields, but only one column in the database.
The first input is stored data in numbers and the other one is stored data in numbers divided by 24.
You can put data only in one field.
Is there any possible way to do this?
UPD:
Migration:
def change
add_column :employees, :hourly_payment , :integer
end
View:
employees/_form.html.erb
<%= simple_form_for #employee do |form| %>
<%= form.input :name %>
<%= form.input :hourly_payment %>
<%= form.input :monthly_payment %>
<%= form.button :submit, class: "btn btn-success" %>
<% end %>
Your model and database tables are the internals of your application and are not actually tied to the view by anything except how easy ActiveRecord makes it to use convention over configuration to link the two*.
In Rails style MVC the controller is responsible for passing user input to the model. Usually you would just do this with simple mass assignment:
class UsersController < ApplicationController
def create
#user = User.new(user_params)
# ...
end
private
def user_params
params.require(:user)
.permit(:email, :salary)
end
end
This is basically just passing a whitelisted hash of parameters straight to the model as is and it all gets passed to the setters that ActiveRecord magically created for you by reading the database schema.
But there is nothing stopping you from assigning attributes manually:
class UsersController < ApplicationController
def create
#user = User.new(user_params) do |user|
user.salary = calculated_salary
end
# ...
end
private
def user_params
params.require(:user)
.permit(:email)
end
def calculated_salary
if params[:user][:hourly_payment].present?
params[:user][:hourly_payment]
elsif params[:user][:monthly_payment].present?
params[:user][:monthly_payment].to_i / 168
else
0 # sorry no cookies for you
end
end
end
Or monkeying with the parameters object:
def user_params
params.require(:user)
.permit(:email)
.merge(salary: calculated_salary)
end
It is after all just a hash on steroids. The only thing that Rails will prevent you from is passing a parameters object that has not been whitelisted.
There is no stone tablet for what you can do in a controller. The only thing to bear in mind is that controllers are notoriously hard to test and fat controllers are a recipe for disaster.
If you're doing anything more complicated there are better solutions such as form objects, decorators or service objects.
You'll need to create a view for that. Here is an example of migration:
def up
sql = %(CREATE OR REPLACE VIEW my_view_models AS
SELECT m.*,m.int_field*12 as my_new_value from my_models m
)
self.connection.execute(sql)
end
def down
self.connection.execute('DROP VIEW IF EXISTS my_view_models')
end
Then, you can access your value with method my_new_value on your model. You'll need to change the name of the default table matching your model.
class MyModel
self.table_name = 'my_view_models'
end
And access it via
MyModel.first.my_new_value
I'm trying to save a Form Object in Rails through and association like this:
document.translations_forms.save(translation_params)
And on my Document model I associated it this way:
class Document < ApplicationRecord
has_many :translations_forms
...
end
But when I run the first command above, I getting this error:
NoMethodError: undefined method `relation_delegate_class' for Document::TranslationsForm:Class
I tried declaring the TranslationFrom Object adding the Document namespace
class Document::TranslationsForm
include ActiveModel::Model
belongs_to :document
def save(params: {})
return false if invalid?
self.document.translation.create(params)
end
end
But didn't work either, my TranslationForm object is in the app/forms/translations_form.rb directory, and I'm using rails 6, what can I do to associate the model with my form object?
A form object (which is a vague term) is usually just a variation of the Decorator pattern.
So you could simply setup the form object so that it wraps an instance of the model class:
class Document
class TranslationForm
include ActiveModel::Model
attribute_reader :document
def initialize(record = nil, attributes = {})
# lets you use the form object for existing records
if record
#document = record
#document.assign_attributes(attributes)
end
#document ||= Document.new(attributes)
end
def to_model
document
end
def save
# triggers validations on the form object
if valid?
document.save
else
false
end
end
end
end
def create
#document = Document::TranslationForm.new(document_params)
if #document.save
redirect_to #document
else
render :new
end
end
def update
#document = Document::TranslationForm.new(
Document.find(params[:id]),
document_params
)
if #document.save
redirect_to #document
else
render :edit
end
end
To add a validation to the form object (instead of directly to the model) just use delegatation:
class Document
class TranslationForm
# ...
validates :foo, presence: true
delegate :errors, to: :document
delegate :foo, to: :document
end
end
delegate :errors, to: :document makes it so that your validations will add errors to the underlying model instead of the errors object of your form object.
I am battling an error with nested attributes and trying to fix the cop error at the same time. So here is the walk through. A coupon code may be submitted with the form using nested attributes that may affect the price of the job. This only occurs if the coupon code is valid. In this scenario the coupon code has already been assigned so the first if coupon_code && coupon.nil? is triggered. When the form comes back around the flash message works correctly but simple form does not display the value. I could adjust simple form to have the value with an instance variable but I'm starting to smell something a bit off here in my logic. Also, the smell of Assignment Branch Condition is starting to worry me. I can move forward with this, but the user would like to see the code. I would too.
Cop Error:
app/controllers/payments_controller.rb:9:3: C: Assignment Branch Condition size for update is too high. [17.97/15]
Controller:
class PaymentsController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :route_not_found_error
Numeric.include CoreExtensions::Numeric::Percentage
def update
#job = Job.find(params[:job_id])
coupon_code = params[:job][:coupon_attributes][:code]
coupon = validate_coupon(coupon_code)
if coupon_code && coupon.nil?
#coupon_code = coupon_code
flash.now[:error] = t('flash_messages.coupons.id.not_found')
render 'payments/new', layout: 'nested/job/payment'
else
update_job(#job, coupon)
update_coupon(coupon, #job) if coupon
redirect_to #job.vanity_url
end
end
def new
#job = Job.find(params[:job_id])
return if reroute?(#job)
render 'payments/new', layout: 'nested/job/payment'
end
private
def update_job(job, coupon)
job.start_at = DateTime.now
job.end_at = AppConfig.product['settings']['job_active_for_day_num'].days.from_now
job.paid_at = DateTime.now
job.price = price_job(coupon)
# job.save
end
def validate_coupon(coupon_code)
return nil unless coupon_code.present?
coupon = Coupon.active.find_by_code(coupon_code)
return nil unless coupon.present?
coupon
end
def price_job(coupon)
price = AppConfig.product['settings']['job_base_price']
return price unless coupon
price = coupon.percent_discount.percent_of(price)
price
end
def update_coupon(coupon, job)
coupon.job_id = job.id
coupon.executed_at = DateTime.now
coupon.save
end
end
View:
ruby:
content_for :body_id_class, 'PaymentNew'
content_for :js_instance, 'viewPaymentNew'
content_for :browser_title, 'Payment'
job_base_price = AppConfig.product['settings']['job_base_price']
coupon_code = #coupon_code ||= ''
= simple_form_for(#job, url: job_payment_path, html: { id: 'payment-processor-form' }) do |j|
div[class='row']
div[class='col-md-12']
div[class='panel panel-default']
div[class='panel-heading']
h3[class='panel-title']
|Total Cost
div[class='panel-body']
h2[class='job-cost' data-initial = "#{job_base_price}"]
= number_to_currency(job_base_price)
div[class='panel-heading']
h3[class='panel-title']
|Have a coupon?
div[class='panel-body']
div[class='row-inline']
div[class='row-block row-block-one']
= j.simple_fields_for :coupon_attributes, #job.coupon do |c|
= c.input_field :code, maxlength: 50, id: 'coupon-code', class: 'form-control', data: { 'initial' => 0 }, value: coupon_code
div[class='row-block']
button[type='button' class='btn btn-primary' id='coupon-verify' ]
|Verify
p[class='help-hint']
= t('simple_form.hints.coupon.code')
div[class='row']
div[class='col-md-12']
= j.button :button, type: 'button', class: 'btn-primary text-uppercase', id: 'purchase-job' do
= job_posting_button_step_label
Updates
Refactoring this code to work with the post below. Factories fixed factorygirl create model association NoMethodError: undefined method
You have quite a few code smells going on in that fat old controller.
Most of them seem to be symtoms that all is not well on the model layer and that you are not modeling the domain very well.
You might want to consider something like this:
class Job < ActiveRecord::Base
has_many :payments
end
class Payment < ActiveRecord::Base
belongs_to :job
belongs_to :coupon
end
class Coupon < ActiveRecord::Base
validates_uniqueness_of :code
end
This will let our countroller focus on CRUD'ing a single resouce rather than trying to herd a bunch of cats.
So lets look at enforcing the business logic for coupons.
class Payment < ActiveRecord::Base
belongs_to :job
belongs_to :coupon
validate :coupon_must_be_active
attr_writer :coupon_code
def coupon_code=(code)
coupon = Coupon.find_by(code: code)
#coupon_code = code
end
private
def coupon_must_be_active
if coupon
errors[:coupon] << "must be active." unless coupon.active?
elsif #coupon_code.present?
errors[:coupon_code] << "is not valid."
end
end
end
The custom attribute writer loads the coupon from the a code. The validation sets up our business logic rules.
We really should do the same when it comes to the job pricing:
class Job < ActiveRecord::Base
after_initialize :set_price
def set_price
self.price ||= AppConfig.product['settings']['job_base_price']
end
end
class Payment < ActiveRecord::Base
after_initialize :set_price
validates_presence_of :job
def net_price
return job.price unless coupon
job.price * (coupon.percent_discount * 00.1)
end
# ...
end
We can then write our controller like so:
class PaymentsController
before_action :set_job
# GET /jobs/:job_id/payments/new
def new
#payment = #job.payments.new
end
# POST /jobs/:job_id/payments
def create
#payment = #job.payments.create(payment_params)
end
# PATCH /jobs/:job_id/payments/:id
def update
#payment = #job.payments.find(params[:id])
end
private
def set_job
#job = Job.find(params[:job_id])
end
def payment_params
params.require(:payment)
.permit(:coupon_code)
end
end
We can then simply setup the form with:
= simple_form_for([#job, #payment]) do |f|
= f.input :coupon_code
= f.submit
Note that you don't want to take the price from the user unless you intend to implement the honor system - you should get it from your models by setting up association callbacks.
I am using Mongoid(3.0.23) and I want to add nicer URL's, I have followed this rails cast but for some reason my site throws an undefined error for the find_by_slug method. I have read about some gems I could use but it seems pointless for such a simple task.
Model
validates :slug, :uniqueness => true
before_validation :generate_url
def generate_url
self.slug ||= self.title.parameterize if slug.blank?
end
def to_param
slug
end
field :slug
View
<% #events.each do |e| %>
<%= link_to e.title, event_path(e) %>
<% end %>
Controller
def show
#event = Event.find_by_slug!(params[:id])
end
Maybe try:
Event.find_by(slug: params[:id])
Also, not sure if it's necessary but you could specify the type:
field :slug, type: String
Mongoid defines the attribute finder, but not the bang version.
Event.find_by_slug(params[:id])
# => valid
Event.find_by_slug!(params[:id])
# => not defined
In any case, given the way ActiveModel is taking and according to best practices, it's better for you define all the public API of your model.
class Event
def self.find_by_slug!(slug)
where(slug: slug).first || raise(Mongoid::Errors::DocumentNotFound, self, slug: slug)
end
end
You can also re-use find_by_slug, but as I said, because ActiveRecord is deprecating find_by_attribute, I prefer to write the code directly.
I am making a view helper to render set of data in a format. I made these classes
require 'app/data_list/helper'
module App
module DataList
autoload :Builder, 'app/data_list/builder'
##data_list_tag = :ol
##list_tag = :ul
end
end
ActionView::Base.send :include, App::DataList::Helper
helper is
module App
module DataList
module Helper
def data_list_for(object, html_options={}, &block)
builder = App::DataList::Builder
arr_content = []
object.each do |o|
arr_content << capture(builder.new(o, self), &block)
end
content_tag(:ol, arr_content.join(" ").html_safe, html_options).html_safe
end
end
end
end
builder is
require 'app/data_list/column'
module App
module DataList
class Builder
include App::DataList::Column
include ActionView::Helpers::TagHelper
include ActionView::Helpers::AssetTagHelper
attr_reader :object, :template
def initialize(object, template)
#object, #template = object, template
end
protected
def wrap_list_item(name, value, options, &block)
content_tag(:li, value).html_safe
end
end
end
end
column module is
module App
module DataList
module Column
def column(attribute_name, options={}, &block)
collection_block, block = block, nil if block_given?
puts attribute_name
value = if block
block
elsif #object.respond_to?(:"human_#{attribute_name}")
#object.send :"human_#{attribute_name}"
else
#object.send(attribute_name)
end
wrap_list_item(attribute_name, value, options, &collection_block)
end
end
end
end
Now i write code to test it
<%= data_list_for #contracts do |l| %>
<%= l.column :age %>
<%= l.column :contact do |c| %>
<%= c.column :phones %>
<% end %>
<%= l.column :company %>
<% end %>
Every thing is working fine , age , contact , company is working fine. But phones for the contact is not showing.
Does any one have an idea, i know i have missed something in the code. Looking for your help.
Updated question with complete source is enter link description here
There are two issues I can see in the column module.
1) If a block is provided you're setting it to nil - so if block is always returning false. 2) Even if block wasn't nil you're just returning the block as the value, not actually passing control to the block. You should be calling block.call or yielding. Implicit blocks execute faster, so I think your column module should look more like this:
module DataList
module Column
def column(attribute_name, options={})
value = begin
if block_given?
yield self.class.new(#object.send(attribute_name), #template)
elsif #object.respond_to?(:"human_#{attribute_name}")
#object.send :"human_#{attribute_name}"
else
#object.send(attribute_name)
end
end
wrap_list_item(attribute_name, value, options)
end
end
end
The solution is now posted in the discussion.