0

I wanted use Service Object in my Rails API. Inside my Service Object I want to save Model and return true if saving was successful, but if saving was unsuccessful then I want to return false and send error messages. My Service Object looks like that:

class ModelSaver
  include ActiveModel::Validations
  
  def initialize(params)
    @params = params
  end

  def save_model
    model ||= Model.new(params)

    return false unless model.valid?
    model.save!

  rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
    model.errors.add(:base, 'here I want default error message')
    false
  end

  private

  attr_reader :params
end

The problem is that I don't know how to send errors messages in response. When I try to send service_object.errors.messages it displays an empty array to me. It looks like that in Controller:

class ModelController < ApplicationController
...
  def create
    service_object = ModelSaver.new(params)

    if service_object.save_model
       render json: service_object
    else
       render json: { errors: service_object.errors.messages }, status: :unprocessable_entity
    end
  end
...
end

So how can I get Model errors from Service Object inside Controller?

N0ne
  • 134
  • 10

2 Answers2

0

You can solve this by providing methods to your service object that allow returning the model or the errors even after save_model returned with false.

I would change the service object to

class ModelSaver
  include ActiveModel::Validations

  attr_reader :model

  def initialize(params)
    @params = params
  end

  def errors
    @model.errors
  end

  def save_model
    @model ||= Model.new(params)

    return false unless model.valid?
    @model.save!

  rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
    @model.errors.add(:base, 'here I want default error message')
    false
  end

  private

  attr_reader :params
end

and the controller method to

def create
  service_object = ModelSaver.new(params)

  if service_object.save_model
     render json: service_object.model
  else
     render json: { errors: service_object.errors.messages }, status: :unprocessable_entity
  end
end
spickermann
  • 100,941
  • 9
  • 101
  • 131
  • I have one question - why I get default error message (e.g. content: 'can't be blank') even if I rescure exception `ActiveRecord::RecordInvalid` in Service Object and add error message by `@model.errors.add(:base, 'diff message')`? – N0ne Sep 01 '21 at 20:13
  • Because you return when your record is invalid and therefore you never raise the exception. If you always want to raise an exception even on normal validation errors then just delete the line `return false unless model.valid?`. – spickermann Sep 02 '21 at 05:53
0

I would refactor the service object so that it just delegates to the underlying model:

class ModelSaver
  attr_reader :model
  delegate :errors, to: :model
  
  def initialize(**params)
    @model = Model.new(**params)
  end

  def save_model
    if model.save
      true
    else
      model.errors.add(:base, 'you forgot to frobnobize the whatchamacallit')
      false
    end
  end
end
class ModelController < ApplicationController
  def create
    service_object = ModelSaver.new(**params)

    if service_object.save_model
       render json: service_object
    else
       render json: { errors: service_object.errors.messages }, status: :unprocessable_entity
    end
  end
end
max
  • 96,212
  • 14
  • 104
  • 165
  • It works too :) but I don't know why I get default message from model like `:content => 'can't be blank'` even if I rescure exception `ActiveRecord::RecordInvalid` in Service Object and add error message by `@model.errors.add(:base, 'diff message')`? – N0ne Sep 01 '21 at 20:20
  • You're confusing exceptions and ActiveModel validations. Exceptions are language level control flow. Validations are just callbacks that are run when you call .valid? or when you save the record. `rescue ActiveRecord::RecordInvalid` does in no way prevent the validation errors from being added to the errors object on the model. – max Sep 01 '21 at 20:25
  • There are various ways of skipping validations like `.save(false)` but this sounds more like you should ask a separate question and describe carefully what you're trying to acheive. – max Sep 01 '21 at 20:29