I have two data sources: my database and a third party API. The third-party API is the "source of truth" but I want a user to be able to "bookmark" an item from the third-party API which will then persist it in my database.
The challenge I'm facing is displaying both sets of items in the same list without too much complexity. Here's an example:
Item 1 (not bookmarked, from third-party API)
Item 2 (bookmarked, persisted locally)
Item 3 (bookmarked, persisted locally)
Item 4 (not bookmarked, from third-party API)
...etc
I want the view to fetch the list of all items from the controller and have 0 idea of where the items came from, but should only know whether or not each item is bookmarked so that it can be displayed (e.g. so the user can mark an unbookmarked item as bookmarked).
Generics would be one way to solve this in other languages, but alas, Ruby doesn't have generics (not complaining). In Ruby/Rails, what's the best way to wrap/structure these models so the view only has to worry about one type of item (when in reality there are two types behind the scenes?)
I'd suggest coming up with an object that takes care of fetching the items from both the third-party API and your database, the result of such operation would be an array of items that respond to the same methods, no matter where they came from.
Here's an example on how I'd go about it:
class ItemsController < ApplicationController
def index
#items = ItemRepository.all
end
end
In the code above ItemRepository is responsible for fetching items from both the database and the third party API, the view would then iterate over the #items instance variable.
Here's a sample implementation of the ItemRepository:
class ItemRepository
def self.all
new.all
end
# This method merges items from the API and
# the database into a single array
def all
results_from_api + local_results
end
private
def results_from_api
api_items.map do |api_item|
ResultItem.new(name: api_item['name'], bookmarked: false)
end
end
# This method fetches the items from the API and
# returns an array
def api_items
# [Insert logic to fetch items from API here]
end
def local_results
local_items.map do |local_item|
ResultItem.new(name: local_item.name, bookmarked: true)
end
end
# This method is in charge of fetching items from the
# database, it probably would use the Item model for this
def local_items
Item.all
end
end
The final piece of the puzzle is the ResultItem, remember ItemRepository.all will be returning an array containing objects of this type, so all you need to do is store the information that the view needs from each item on this class.
In this example I assume that all the view needs to know about each item is its name and whether it has been bookmarked or not, so ResultItem responds to the bookmarked? and name methods:
class ResultItem
attr_reader :name
def initialize(name:, bookmarked:)
#name = name
#bookmarked = bookmarked
end
def bookmarked?
!!#bookmarked
end
end
I hope this helps :)
PS. Sorry if some of the class names are too generic I couldn't come up with anything better
Related
I'm writing an application where user enters a date, and then the system fetches the historical weather data for that week (I assume that Wednesday is representative for the whole week) from an external API. For certain reasons, I don't want to do live calls for each date - I want to fetch it once and persist on-site.
In Spring, I'd put most of it into a service layer. Since I am new to Rails, I am not sure where to put certain logic, but here's my proposal:
WeatherController
def create
transform date entered by user to Wednesday of the same week.
Check if there is a already record for that date, if not, fetch the JSON from external API.
Parse JSON to Ruby object, save.
Return the weather data.
WeatherModel
validate if the date is indeed Wednesday
validate if entered date is unique
Generally, I wouldn't put the logic in a create action. Even though you're creating something, the user of your site is really asking you to show the weather. The user should be oblivious to where you're bringing the info from and how you're caching it.
Option 1 - Use Rails Caching
One option is to use Rails caching in the show action. Right in that action you will do a blocking call to the API, and then Rails will store the return value in the cache store (e.g. Redis).
def show
date = Date.parse params[:date]
#info_to_show = Rails.cache.fetch(cache_key_for date) do
WeatherAPIFetcher.fetch(date)
end
end
private
def cache_key_for(date)
"weather-cache-#{date + (3 - date.wday)}"
end
Option 2: Non-blocking calls with ActiveJobs
Option 1 above will make accessing the data you already accumulated somewhat awkward (e.g. for statistics, graphs, etc). In addition, it blocks the server while you are waiting for a response from the API endpoint. If these are non-issues, you should consider option 1, as it's very simple. If you need more than that, below is a suggestion for storing the data you fetch in the DB.
I suggest a model to store the data and an async job that retrieves the data. Note you'll need to have ActiveJob set up for the WeatherFetcherJob.
# migration file
create_table :weather_logs do |t|
t.datetime :date
# You may want to use an enumerized string field `status` instead of a boolean so that you can record 'not_fetched', 'success', 'error'.
t.boolean :fetch_completed, default: false
t.text :error_message
t.text :error_backtrace
# Whatever info you're saving
t.timestamps
end
add_index :weather_logs, :date
# app/models/weather_log.rb
class WeatherLog
# Return a log record immediately (non-blocking).
def self.find_for_week(date_str)
date = Date.parse(date_str)
wednesday_representative = date + (3 - date.wday)
record = find_or_create_by(date: wednesday_representative)
WeatherFetcherJob.perform_later(record) unless record.fetch_completed
record
end
end
# app/jobs/weather_fetcher_job.rb
class WeatherFetcherJob < ActiveJob::Base
def perform(weather_log_record)
# Fetch from API
# Update the weather_log_record with the information
# Update the weather_log_record's fetch_completed to true
# If there is an error - store it in the error fields.
end
end
Then, in the controller you can rely on whether the API completed to decide what to display to the user. These are broad strokes, you'll have to adapt to your use case.
# app/controllers/weather_controller
def show
#weather_log = WeatherLog.find_for_week(params[:date])
#show_spinner = true unless #weather_log.fetch_completed
end
def poll
#weather_log = WeatherLog.find(params[:id])
render json: #weather_log.fetch_completed
end
# app/javascripts/poll.js.coffee
$(document).ready ->
poll = ->
$.get($('#spinner-element').data('poll-url'), (fetch_in_progress) ->
if fetch_in_progress
setTimeout(poll, 2000)
else
window.location = $('#spinner-element').data('redirect-to')
)
$('#spinner-element').each -> poll()
# app/views/weather_controller.rb
...
<% if #show_spinner %>
<%= content_tag :div, 'Loading...', id: 'spinner-element', data: { poll_url: poll_weather_path(#weather_log), redirect_to: weather_path(#weather_log) } %>
<% end %>
...
In rails I prefer to create POROs (plan old ruby objects) to handle most of the core logic in my applications. In doing so we can keep our controllers dead simple and our models void of logic that does not pertain to saving data to the database. If you don't work at keeping unnecessary logic out of of our models they will become bloated and extremely hard to test.
The two PORO patterns I use the most are actions and services.
actions normally relate directly to and assist one controller action.
To take your example lets create one. We will create a WeatherCreator class. I like names that are insanely explicit. What does WeatherCreator do you ask? It creates a Weather record, of course!
# app/actions/weather_creator.rb
class WeatherCreator
attr_reader :weather
def initialize(args={})
#date = args.fetch(:date)
#weather = Weather.new
end
def create
build_record
#weather.save
end
private
def build_record
# All of your core logic goes here!
# Plus you can delegate it out to various private methods in the class
#
# transform date entered by user to Wednesday of the same week.
# Check if there is a already record for that date, if not, fetch the JSON from external API.
# Parse JSON to Ruby object, save.
#
# Add necessary data to your model in #weather
end
end
Then in our controller we can use the action class
# app/controllers/weather_controller.rb
class WeatherController < ApplicatonController
def create
creator = WeatherCreator.new(date: params[:date])
if creator.create
#weather = creator.weather
render :new
else
flash[:success] = "Weather record created!"
redirect_to some_path
end
end
end
Now your controller is stupid simple.
The great benefit of this is that your testing efforts can focus just on the action logic object and it's interface.
# spec/actions/weather_creator_spec.rb
require 'rails_helper'
RSpec.describe WeatherCreator do
it "does cool things" do
creator = WeatherCreator.new(date: Time.zone.now)
creator.create
expect(creator.weather).to # have cool things
end
end
service objects on the other hand would live in app/services/. The difference is that these objects are used in many places in an app but the same isolation of logic and testing practices apply.
Depending on your app you can create different types of POROS for various purposes as a general service object category can also grow out of control.
To make things clear you can utilize different naming practices. So we could take the WeatherCreator class and instead call it WeatherCreatorAction or Action::WeatherCreator. Some goes with services SomeLogicService or Service::SomeLogic.
Use whatever suites your preferences and style best. Cheers!
I will give you little interesting way to implement easy and interesting way. You can make it like bookmark logic:
For example:
How's bookmark work ? User adds an URL to bookmarks, server saves the data of that bookmark, and when another user tries to add the same URL to bookmark, server not saves URL to bookmark because its duplicated Bookmark. Its just, server finds that bookmark and assigns to that user too. and again again again for all users who tries to add that the same url to bookmark.
Weather:
In your case, all you need is: If user request weather of that city and if you dnt have that data then fetch from api give it to user and save it to DB. and if another will request the same city, now just responding from DB not from 3rd party API. All you need is update the data, when it gets requested.
I want to include the logic from one controller action (submitting recipes a chef is allowed to cook) while also adding extra data to submit and validate (editing a chef's contact information). How would I accomplish this without duplicating all the logic?
Example controllers/actions:
ChefsController
#recipes_allowed_to_cook
cooks have long list of recipes, and here we decide 1-5 recipes of their list that they're allowed to cook at our restaurant
HiringController
#recipes_and_contact_info
Edit/submit recipes via CooksController#recipes_allowed_to_cook logic
while also adding/editing a chef's contact information
i.e. On submit, both recipes and contact info will be validted, but
recipes will be validated using same code as
CooksController#recipes_allowed_to_cook
The problem is, #recipes_allowed_to_cook has many instance variables and two different sections (one for :get and another for :post). I want to be able to use this logic simultaenously as I'm also submitting the data for a chef's contact info, so if either portion has errors we render the #recipes_and_contact_info.
You can use a Service Class:
# lib/controllers_logic/allowed_recipes_computor.rb
class ControllersLogic::AllowedRecipesComputor
attr_reader :chief, :recipes
def initialize(chief)
#chief = chief
end
def execute
#recipes = #chief.recipes.where(some_logic_to_filter_recipes)
self
end
end
Then in your controllers' actions:
def recipes_allowed_to_cook
#chief = Chief.find(params[:id])
computor = ControllersLogic::AllowedRecipesComputor.new(#chief).execute
#recipes = computor.recipes
end
An example: When using kaminari, or any other "paging" mechamism using scopes, you call that scope on the model.
SongsController < ApplicationController
def index
#songs = Song.most_popular_with_covers.page(params[:page]).per(10)
end
end
I'm trying to find out if this is the right place. In pure MVC, I would expect such calls (like limits, orders, and so on) to be handled by the model-layer. To illustrate with an IMHO bad example:
SongsController < ApplicationController
def index
#songs = Song.order_by(:listen_count, :desc).includes(:albums => :covers).page(params[:page]).per(10)
end
end
I am now putting knowledge about how "popularity" is calculated into my controller, my controller needs to know how to fetch the cover image, it needs to know how songs, covers and images are connected and so on. So, to go all the way to the opposite side (skinny controller):
#songs = Song.paged_most_popular_with_covers(params[:page])
Yet that quickly turns into a spagetti of methods on the models (fat models). Lets say some context needs them paged, but only get the top-5, e.g. for a side-bar-widget:
Song.paged_most_popular_with_covers(page: 1, per: 5) #or
Song.paged_top_five_with_covers #or
Song.paged_top_with_covers(amount: 5)
You could extend this behaviour to other concepts as well: fetch a list of resources for a select-field (ordered differently, not paged, pluck only name and id etc.), fetch a list of resources for a sidebar-widget (only need title and id, ordered and limited differently) and so on.
In that case the question becomes broader: "Should the models care about sending back lists, ready to be used in context C for view V, or should models send back dumb lists and have the controller manipulate these?".
After updating object attributes in my db, I can't seem to retrieve the values back in a subsequent method call. At the point I attempt to retrieve the data, I've confirmed the attributes have updated in my db.
def task
update_objects(data)
retrieve_data
end
def update_objects(data)
data.each do |item|
keyword = self.keywords.find_by_description(item.keyword)
keyword.update_attributes(:total_value => item.totalValue.to_f, :avg_revenue_per_transaction => item.revenuePerTransaction.to_f)
end
end
def retrieve_data
keywords = self.keywords # The updated attributes in keywords are nil
# Do stuff with keywords
end
This is because the keywords collection has probably already loaded before you call retrieve_data. However, calling find_by_description doesn't make use of the loaded collection (since it's using the query API) and fetches a new object from the database directly which you are updating. The original loaded collection doesn't know about this. There are several ways to fix this:
You can call reload to refresh the collection from the database:
def retrieve_data
keywords = self.keywords(true)
end
Or don't use the query API, but rather use the collection itself:
keyword = self.keywords.detect{|k| k.description == item.keyword}
Or simply avoid loading the collection in advance.
I'm trying to add a string to the user model under a location column, based on the user's location. I have everything setup to the point that I know the value of #city+#state is added to the appropriate column in the correct model. The problem is, it appears that request.location.city and request.location.state function properly in the controller and views, but not in the model.
def add_location
#city = request.location.city
#state = request.location.state
#location = #city+#state
self.location = #location
end
When a user is created, rather than creating a string such as "losangelescalifornia", nothing is created. When I define #city = "bob" and #state = "cat", all users created have "bobcat" in the appropriate place. I know then that everything is functioning except these geolocation based methods. So my question is, how would I get these methods (correct me please if that is not what they are) to function in the model, being request.location.city and request.location.state? Many thanks in advance :)
I agree with Rudi's approach, mostly, but I'll offer a little more explanation. The concept you're wrestling with is MVC architecture, which is about separating responsibilities. The models should handle interaction with the DB (or other backend) without needing any knowledge of the context they're being used in (whether it be a an HTTP request or otherwise), views should not need to know about the backend, and controllers handle interactions between the two.
So in the case of your Rails app, the views and controllers have access to the request object, while your models do not. If you want to pass information from the current request to your model, it's up to your controller to do so. I would define your add_location as follows:
class User < ActiveRecord::Base
def add_location(city, state)
self.location = city.to_s + state.to_s # to_s just in case you got nils
end
end
And then in your controller:
class UsersController < ApplicationController
def create # I'm assuming it's create you're dealing with
...
#user.add_location(request.location.city, request.location.state)
...
end
end
I prefer not to pass the request object directly, because that really maintains the separation of the model from the current request. The User model doesn't need to know about request objects or how they work. All it knows is it's getting a city and a state.
Hope that helps.
request variable is not available in the model since it depends on the current HTTP request.
You have to pass to model as param.
def your_action
...
#model.add_location(request)
...
end
def add_location(request)
..
end