A common pattern in a Rails controller action is to
Fetch a resource
Do something to the resource (optional)
Return the resource in a serialized format.
I am looking for a library that abstracts away the first step, so that my controller actions can assume a resource was successfully fetched and avoid checks for exceptional cases.
For example, here is a hypothetical show action:
def show
attrs = params.slice(:handle, :provider)
account = Account.find_by(attrs)
if account
respond_with account
else
head 404
end
end
And what I want is something more like this:
# controller
def show
respond_with resource
end
# some initializer (basically pseudocode)
resource do |params|
attrs = params.slice(:handle, :provider)
Account.find_by(attrs)
end
Where the library would handle returning a 404 if find_by returns nil, or 400 if the provided params are invalid (missing :handle key, include an extra :id key, etc.).
Does anyone know of a library that provides something like this? It is a great use case for a Rack middleware on top of Application.routes.
The gem platformatec/inherited_resources does something very close to this.
Related
I've read several | articles about using params.require(...) in Rails, but nothing that shows them in a non-trivial, real-world scenario.
Specifically, the following URL will be called:
GET http://myapp.example.com/widgets/{clientUuid}
Where {clientUuid} will be a string. I just want to check (from the proper controller action) whether the provided {clientUuid} is non-null and non-empty. I'm wondering if I can just do this:
if params.require(params[:clientUuid]) == null
response = { "error" => "bad client uuid" }
render json: response, status: :bad_request
return
end
And have non-nullness/non-emptiness enforced? If not, what can I do to achieve my desired result?
You're overcomplicating a simple GET request by messing up the route and using a method thats meant for a completely different use.
The idea is that .requires should be used for non-idempotent request methods (POST, PUT,PATCH) where the request contains a body with parameters. It lets you take a single key from the params and whitelist the params contained - which matches the Rails ideom of nesting inputs in a hash with the name of the resource as the root key.
In that case using .requires lets you return a response code to the client that indicates that the request cannot be processed (422 - Unprocessable Entity) as the request body does not have the right structure.
While you could potentially use it creatively on a GET request its wrong from a restful application engineering standpoint. In your case you should be returning a 404 - Not found response code if the clientUuid does not match a record. Usually in rails this is done by using .find which will raise a ActiveRecord::RecordNotFound exception which the framework catches.
Additionally if you have declared the route properly in the first place rails would actually give a 404 automatically as the request would not match if the id segment is missing.
class WidgetsController < ApplicationController
def show
#widget = Widget.find(params[:clientUuid])
end
end
If you want you could bail early so that the database is never queried if the param does not match a condition:
class WidgetsController < ApplicationController
def show
raise ActiveRecord::RecordNotFound if params[:clientUuid].blank?
#widget = Widget.find(params[:clientUuid])
end
end
You can just write:
if params[:clientUuid].blank?
response = { "error" => "bad client uuid" }
render json: response, status: :bad_request
return
end
With params.require it is a bit more difficult, because require raises a ActionController::ParameterMissing exception if the parameter is missing, but allows the parameter to return false (what I guess is still invalid in your example):
begin
uuid = params.require(:cliendUuid)
rescue ActionController::ParameterMissing
# nothing to do, just ensure the exceptions is rescued
end
unless uuid
# handle missing uuid
end
Or:
begin
uuid = params.require(:cliendUuid) || raise ActionController::ParameterMissing
rescue ActionController::ParameterMissing
# handle missing uuid
end
The article you posted re strong parameters is specifically about protecting your database data from user input, usually provided by forms.
params.require(:user).permit(:username)
The above code specifies that for the model User only allow the attribute username to be touched. If you try to update or create a user record in the user table with any other attribute e.g. email, you would get an error because the email attribute has not been 'permitted'. This is what is meant by whitelisting. You will only see the above code in create or update controller methods, or any other method that amends the data in some way. (An exception, of course, is deleting a record).
In your example, the parameter is provided as part of the url which you can also access via the rails provided params hash. However, as your method is not interacting with the db, you don't need to run it through the permit method.
This resource may help.
I have a an json Api who received parameters to create a Device, like name, imei, etc. The Device can have one Blacklist object (has_one :blacklist). I would like to know what's the proper-way to create the blacklist object if a params is present in the post request of Device.
Exemple curl -X POST -d api_key=000000 -d device[name]='stack' -d device[blacklist]='true' https://www.example.com/api/devices.json
In the code for the moment I should have
def create
#device = Device.new
#device.update_attributes(strong_parameters)
if params[:device]['blacklist'] && params[:device]['blacklist'] == true
#blacklist = Blacklist.new(device_id: #device.id)
end
render :device, status: 201 # will render with jbuilder #device and #blacklist
end
But I don't like it that much :
Too much logic in one controller
Verifying parameters inside is a good practice?
If no parameters are given, how to handle the request? I know that strong parameters should return a 400, but what about #device I just created.
This controller smells for me.
Feedbacks welcome.
The result when doing a PATCH
class DevicesController
before_action :found_device, only: :blacklist # get `#device`
before_action :blacklist_device, only: :blacklist
def blacklist
render :device, status: 200
end
private
def blacklist_device
if (params[:device]['blacklisted'] and
params[:device]['blacklisted'] == true and
#blacklist = BlacklistedDevice.create(device_id: #device.id, organisation_id: current_store.organisation.id))
#device.reload
else
render json: { error: "Missing or incorrect 'blacklisted' parameter" }, status: :unprocessable_entity
end
end
end
Too much logic in the conrtoller ? No
I have also heard a lot 'too much logic in the controller is bad' but this is bullshit or rather I believe the words are not accurate enough.
What that phrase means for me, is that for example, model validations should not be in the controller, and the controller should remain light for very basic REST actions. Controller should only be a bridge between the HTML request and the model. Think of it this way : you may have several controllers modifying the same model. What you would write in EVERY controller, should most likely instead be written in the model as a validation.
But here you're dealing with specific requests (transforming a device[blacklist] == true as a Blacklist Model isn't something "natural", so yes in my opinion it should be in the controller.
Plus, a controller action of just 6 lines isn't what we could call "too much logic"
Verifying parameters inside is good Practice ? Yes/No
I assume by that you mean writing specific lines of codes in the controller like if params[xxx] == blabla or something equivalent
The way you did was good. You use specific code only for the special parameter (the blacklist) and the rest of the params go into the model as strong params, so the model validations will do the rest.
Verify parameters only if it's relevant to this particular controller (for example, if it was site-based, you could probably use a different implementation of the blacklist so the difference would have to be in the controller.
If no parameters are given, how to handle the request? I know that strong parameters should return a 400, but what about #device I just created.
This the part I don't quite like about your current implementation. You don't check for the success of your save operations. Here's what you could have written (check the result of every persistence operation result, and render appropriately)
def create
#device = Device.new
if #device.update_attributes(strong_parameters)
if (params[:device]['blacklist']
and params[:device]['blacklist'] == true
and #blacklist = Blacklist.create(device_id: #device.id))
# Handle stuff when everything is cool
render :device, status: 201 # will render with jbuilder #device and
else
# Handle stuff when there's no blacklist param true
end
else
# Handle error on model save
end
end
Inspecting params is well put in the controller - that's it's purpose - the model layer should not have knowledge of request parameters.
But you can put this info in a transient attribute with
class Device
attr_accessor 'create_blacklisted'
end
Then you can create an input field for that new attribute and an after_initialize callback in the Device model as well that can subsequently create the Blacklist entry.
StackOverflow seems to have this style of routes for questions:
/questions/:id/*slug
Which is easy enough to achieve, both in routes and to_param.
However, StackOverflow seems to also redirect to that path when just an ID is passed.
Example:
stackoverflow.com/questions/6841333
redirects to:
stackoverflow.com/questions/6841333/why-is-subtracting-these-two-times-in-1927-giving-a-strange-result/
Same goes for any variation of the slug
stackoverflow.com/questions/6841333/some-random-stuff
Will still redirect to the same URL.
My question is: Is this type of redirection typically handled in the controller (comparing the request to the route) or is there a way to do this in routes.rb?
The reason I wouldn't think this possible in the routes.rb file is that typically, you don't have access to the object (so you couldn't get the slug based off the ID, right?)
For anyone interested, Rails 3.2.13 and also using FriendlyID
Ok, so I think I've got this.
I was looking into doing something with middleware, but then decided that's probably not the place for this type of functionality (since we need to access ActiveRecord).
So I ended up building a service object, known as a PathCheck. The service looks like this:
class PathCheck
def initialize(model, request)
#model = model
#request = request
end
# Says if we are already where we need to be
# /:id/*slug
def at_proper_path?
#request.fullpath == proper_path
end
# Returns what the proper path is
def proper_path
Rails.application.routes.url_helpers.send(path_name, #model)
end
private
def path_name
return "edit_#{model_lowercase_name}_path" if #request.filtered_parameters["action"] == "edit"
"#{model_lowercase_name}_path"
end
def model_lowercase_name
#model.class.name.underscore
end
end
This is easy enough to implement into my controller:
def show
#post = Post.find params[:post_id] || params[:id]
check_path
end
private
def check_path
path_check = PathCheck.new #post, request
redirect_to path_check.proper_path if !path_check.at_proper_path?
end
My || in my find method is because in order to maintain resourceful routes, I did something like...
resources :posts do
get '*id' => 'posts#show'
end
Which will make a routes like: /posts/:post_id/*id on top of /posts/:id
This way, the numeric id is primarily used to look up the record, if available. This allows us to loosely match /posts/12345/not-the-right-slug to be redirected to /posts/12345/the-right-slug
The service is written in a universal fashion, so I can use it in any resourceful controller. I have't found a way to break it yet, but I'm open to correction.
Resources
Railscast #398: Service Objects by Ryan Bates
This Helpful Tweet by Jared Fine
I'm trying to sort out the routing for a multinational web store which only has a presence in certain countries. Each store is different and has a different catalogue of products and there is also a world-wide store for all other countries. I have set up Nginx to prepend the two letter country code from an lookup of the user's IP address so that my Rails app can figure which store to direct the visitor to. I then use Sven Fuch's excellent routing-filter to capture that code, do a lookup against a global SHOPS object and if a match is found then the country code is used, otherwise they get the default (world-wide) store. My routing filter currently looks like this:
module RoutingFilter
class Country < Filter
countries_pattern ||= %r(^/(?i)([a-zA-Z]{2})(?=/|$))
def around_recognize(path, env, &block)
country = "#{extract_segment!(countries_pattern, path)}".upcase
yield(path, env).tap do |params|
params[:shop] = SHOPS.fetch(country.to_sym) || DEFAULT_SHOP
end
end
def around_generate(params, &block)
puts params
shop = params.delete(:shop)
yield.tap do |result|
prepend_segment!(result, shop[:country_code]) if shop
end
end
end
end
Now the curious thing is, the params collection does not contain a :shop param when the around_generate method is executed. My code is directly based on the pagination filter included with the routing-filter gem (I'm not using the locale filter as each of these stores is also multilingual - i18n is handled using accept-language header instead). The original pagination filter by Sven Fuchs looks like this:
module RoutingFilter
class Pagination < Filter
PAGINATION_SEGMENT = %r(/page/([\d]+)/?$)
def around_recognize(path, env, &block)
page = extract_segment!(PAGINATION_SEGMENT, path)
yield(path, env).tap do |params|
params[:page] = page.to_i if page
end
end
def around_generate(params, &block)
page = params.delete(:page)
yield.tap do |result|
append_segment!(result, "page/#{page}") if append_page?(page)
end
end
protected
def append_page?(page)
page && page.to_i != 1
end
end
end
In my filter, shop = params.delete(:shop) results in a Nil object error and I can see from "puts params" that it is indeed not present. Does anyone have any suggestions as to why I'm unable to store and retrieve the :shop param?
Edit: I should mention that I have checked that the parameter gets set correctly in around_recognize - a "puts" of the params collection here does indeed contain the correct :shop object.
If you don't pass a :shop param to url_for (or whatever url generation helper you use here) it won't be passed to around_generate either. around_generate wraps around the url generation part of the routing system.
The Pagination filter assumes the same, e.g. it would be called like blog_posts_path(:page => 2).
But maybe that's not what you want. If you have a look at the Locale filter then this assumes that you sometimes pass a locale to the url helper but sometimes you don't. If :locale is not given it will look it up from I18n.locale which is the current locale set for this request. Maybe you want something similar here?
You could also have a look at the controller's default_url_options. IIRC you can set a default option here, too, so maybe this could work for you. I'm not using this approach anywhere though, so I'm just guessing.
HTH
My rails app produces XML when I load /reports/generate_report.
On a separate page, I want to read this XML into a variable and save it to the database.
How can I do this? Can I somehow stream the response from the /reports/generate_report.xml URI into a variable? Or is there a better way to do it since the XML is produced by the same web app?
Here is my generate_report action:
class ReportsController < ApplicationController
def generate_report
respond_to do |format|
#products = Product.all
format.xml { render :layout => false }
end
end
end
Here is the action I am trying to write:
class AnotherController < ApplicationController
def archive_current
#output = # get XML output produced by /reports/generate_report
# save #output to the database
respond_to do |format|
format.html # inform the user of success or failure
end
end
end
Solved: My solution (thanks to Mladen Jablanović):
#output = render_to_string(:file => 'reports/generate_report.xml.builder')
I used the following code in a model class to accomplish the same task since render_to_string is (idiotically) a protected method of ActionController::Base:
av = ActionView::Base.new(Rails::Configuration.new.view_path)
#output = av.render(:file => "reports/generate_report.xml.builder")
Perhaps you could extract your XML rendering logic to a separate method within the same controller (probably a private one), which would render the XML to a string using render_to_string, and call it both from generate_report and archive_current actions.
What I typically do in this type of situation is to create a separate module/class/model to generate the report (it could even potentially be right in the Product model). This separate component could be in app/models or it could be in lib. In any case, once you have it extracted you can use it anywhere you need it. The controller can call it directly. You can generate it from the console. You can have a cron job generate it. This is not only more flexible, but it also can help smooth out your request response times if the report becomes slow to generate.
Since you are using a template it's understandable that the controller route is convenient, but even if you have to include some kind of ruby templating system in your auxiliary lib, it's still probably going to be less hassle and more flexible then trying to go through the controller.
#output = Product.all.to_xml
I'm sorry, is you question about Xml or about sessions? I mean is the fact that your action generates Xml material to the question? Or do you just want to save the output of the action for latter use?
You said on a "separate" page - you mean on another request? (like after user approved it?)
Why do you want to save the output? Because it should be saved exactly as rendered? (for example user can get frustrated if he clicked to save one report and you saved another)
Or is this thing expensive to generate?
Or may be, I got it wrong and it's about refactoring?