What is the best way to validate url params that are not in the model.
Specifically I have a route like below:
get 'delivery_windows/:date',
to: 'delivery_windows#index',
:constraints => { :date => /\d{4}-\d{2}-\d{2}/ },
as: :delivery_windows
I want to make sure that :date is a valid date and regex is not a solution. the date cant be in the past and not more than 3 months into the future.
Thank you in advance
Thanks to theunraveler and sadaf2605 for their responses.
I ended up doing to combination of their suggestions by using the before_action and raising a routing error in there.
in my controller I added:
class AngularApi::V1::DeliveryWindowsController < ApplicationController
before_action :validate_date_param, only: :index
def index
...
end
private
def validate_date_param
begin
Date.parse(params[:date])
rescue ArgumentError
render json: [{
param: :date,
message: "Incorrect Date Format: Date format should be yyyy-mm-dd."
}].to_json, status: :unprocessable_entity
return
end
end
end
While I'm not sure that I would handle this in the routing layer myself, you should be able to use Advanced Routing Constraints for this.
The idea is that constraints can accept an object that responds to matches?. If matches? returns true, then the constraint passes, otherwise, the constraint fails. A simple implementation of this would be the following:
In your config/routes.rb, including something like this:
require 'routing/date_param_constraint'
get 'delivery_windows/:date',
to: 'delivery_windows#index',
constraints: DateParamConstraint,
as: :delivery_windows
Then, somewhere in your application (perhaps in lib/routing/date_param_constraint.rb), define a class like the following:
module DateParamConstraint
def matches?(request)
# Check `request.parameters[:date]` to make sure
# it is valid here, return true or false.
end
end
Well you can filter your date at controller, and raise 404 not found when you get date that does not fulfil your requirement.
def show
date=params[:date].strftime("%Y-%m-%d').to_date
if date > 0.day.ago or date > 3.month.from_now
raise ActionController::RoutingError.new('Not Found')
end
end
Related
I am following this tutorial in YT:
https://www.youtube.com/watch?v=oyjzi837wME&list=PLWqjhA7WxVW6L7AWzQElmYfXV3NUv_Lbs&index=1
def update
airline = Airline.find_by(slug: params[:slug])
if airline.update(airline_params)
render json: AirlineSerializer.new(airline, options).serialized_json
else
render json: {error: "airline not "}, status: 422
end
end
But I am getting this error:
NoMethodError (undefined method `update' for nil:NilClass)
Any help would be greatly appreciated.
In controllers you almost always want to use find or find_by! so that an ActiveRecord::RecordNotFound exception is raised and you bail early instead of getting a nil error:
def update
airline = Airline.find_by!(slug: params[:slug])
if airline.update(airline_params)
render json: AirlineSerializer.new(airline, options).serialized_json
else
render json: { errors: airline.errors.full_messages }, status: 422
end
end
After all if the record can't even be found there is no reason to continue processing - which is something the tutorial author didn't consider or test for.
This exception is rescued by Rails by default which sends a 404 response but you can override it on a per controller basis by using rescue_from:
class AirlinesController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :not_found
# ...
private
def not_found
render json: { error: "Oh noes" }, status: :not_found
end
end
On a side note params[:slug] is a bit of an antipattern. Your routes should just stick with the conventional /airlines/:id. :id in this case just means a unique identifier for the resource and not the id column. This avoids the need to refactor if you want to look records up by id or slug.
There are also some other problems with the tutorial code:
Authentication is opt-in as evidenced by before_action :authenticate, only: %i[create update destroy]. Use a opt-out secure by default approach instead so that you don't inadvertantly leave security holes. You do this by adding before_action :authenticate in your ApiController (or whatever the base class is) and then using skip_before_action :authenticate, ... to opt out on endpoints that should not require authentication.
There is no validation of slugs and no guarentee of uniqueness for either the slug or name in the form of unique indexes. Just validating the name doesn't actually guarentee the uniqueness of the slug as the name can be changed but the slug is generated when the record is first created.
parameterize is a quite naive solution to the problem of slugging. For real world use cases you need a library like stringex. Or use FriendlyID if you want to avoid reinventing the wheel.
I have spotted my mistake in following the tutorial. The code in the route should be as below:
namespace :api do
namespace :v1 do
resources :airlines, param: :slug
resources :reviews, only: [:create, :destroy]
end
end
Instead I have put resources :airlines, params: :slug with an s in the param.
So I am not very sure why there is no warning about this error when I tried to run it.
I am rookie in rails restful web service and i am trying build service that returns a json dump of deals.So far my app returns these deals in json format when you hit http://localhost:3000/api/deals. Now i want to add two mandatory parameters(deal_id and title) and two optional parameters in the uri http://localhost:3000/api/deals?deal_id=2&title=book. What is the best way to validate these two mandatory parameters?In other words I just want to do the query only if deal_id and title parameters are present. Assuming Deal model has fields deal_id, title, description and vendor.
Here is my code
Controller
module Api
class DealsController < ApplicationController
respond_to :json
def index
#deals = Deal.all
respond_with (#deals)
end
end
end
routes
namespace :api,:defaults => {format:'json'} do
resources :deals
end
To validate presence of query parameters in a Rails route, you can use the :constraints option. So, in your case, if you want to require the presence of parameters deal_id and title, you can do so by changing:
resources :deals
To:
resources :deals, :constraints => lambda{ |req| !req.params[:deal_id].blank? && !req.params[:title].blank? }
Then, in your controller, you can access all four parameters in the params hash.
Alternatively, if you want to provide more user friendly error messages, you can do validation in the controller. There are a number of approaches. I would probably do something like this:
def action
if params[:deal_id].blank? || params[:title].blank?
flash[:warning] = 'Deal ID and title must be present'
redirect_to root_path and return
end
#rest of your code goes here
end
I'm using Rails 4 with strong parameters to try to find a user by a parameter called "provider_id".
The hope is that I'll be able to make a call with my API to a URL such as:
url.com/api/v1/user?provider=facebook?provider_id=12345
My routes are as follows: routes.rb
namespace :api do
namespace :v1 do
resources :users
match '/:provider/:provider_id', to: 'users#find_by_provider', via: 'get'
end
end
My Strong parameters are:
def user_params
params.require(:user).permit(:name, :age, :location, :provider, :provider_id) if params[:user]
end
My Function is:
def find_by_provider
#user = User.find(params[:provider_id])
respond_to do |format|
format.json { render json: #user }
end
end
Currently, I'm testing with:
url.com/api/v1/facebook/12345
and it is returning:
"{"provider"=>"facebook",
"provider_id"=>"12345"}"
which is good! But I now get the error: "Couldn't find User with id=12345"
Also, somewhat related: occasionally I receive an error that says "param not found: user".
Any suggestions? Thanks!
Change:
#user = User.find(params[:provider_id])
To:
#user = User.find_by(:provider_id => params[:provider_id])
find method will alyways search objects with the id column. Use the where method to search by other criterias/columns
Use:
#user = User.where(provider_id: params[:provider_id]).take
Take a look at http://guides.rubyonrails.org/active_record_querying.html if you want to learn more about the active record query interface.
This is a perfect example where to use find_by! (note the !).
#user = User.find_by!(:provider_id => params[:provider_id])
It works like find_by and returns one User. But if the user is not found it raises an ActiveRecord::RecordNotFound error. That exception is handled by Rails automatically and is turned into a 404 error page.
I am implementing event categories into my app, and now I have encountered the following problem. If the user inputs an existing URL (say, 'site/types/1') he will get to the desired category, however if he enters site/types/asdf (a non-existent URL) he will get an empty index page, and no error. I would like him to get a 404 or an error page, and I know that can be achieved with a bang in a controller, if using a 'find_by!' method (right?). However my query differs and I cant seem to figure out the principle.
How can I make this work with more complex queries that use scopes and lambdas? Thank you
I have the following code:
event controller
def index
if params[:type]
#events_future = Event.future_by_type(params[:type])
else
#events_future = Event.future_events
end
end
event model
scope :future_by_type, lambda { order(...).where(...) }
router
get 'types/:type', to: 'events#index', as: :type
resources :types, except: :show
You can just throw an 404 exception, Rails will redirect to 404.html automatically:
def index
if params[:type]
#events_future = Event.future_by_type(params[:type])
else
#events_future = Event.future_events
end
#complex query here
.......
!#events_future.empty? || raise ActionController::RoutingError.new('Not Found')
end
You could use type in a RESTful way, i.e.
resources :types, only: [:index, :show]
And within your TypesController:
def index
#events = Event.future_events
end
def show
#type = Type.find(params[:id]) # raises RecordNotFound if type doesn't exist
#events = Event.future_by_type(#type)
end
For example, I would like
/apples/123?_format=json
to act like
/apples/123.json
where it renders the *.json.* templates, executes respond_to {|format| format.json {...}}, etc.
Is this at all possible?
Thanks!
You can do the following to disable Rails' automatic handling of the .ext format:
constraints format: false do
resources :apples
# ...
end
Then, and this is a bit gross but I don't see a better way to do this at the moment, you can do the following to update ActionController on what format to serve:
class ApplicationController < ActionController::Base
before_filter :set_format_from_query_string
private
def set_format_from_query_string
request.format = params.fetch(:_format, 'json')
end
end
This will allow your respond_to block to toggle based on the _format query string parameter and uses json as the default format.