Rails API Versioning, Routing issue - ruby-on-rails

I'm trying to implement a simple rails api app with versioning, based on Railscasts #350 episode. I wanted to access a specific API version of the app by providing the version through Accept header in this format: application/vnd.rails5_api_test.v1. And when no Accept header is provided the request will be routed to the current default version of the app. To handle this I have created an api_constraints file in my lib directory which is to be required in routes.
I have created two versions of the app v1 and v2 in which, v1 has the Users resource and v2 has Users and Comments resources.
Everything was working as expected, except for when I request URL localhost:3000/comments by passing version 1 through the headers using Postman, I'm getting the response from the comments resource, displaying all the comments. But I'm expecting the response to be status: 404 Not Found, as the comments resource was in version 2 and the requested version is 1.
This is the response from the server:
Started GET "/comments" for 127.0.0.1 at 2016-04-01 20:57:53 +0530
Processing by Api::V2::CommentsController#index as application/vnd.rails5_api_test.v1
Comment Load (0.6ms) SELECT "comments".* FROM "comments"
[active_model_serializers] User Load (0.9ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
[active_model_serializers] Rendered ActiveModel::Serializer::CollectionSerializer with ActiveModelSerializers::Adapter::JsonApi (4.32ms)
Completed 200 OK in 7ms (Views: 5.0ms | ActiveRecord: 1.5ms)
Here are my working files:
Constraints file, lib/api_constraints.rb:
class APIConstraints
def initialize(options)
#version = options[:version]
#default = options[:default]
end
def matches?(req)
req.headers["Accept"].include?(media_type) || #default
end
private
def media_type
"application/vnd.rails5_api_test.v#{#version}"
end
end
Routes file, config/routes.rb:
Rails.application.routes.draw do
require "api_constraints"
scope module: 'api/v1', constraints: APIConstraints.new(version: 1) do
resources :users
end
scope module: 'api/v2', constraints: APIConstraints.new(version: 2, default: true) do
resources :users
resources :comments
end
end
Users controller for v1, api/v1/users_controller.rb:
class Api::V1::UsersController < ApplicationController
def index
#users = User.all
render json: #users, each_serializer: ::V1::UserSerializer
end
end
Users controller for v2, api/v2/users_controller.rb:
class Api::V2::UsersController < Api::V1::UsersController
def index
#users = User.all
render json: #users, each_serializer: ::V2::UserSerializer
end
end
Comments controller for v2, api/v2/comments_controller.rb:
class Api::V2::CommentsController < ApplicationController
def index
#comments = Comment.all
render json: #comments, each_serializer: ::V2::CommentSerializer
end
end
user serializer for v1, user_serializer.rb:
class V1::UserSerializer < ActiveModel::Serializer
attributes :id, :name, :email
end
user serializer for v2, user_serializer.rb:
class V2::UserSerializer < V1::UserSerializer
has_many :comments
end
comments serializer for v2, comment_serializer.rb:
class V2::CommentSerializer < ActiveModel::Serializer
attributes :id, :description
belongs_to :user
end
I have tried removing the default: true option in the routes and then it is working as expected. But I want it to work with default option.
Could anyone please let me know where I'm getting this wrong and also share your thoughts on this approach. If this one is not the best way to do, then guide me through the correct way of implementing it. Thanks in advance for anyone who takes time in helping me out. :) Cheers!

I don't think this can be solved easily as the v1 doesn't have comments the v2 will be matched regardless.
Is you APIConstraints using this method?
def matches?(req)
#default || req.headers['Accept'].include?("application/vnd.example.v#{#version}")
end
I think the method is a bit too loose here and should look like this to ignore requests that do have a version.
def matches?(req)
(#default &&
req.headers['Accept'].grep(/^application/vnd.example.v\d+/$).empty?
) || req.headers['Accept'].include?("application/vnd.example.v#{#version}")
end

Related

Rails, updating records for an invitation system

I want to allow users to accept invitations, the accept tag is in the invite model itself (so I need to update the table). So far nothing occurs when the user clicks the accept button
View
<% #invites.where(user_id: current_user.id).find_each do |invite| %>
...
<%= button_to "Accept", accept_invite_invites_path(invite), method: :put %>
end
Routes
resources :invites do
collection do
get 'accept_invite'
end
end
Controller
def accept_invite
#invite = Invite.find(params[:id])
#invite.accept
end
def decline_invite
#invite = Invite.find(params[:id])
#invite.decline
end
def set_invites
#invite = #story.invites.find(params[:id])
end
def new
#invite = #story.invites.new
end
I get "undefined method `invites' for nil:NilClass" if I keep :update as a part of set_invites, removing update allows my code to run, but no changes to the database is made.
Model
def accept
accept = true
save
end
def decline
accept = false
save
end
Console
Processing by InvitesController#update as
Parameters: {"authenticity_token"=>"BDle9fqXHT9ZFctMbO4RvxfPuTQXe2Nq+b6/T29B3xjpYdtMozVUFLiRlaQFtuYzMrBceTQn8OtfGjJTe4wa/Q==", "id"=>"accept_invite"}
User Load (1.7ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 ORDER BY `users`.`id` ASC LIMIT 1
No template found for InvitesController#update, rendering head :no_content
Completed 204 No Content in 85ms (ActiveRecord: 1.7ms)
It's weird because the database is selecting from the user table rather than updating the invites table
So what is the problem? Is the route faulty? My set_invites method?
So what is the problem? Is the route faulty? My set_invites method?
Yes,your route is faulty. As I can see you declared your route on a collection, but you need it on a member. And also you should change it to put.
resources :invites do
member do
put 'accept_invite'
end
end

using strong parameters for a server-side autocomplete resource

playing around with this railscast, except I'm using Rails 5 where attr_accessible is deprecated. VenueSuggestion is a resource for suggesting venues in the db as user types something in the related field of a form. the problem I'm having right now is that as I start typing things that match db contents, there are no search results.
the model:
class VenueSuggestion < ApplicationRecord
# should anything go in place of attr_accessible?
def self.terms_for(prefix)
suggestions = where("term like ?", "#{prefix}_%")
suggestions.order("popularity desc").limit(10).pluck(:term)
end
def self.index_venues
Venue.find_each do |venue|
index_term(venue.name)
index_term(venue.address)
venue.name.split.each { |t| index_term(t) }
end
end
def self.index_term(term)
where(term: term.downcase).first_or_initialize.tap do |suggestion|
suggestion.increment! :popularity
end
end
end
the controller:
class VenueSuggestionsController < ApplicationController
#is this right?
def create
VenueSuggestion.create(params[:venue_suggestion])
end
def index
render json: VenueSuggestion.terms_for(params[:term])
end
# is this right?
private
def venue_suggestion_params
params.require(:venue_suggestion).permit(:term, :popularity)
end
end
the rake task:
namespace :venue_suggestions do
desc "Generate venue suggestions for event form"
task :index => :environment do
VenueSuggestion.index_venues
end
end
what the log shows:
Started GET "/venue_suggestions?term=sp" for ::1 at 2016-05-25 21:27:31 -0400
Processing by VenueSuggestionsController#index as JSON
Parameters: {"term"=>"sp"}
(1.4ms) SELECT "venue_suggestions"."term" FROM "venue_suggestions" WHERE (term like 'sp_%') ORDER BY popularity desc LIMIT $1 [["LIMIT", 10]]
[active_model_serializers] Rendered ActiveModel::Serializer::CollectionSerializer with ActiveModelSerializers::Adapter::Attributes (0.06ms)
Completed 200 OK in 4ms (Views: 0.6ms | ActiveRecord: 1.4ms)
The strong params features provides an interface for protecting attributes from end-user assignment. This makes Action Controller parameters forbidden to be used in Active Model mass assignment until they have been whitelisted.
class PeopleController < ActionController::Base
# Using "Person.create(params[:person])" would raise an
# ActiveModel::ForbiddenAttributes exception because it'd
# be using mass assignment without an explicit permit step.
# This is the recommended form:
def create
Person.create(person_params)
end
# This will pass with flying colors as long as there's a person key in the
# parameters, otherwise it'll raise an ActionController::MissingParameter
# exception, which will get caught by ActionController::Base and turned
# into a 400 Bad Request reply.
def update
redirect_to current_account.people.find(params[:id]).tap { |person|
person.update!(person_params)
}
end
private
# Using a private method to encapsulate the permissible parameters is
# just a good pattern since you'll be able to reuse the same permit
# list between create and update. Also, you can specialize this method
# with per-user checking of permissible attributes.
def person_params
params.require(:person).permit(:name, :age)
end
end
Keep in mind that you will need to specify term inside venue_suggestions with something like
params = ActionController::Parameters.new(venue_suggestion: { term: "something" })
Source: http://edgeapi.rubyonrails.org/classes/ActionController/StrongParameters.html

respond_to :json ActionController::UnknownFormat

I am adding an api to an existing rails app that is not working like I want it to. I had the error below that I fixed with the fix below. I am looking for a proper explanation of why my original code was not working (was it working in older versions of rails? I'm on 4.2). I understand why the fix is working but does it have any drawbacks to the original code.
My route:
namespace :api do
namespace :v1 do
resources :users, :only => [:show]
end
end
My controller:
class Api::V1::UsersController < ApplicationController
respond_to :json
def show
respond_with User.find(params[:id])
end
end
My error:
ActionController::UnknownFormat
Processing by Api::V1::UsersController#show as HTML
Parameters: {"id"=>"1"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
The fix:
def show
#user = User.find(params[:id])
render :json => #user
end
My guess is that it was not clear from the request url and header parameters which format your application should return.
In your first version the request url should end with .json or the request should have a HTTP Accept header with the value application/json. Otherwise there is no way to tell that this was a request that should return JSON data.
In your second version you just say: Hey, return this as JSON, no matter what format the request has.
there was no bug you requested /api/v1/users/1 which defaults to look for html.
You could make your intention clear calling: /api/v1/users/1.json or in your routes:
namespace :api, defaults: { format: :json } do
This is mean that you make request in unsupported format (in your case not in JSON). Look at this code for more details.
I think you forget to specify Accept header or add .json suffix at the end of your URL.

Rails: How to read the value of a select without a form

I'm running into a perplexing issue that I can only resolve partway, and hopefully, someone more experienced can tell me whether I can achieve what I wish, or if I'm barking up the wrong tree.
I have a Rails 4 application which uses Devise and CanCan. I'd like to make a small subset of application functionality available to guest users (not logged in). I can achieve this by specifying a get route to a controller method and using link_to to reach that method. I cannot, however, figure out how to get the value of a select box to pass along as parameters on that page without making that view a form using form_tag (there is no model associated with this view).
I can pass hardcoded params along like so:
<%= link_to "Month", activities_search_month_path(:resource_id => 4) %>
but I'd rather have something like:
<%= link_to "Month", activities_search_month_path(:foo => :resource_id) %>
where the second symbol refers to the value of a select_tag. This second example delivers a literal value of "resource_id" when I dump the :foo key unless I convert my view to a form.
If I turn the view into a form by enclosing all the erb in a form_tag, I get a 401 Forbidden error, after which the Devise sign in form is rendered. My guess is that any time you want to process a form, Rails (or Devise) demands authentication on some level. The behavior is the same when I use button_to rather than link_to, since button_to wraps itself in a form under the covers.
How can I set that resource_id argument in my link_to, or will I be forced to create a guest user access level and silently log in guest users? It's important for the UX that users can access this functionality with the least amount of effort possible.
Thanks in advance.
Addendum: quick_search method from controller
def quick_search
puts "quick search 0"
if(params[:time_period] == 'today')
#resource = Resource.find(params[:resource_id])
#site = Site.find(params[:site_id])
#time_period_string = "Activities for #{localize_date(Date.today)} at #{#resource.name}, #{#site.name}"
puts "quick search 1"
if user_signed_in?
puts "quick search 2a"
#activities = Activity.where("system_id = ? and start_date = ? and activity_status_id = ? and resource_id = ?", current_system_id, #today, 2, params[:resource_id])
else
puts "quick search 2b"
if(Setting["#{current_subdomain_not_signed_in}.quick_search_guest_access"] == 'true')
puts "quick search 3a"
current_system_id = current_system_id_not_signed_in
#activities = Activity.where("system_id = ? and start_date = ? and activity_status_id = ? and resource_id = ?", current_system_id, #today, 2, params[:resource_id])
else
puts "quick search 3b"
redirect_to '/users/sign_in'
end
end
end
Note: the quick_search method is never entered. CanCan (or maybe Devise) steps in immediately and redirects to sign in:
Console output:
Started GET "/activities/quick_search" for 127.0.0.1 at 2015-04-12 18:01:58 -0700
Processing by ActivitiesController#quick_search as HTML
(0.2ms) SELECT DISTINCT "systems"."subdomain" FROM "systems"
Completed 401 Unauthorized in 1ms
Started GET "/users/sign_in" for 127.0.0.1 at 2015-04-12 18:01:58 -0700
Processing by Devise::SessionsController#new as HTML
(0.2ms) SELECT DISTINCT "systems"."subdomain" FROM "systems"
Rendered layouts/_header.html.erb (0.8ms)
Rendered devise/shared/_links.html.erb (4.1ms)
Rendered devise/sessions/new.html.erb within layouts/application (14.7ms)
Rendered layouts/_footer.html.erb (0.0ms)
Completed 200 OK in 285ms (Views: 282.3ms | ActiveRecord: 0.2ms)
Ability.rb
can :quick_search, Activity
can :search_day, Activity
can :search_week, Activity
can :search_month, Activity
The odd thing is that link_to quick_search fails with a 401, but link_to the other three methods works fine -- I just can't get parameters to them dynamically.
If you are using CanCan(Can?) you can define a special ability for guests.
How does your Ability-model look?
Which controller are handling the action that you want to view?
How do you authenticate with CanCan in this controller?
https://github.com/CanCanCommunity/cancancan/wiki/CanCan-2.0
Under the "Defining Abilities" you can see a non-user example.
Fixing CanCan is probably the best option, if you do not want to:
For the part with the link and select box it would be easiest to handle as a form and then handle the redirect in the controller, it could also be done with a remote ajax form.
http://edgeguides.rubyonrails.org/working_with_javascript_in_rails.html
This should work:
<% form_tag Activity, activity_quick_search_path, remote: true do %>
<%= select_tag :resource_id...%>
<%= submit_tag %>
<%end%>
Edit after comments:
The culprit here is(was) an:
before_action :authenticate_user!
Causing Devise to redirect to sign in page.
However, if you have CanCan you shouldn't need the authenticate_user.
Short example:
With only Devise I would do:
class NewsController < ApplicationController
before_action :authenticate_user!
before_action :set_news, except: [ :index, :new ]
def index
#news = News.all
end
def show
end
def new
#news = News.new
end
def edit
end
def create
#news = News.new(news_params)
flash[:notice] = 'News created' if #news.save!
redirect_to #news
end
def update
#news.update! news_params
redirect_to #news
end
def destroy
#news.destroy!
redirect_to News
end
private
def news_params
params.require(:news).permit(some_attributes)
end
def set_news
#news = News.find(params[:id])
end
end
How it looks with CanCanCan:
class NewsController < ApplicationController
load_and_authorize_resource
def index
end
def show
end
def new
end
def edit
end
def create
flash[:notice] = 'News created' if #news.save!
redirect_to #news
end
def update
#news.update! news_params
redirect_to #news
end
def destroy
#news.destroy!
redirect_to News
end
private
def news_params
params.require(:news).permit(some_attributes)
end
end
Which I find super neat 😄
Hope that this can help as well.

Rails: NoMethodError (Undefined method _url for _controller. I can't seem to respond_with json properly after my create. Why?

List item
My config/routes.rb file...
Rails.application.routes.draw do
namespace :api, defaults: {format: 'json'} do
namespace :v1 do
resources :hotels do
resources :rooms
end
end
end
My app/controllers/api/v1/hotels_controller.rb
module Api
module V1
class HotelsController < ApplicationController
respond_to :json
skip_before_filter :verify_authenticity_token
def index
#hotels = Hotel.all
respond_with ({hotels: #hotels}.as_json)
#respond_with(#hotels)
end
def show
#hotel = Hotel.find(params[:id])
respond_with (#hotel)
end
def create
#hotel = Hotel.new(user_params)
if #hotel.save
respond_with (#hotel) #LINE 21
end
end
private
def user_params
params.require(:hotel).permit(:name, :rating)
end
end
end
end
When I go to POST through Postman, my data saves just fine, but I get this NoMethodError. Why is this? The issue seems to be occurring at line 21, which is the respond_with(#hotel) line. Should it not just be responding with json ouput for the newly created hotel, via the show method?
(1.1ms) COMMIT
Completed 500 Internal Server Error in 76ms
NoMethodError (undefined method `hotel_url' for #<Api::V1::HotelsController:0x0000010332df58>):
app/controllers/api/v1/hotels_controller.rb:21:in `create'
Rendered /Users/.rvm/gems/ruby-2.0.0-p451#railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/_source.erb (1.0ms)
Rendered /Users/.rvm/gems/ruby-2.0.0-p451#railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb (1.7ms)
Rendered /Users/.rvm/gems/ruby-2.0.0-p451#railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb (1.4ms)
Rendered /Users/.rvm/gems/ruby-2.0.0-p451#railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb within rescues/layout (31.5ms)
Because your route is in the API + v1 namespace, you actually need to redirect to the api_v1_hotel_url(#hotel) after you successfully create your resource. Of course, this is an API and there is no real redirecting, but the default Rails responder doesn't know that. It also doesn't know about your routing namespaces.
With just the default responder, you would have to do
respond_with :api, :v1, #hotel
So that Rails will build a URL that exists. Alternatively, you can create a custom responder that remove the :location option. Here is the default responder: http://api.rubyonrails.org/files/actionpack/lib/action_controller/metal/responder_rb.html
Reading through the source code for that class is very helpful in understanding respond_with. For example, you don't need to use if record.save before you use respond_with with this Responder. Rails will check if the record saved successfully for you and render a 422 with errors if it failed to save.
Anyway, you can see that the responder sets up a lot of variables in it's initializer:
def initialize(controller, resources, options={})
#controller = controller
#request = #controller.request
#format = #controller.formats.first
#resource = resources.last
#resources = resources
#options = options
#action = options.delete(:action)
#default_response = options.delete(:default_response)
end
If you subclassed this responder, you could make something like this:
class CustomResponder < ActionController::Responder
def initialize(*)
super
#options[:location] = nil
end
end
You can set a controller's responder using responder=:
class AnyController < ActionController::Base
self.responder = CustomResponder
# ...
end
To be clear, let me recap:
When you use respond_with, Rails will try to infer what route to redirect to after a successful create. Imagine you had a web UI where you can create hotels. After a hotel is created, you will be redirected to that hotel's show page in the standard Rails flow. That is what Rails is trying to do here.
Rails does not understand your route namespaces when inferring the route, so it attempts hotel_url - a route which does not exist!
Adding symbols in front of the resource will allow Rails to infer the route correctly, in this case api_v1_hotel_url
In an API, you can make a custom responder which just sets the inferred location to nil, since you don't actually need to redirect anywhere with a simple JSON response. Custom responders can also be useful in many other ways. Check out the source code.

Resources