Search code examples
ruby-on-railsapidecorator

How to Decorate a Hash Object from 3rd Party API - Rails


I am consuming an external 3rd party API in my rails application and I'm wondering how I should best handle the response so I can add functionality to each object in the view.

I have a search box that triggers a call to an external museum API. I have a service object handling the call and parsing the response, so I can ensure a consistent structure. I am then presenting the items returned in a listing so that individual items can be imported into the app.

I want to add a thumbnail image for each item in the listing where an image id exists in the API response or add a placeholder where it doesn't. I'm handling this with some logic in the view which I would like to extract out.

   <% @mus_objects.each do |ob| %>
      <div class="row mt-5">
        <div class="col-lg-6">
          <% if ob['_primaryImageId'] %>
            <%= image_tag("https://iiif_image_server_example.com/#{ob['_primaryImageId']}/full/!140,140/0/default.jpg") %>
          <% else %>
            <%= image_tag("https://placehold.it/140x140?text=No+Image") %>
          <% end %>
          <%= ob["_primaryTitle"] %>
          <%= link_to 'Import', new_admin_museum_object_path(ob) %>
        </div>
      </div>
    <% end %> 

This is my controller code:

class Admin::MuseumApiController < ApplicationController

  def search
    @search_term = params[:search_term]
    if params[:searchtype] == 'search_term'
      response = MuseumApiManager::FetchBySearchTerm.call(@search_term)
    elsif params[:searchtype] == 'systemNumber'
      response = MuseumApiManager::FetchBySystemNumber.call(@search_term)
    end
    if response && response.success?
      @mus_objects = response["records"]
    end
  end

end

The response from my service object call looks like this:

#<OpenStruct success?=true, records=[{"systemNumber"=>"O123", "objectType"=>"Bottle", "_primaryTitle"=>"Bottle", "_primaryImageId"=>'1234'}]>

So my @mus_objects used by the view is an array of hashes. I'm not sure what the best approach is for handling this. I'm thinking that something like a Decorator would be appropriate so that I could instead call ob.thumbnail_url within the view but a decorator requires a model.

**** Experimenting with Answer Proposed using ActiveModel ****

I've created an PORO as suggested and brought the search methods in here, although just while I'm testing it our I've left the logic in the ServiceObject.

class MuseumApiObject
  include ActiveModel::Model

  def self.fetch_by_search_term(search_term)
    response = MuseumApiManager::FetchBySearchTerm.call(search_term)
    response["records"]
  end

  def thumbnail_url
    if _primaryImageId
      "https://iiif_example.com/#{_primaryImageId}/full/!140,140/0/default.jpg"
    else
      'https://placehold.it/140x140?text=No+Image'
    end
  end
end

now in the controller I am doing this:

class Admin::MuseumApiController < ApplicationController

  def search
    @search_term = params[:search_term]
    @mus_objects = MuseumApiObject.fetch_by_search_term(@search_term)
  end

end

Now this still just creates an array of hashes. How can I instantiate this array of hashes into a load of MuseumApiObjects? I tried using create with the array but it does not exist.


Solution

  • I prefer to create model objects (not ActiveRecord) for encapsulating domain logic in my code (instead of services). This can be done by including ActiveModel::Model in POROs.

    This strategy allows me to keep all of my business logic in my models, and now not all models need to have an associated table. As a plus, I still get all of the goodness like validations and callbacks, and I can also write my test cases like I would for any other model.

    In your specific use case, I will create a MuseumObject model class (which may also include the logic for API interaction), and also the thumbnail_url method.

    class MuseumObject
      include ActiveModel::Model
    
      attr_accessor :system_number, :object_type, primary_title, primary_image_id
    
      def initialize(attributes)
        self.system_number = attributes['systemNumber']
        ...
      end
    
      def self.search
        ...
      end
    
      def thumbnail_url
        ...
      end
    end
    

    Edit:
    You will need to initialize the object using the hash. You can define attr_accessors for all of your fields, and then you can simply initialize the objects with the hashes.

    Say for example if your array of hashes is stored in museum_hashes variable, you can convert it to an array of your model objects as,

    museum_hashes.map { |hash| MuseumObject.new(hash) }