The Ruby on Rails experts
Logo_144 ClearCove Software, Inc. Rails Recipes

In multi-step operations, you want to be able to provide meaningful feedback to your users if something goes sideways.

Outcome is a great way to shuttle the outcome of a process from the processing method back to the user. An Outcome instance has the following attributes:

  • success: [Boolean] whether process was successful or not.
  • result: [Object] the return value of the process.
  • messages: [Array] an array of messages.

How to use it in your app

In this example we use a PurchaseTransaction Service Object to handle a user's purchase in our online store. Processing a purchase is a fairly complex process that involves several other ActiveRecord models, as well as API calls to 3rd party services. A lot that can go wrong.

So we wrap the entire process into a method and call that single method from our controller. The @outcome instance will contain detailed messaging if something went wrong, and how to fix it.

# app/controllers/purchase_transactions_controller.rb
class PurchaseTransactionsController < ApplicationController

  ...

  def create
    @outcome = PurchaseTransaction.create(params[:purchase])

    respond_to do |format|
      format.html {
        if @outcome.success?
          # Everything went well, display messages as flash notice
          flash[:notice] = @outcome.messages_to_html
          # redirect to new purchase
          redirect_to purchase_path(@outcome.result)
        else
          # Something didn't work. Display messages as flash warning
          flash[:alert] = @outcome.messages_to_html
          # re-render the form
          @purchase = @outcome.result
          render :action => 'new'
        end
      }
    end
  end

  ...

end
# app/models/purchase_transaction.rb
class PurchaseTransaction

  # Creates a new purchase
  def self.create(params)
    # initialize outcome attrs
    l_success = true
    l_messages = []
    # initialize AR model
    new_purchase = Purchase.build(params)
    # ... more complex processing:
    l_outcome = make_api_request_for_product_type(new_product.product_type)
    if l_outcome.success?
      # ... do more processing on success
      l_messages += l_outcome.messages
      new_purchase.save
    else
      # stop processing, set success flag and collect error messages.
      l_success = false
      l_messages += l_outcome.messages
    end
    # Return an instance of Outcome
    Outcome.new(l_success, new_purchase, l_messages)
  end

end

Show me the code

This is all there is to the Outcome.rb class:

class Outcome

  attr_reader :success, :result, :messages

  # Initializes an Outcome instance
  # @param[Boolean] success indicates if process was successful.
  # @param[Object] result the return value of the process. Anything you want to return
  # @param[Array<String>, optional] messages messages as Array of strings, optional
  def initialize(success, result, messages = [])
    @success, @result, @messages = success, result, messages
  end

  def success?
    @success
  end

  def messages_to_html
    @messages.join('<br/>').html_safe
  end

end

Credits

This library is inspired by Mutations.