Reputation: 21180
I'm very new to Rails, and I'm a little overwhelmed where I do simple things like create an API call. I've set up a route at /reports
which has this controller:
class ReportsController < ApplicationController
@client = # Api-accessing gem
@all_reports = []
def self.request_report
begin
puts "Step 1:"
step1 = @client.request_report(opts = {"max_count" => 1})
step1_result = step1.parse
puts "Done!"
puts step1_result
rescue Excon::Errors::ServiceUnavailable => e
puts "Didn't work"
logger.warn e.response.message
retry
end
end # End request_report
request_report
end
This correctly calls the external API when I first load the /reports
route, but when I refresh the page the code isn't re-run.
Perhaps I'm misunderstanding what controllers are used for? Am I meant to be putting this code somewhere else? Or is there a caching issue?
Upvotes: 4
Views: 2727
Reputation: 76784
To add to @max
's excellent answer, you need to appreciate that Rails is based on a stateless protocol (HTTP
)...
each request message can [only] be understood in isolation.
This means that if you want to create a set of controller actions, you have to appreciate that each call is going to create a new instance of your classes etc. This, coupled with the idea of a RESTful set of actions, should give you a basis from which to build your functionality.
--
#config/routes
scope constraints: { subdomain: "api" } do
resources :reports #-> http://api.url.com/reports
end
#app/controllers/reports_controller.rb
class ReportsController < ApplicationController
respond_to :json #-> requires "responders" gem
def index #-> instance method
@reports = Report.all
respond_with @reports #-> all reports
end
def show
@report = Report.find params[:id]
respond_with @report
end
end
I'll leave the service object stuff as I have no experience with it.
--
If you're pulling from an external API, you have several considerations:
- Calls ideally need to be asynchronous (unless you use multi-threading)
- Calls need to be made in the instance method
Your current pattern calls the API on the class, which is why you can't refresh it:
class ReportsController < ApplicationController
@client = # Api-accessing gem
@client
is only invoked (I don't know why it works, as it should be a class variable) with the class.
So if you send a new request
(which creates an instance of ReportsController
), @client
is going to be declared that one time.
To get it working correctly, @client
needs to be defined with each instance method:
class ReportsController < ApplicationController
def index
@client = # Api-accessing gem
This way, each time you invoke ReportsController#index
, a new API call will be made. Might seem trivial, but the data scope is massive.
Finally, you need to read up about MVC
(Model View Controller
):
This will show you how controllers
are meant to be used in Rails applications etc.
Upvotes: 2
Reputation: 102443
The only public API of controller are the actions which respond to a HTTP request. In your case get "/reports" => "reports#request_report"
is a route which corresponds to the action request_report
.
However actions are instance methods, not class methods:
class ReportsController
def request_report # self.request_report would make this a class method!
# @todo get reports from somewhere and
# return some sort of response.
end
# any method call here happens when the class is evaluated.
end
You are declaring the action as a class method and then calling it when the ReportsController class is evaluated. Sorry to say but just about everything about your controller is wrong.
The Rails convention would be to call the action index
.
Controllers in Rails should only be instantiated by the router (or your test framework). So they are definatly the wrong place to put resuable bits and bobs. If you ever see someone doing ReportsController.new.foo
or ReportsController.foo
- fire them on the spot.
If its a pretty trivial one-off you can place it in private method in your controller.
Some place API calls on the model layer - however that is debatable since ActiveRecord models already are supercharged to the gills with powers and responsibilities.
One solution that has worked well for me is Service Objects. They are easy to test and have a clear single responsibility.
class RequestReportService
def initalize(client)
@client = client
end
def call(opts = {})
begin
return @client.request_report(opts.merge("max_count" => 1))
rescue Excon::Errors::ServiceUnavailable => e
nil
end
end
end
class ReportsController
def index
@reports = RequestReportService.new(@client).call
end
end
Upvotes: 8
Reputation: 97
Well I actually never seen anyone code like this in a rails controller. Rails is a mvp framework. Controller are use to negotiate between your model and the views. First of all, if you routed correctly to your controller like
get "/reports" => "request_report#reports"
your controller should have a method like the following
def request_report
@client = Client.find(params[:id])
end
And then the controller will render and display the view in your app/views/reports/request_report.html.erb with access to the @client variable you just search from your database.
I am not sure why you are calling the block request_report at the bottom of the page, it just doesn't make sense in a controller. And you certainly don't really need to write self in front of a controller method.
def self.request_report
your code
end
As for where to put your api controller, usually for an api controller, we can create new folders under controllers, so the structure will be like
app/controllers/api/v1/your_api_controller.rb
Then in your_api_controller.rb you will need to add namespace infront of your controller like this.
class Api::V1::ReportsController < ActionController::Base
end
It is the same with your routes, you will add namespace in your route.rb
namespace :api do
namespace :v1 do
get "/reports" => "request_report#reports"
end
end
Upvotes: 0