Home > Software engineering >  Handling validation errors on delegatee with delegated types
Handling validation errors on delegatee with delegated types

Time:09-28

I am having an issue understanding how to use Rails' delegated types when it comes to validations failing on the delegatee.

Having the following code

inbox.rb

class Inbox < ApplicationRecord
    delegate :name, to: :inboxable
    delegated_type :inboxable, types: %w[ Mailbox Messagebox ], dependent: :destroy
end

class Mailbox < ApplicationRecord
  include Inboxable

  belongs_to :inbox_domain

  validates :alias, presence: true, uniqueness: true

  def name
    "#{self.alias}@#{self.domain.name}"
  end
end

messagees_controller.rb

def create
  @mailbox =  Inbox.create inboxable: Mailbox.new(mailbox_params)

  if @mailbox.save
    redirect_to @mailbox.inboxable, notice: "<b>#{@mailbox.name}</b> was created."
  else
    render :new
  end
end

private

def mailbox_params
  params.require(:mailbox).permit(:alias, :inbox_domain_id)
end

When i want to create a mailbox where the alias is already taken, the following error is thrown because Mailbox.new fails the validation

ActiveRecord::NotNullViolation (PG::NotNullViolation: ERROR:  null value in column "inboxable_id" violates not-null constraint
DETAIL:  Failing row contains (13, 2021-09-26 20:48:53.970799, 2021-09-26 20:48:53.970799, Mailbox, null, f).
):

Tried solution

What is the correct way to handle this scenario? I have tried to check explicitly Mailbox.new first, like this:

 mailbox = Mailbox.new(mailbox_params)
   if mailbox.valid?
    @inbox =  Inbox.create inboxable: mailbox
......

While it technically works, it's a mess once you also have to validate attributes on Inbox itself

CodePudding user response:

Use validates_associated to trigger the validations on the associated record:

class Inbox < ApplicationRecord
  delegate :name, to: :inboxable
  delegated_type :inboxable, types: %w[ Mailbox Messagebox ], dependent: :destroy
  validates_associated :inboxable
end

This will add an error ("Inboxable is invalid") to the errors object on this model and prevent saving if the associated mailbox is not valid.

What you want in your controller is:

def create
  # .create both instanciates and saves the record - not what you want here
  @mailbox = Inbox.new(inboxable: Mailbox.new(mailbox_params))
  if @mailbox.save
    redirect_to @mailbox.inboxable, notice: "<b>#{@mailbox.name}</b> was created."
  else
    render :new
  end
end

If you want to display the errors for the associated item you need to access and loop through the errors object on it:

# app/views/shared/_errors.html.erb
<ul>
  <% object.errors.each do |attribute, message| %>
    <li><%= message %>
  <% end %>
</ul>
<%= form_with(model: @inbox) do |form| %>
  <% if form.object.invalid? %>
    <%= render(partial: 'shared/errors', object: form.object) %>
    <% if form.object.inboxable.invalid? %> 
      <%= render(partial: 'shared/errors', object: form.object.inboxable) %>
    <% end %>
  <% end %>

  # ...
<% end %>
  • Related