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.
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_accessor
s 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) }