I'm trying to add a simple JSON API to my APP
#routes.rb
namespace :api, :format => :json do
namespace :v1 do
resources :users
end
end
#controllers/api/v1/users_controller.rb
class Api::V1::UsersController < ApplicationController
respond_to :json
def index
respond_with User.all
end
end
#inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'API'
end
rake routes gives me the following:
api_v1_users GET /api/v1/users(.:format) api/v1/users#index
POST /api/v1/users(.:format) api/v1/users#create
new_api_v1_user GET /api/v1/users/new(.:format) api/v1/users#new
edit_api_v1_user GET /api/v1/users/:id/edit(.:format) api/v1/users#edit
api_v1_user GET /api/v1/users/:id(.:format) api/v1/users#show
PATCH /api/v1/users/:id(.:format) api/v1/users#update
PUT /api/v1/users/:id(.:format) api/v1/users#update
DELETE /api/v1/users/:id(.:format) api/v1/users#destroy
When I visit http://localhost:3000/api/v1/users I got the following error:
LoadError in API::V1::UsersController#index Unable to autoload
constant API::V1::UsersController, expected
/path/to/app/controllers/api/v1/users_controller.rb to define it
The error message says exactly what to do. Make sure you use API instead of Api in your controller name.
# == HERE ==
class API::V1::UsersController < ApplicationController
respond_to :json
def index
respond_with User.all
end
end
Or second way declare another acronym, or remove it anyway:
inflections.rb:
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'Api'
end
Related
I've got controller below controller:
module Api
module V1
module Account
class PasswordsController < Devise::PasswordsController
respond_to :json
def create
# some code
end
def update
# some code
end
def is_token_valid
::Account.find_by(reset_password_token: params[:token])
end
end
end
end
end
I want to setup an endpoint for front-end dev where he will check if reset_password_token exist in DB (devise here). I don't know how to made a path like: /api/v1/account/password/is_token_valid
My routes:
namespace :api, defaults: { format: :json } do
namespace :v1 do
namespace :account do
devise_for :accounts, singular: 'account', path: '', controllers: {
sessions: 'api/v1/account/sessions',
registrations: 'api/v1/account/registrations',
confirmations: 'api/v1/account/confirmations',
passwords: 'api/v1/account/passwords',
}
end
resource :account, only: [:show]
EDIT
routes:
root#b2faabb49f91:/usr/src/app# rake routes | grep account
new_account_session GET /api/v1/account/sign_in(.:format) api/v1/account/sessions#new {:format=>:json}
account_session POST /api/v1/account/sign_in(.:format) api/v1/account/sessions#create {:format=>:json}
destroy_account_session DELETE /api/v1/account/sign_out(.:format) api/v1/account/sessions#destroy {:format=>:json}
new_account_password GET /api/v1/account/password/new(.:format) api/v1/account/passwords#new {:format=>:json}
edit_account_password GET /api/v1/account/password/edit(.:format) api/v1/account/passwords#edit {:format=>:json}
account_password PATCH /api/v1/account/password(.:format) api/v1/account/passwords#update {:format=>:json}
I have crafted some nested routes files using the below in my config/initializers:
class ActionDispatch::Routing::Mapper
def draw(routes_name, sub_path=nil)
if sub_path.present?
instance_eval(File.read(Rails.root.join("config/routes/#{sub_path}/#{routes_name}.rb")))
else
instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb")))
end
end
end
However, when used like this in the routes file:
Rails.application.routes.draw do
scope :api do
["v1"].map { |version| draw :base, "api/" + version }
end
end
The routes there do not appear nested as api/users etc. Not sure why the scope is getting ignored there.
EDIT: More Detailed Example
routes.rb
namespace :api, defaults: { format: :json } do
Rails.application.routes.draw do
["v1"].map { |version| draw :base, "api/" + version }
end
end
base.rb
namespace :v1 do
Rails.application.routes.draw do
[:identity].map { |path| draw path, "api/v1"}
end
end
identity.rb
Rails.application.routes.draw do
# Ensures proper namespace when fetching controllers, but does not add to path for routes
scope module: :identity do
namespace :users do
put '/', action: :update
patch '/', action: :update
get 'user_from_token', action: :update
end
end
end
Rails.application.routes.draw was redundant
# config/routes.rb
Rails.application.routes.draw do
scope :api do
["v1"].map { |version| draw :base, "api/" + version }
end
end
# config/routes/api/v1/base.rb
namespace :v1 do
# Rails.application.routes.draw do
[:identity].map { |path| draw path, "api/v1"}
# end
end
# config/routes/api/v1/identity.rb
# Rails.application.routes.draw do
# Ensures proper namespace when fetching controllers, but does not add to path for routes
scope module: :identity do
namespace :users do
put '/', action: :update
patch '/', action: :update
get 'user_from_token', action: :update
end
end
# end
$ rake routes
Prefix Verb URI Pattern Controller#Action
v1_users PUT /api/v1/users(.:format) v1/identity/users#update
PATCH /api/v1/users(.:format) v1/identity/users#update
v1_users_user_from_token GET /api/v1/users/user_from_token(.:format) v1/identity/users#update
rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
I'm working with a web api in rails 4.1.8 and i'm doing some testing but always is returning me
Failure/Error: post '/v1/users', subdomain: 'api', user: #user.to_json, format: :json
ActionController::UrlGenerationError:
No route matches {:action=>"/v1/users", :controller=>"api/v1/users", :format=>:json, :subdomain=>"api", :user=>"JSON_OBJ"}
but if i test it in the browser with Advance Rest Console it works, i don't know what i'm doing wrong
here is me code
routes.rb
namespace :api, path: '/', constraints: {subdomain: 'api'}, defaults: { format: :json } do
namespace :v1 do
with_options except: [:edit, :new] do |except|
except.resources :users do
collection do
post 'login'
post 'showme'
end
end
except.resources :products
except.resources :locations
end
end
end
and my controller spec
module API
module V1
describe UsersController do
before do
request.host = "api.example.com"
expect({:post => "http://#{request.host}/v1/users"}).to(
route_to( controller: "api/v1/users",
action: "create",
subdomain: 'api',
format: :json
)
) # => PASS
# token expectations
#auth_token = allow(JWT::AuthToken).to(
receive(:make_token).and_return("mysecretkey")
)
expect(JWT::AuthToken.make_token({}, 3600)).to eq("mysecretkey")
end
describe "Create User" do
before(:each) do
#user = FactoryGirl.attributes_for :user
end
it 'should return a token' do
post '/v1/users', subdomain: 'api', user: #user.to_json, format: :json # Error
response_body = JSON.parse(response.body, symbolize_names: true)
expect(response_body['token']).to eql "mysecretkey"
end
end
end
end
end
rake routes
login_api_v1_users POST /v1/users/login(.:format) api/v1/users#login {:format=>:json, :subdomain=>"api"}
showme_api_v1_users POST /v1/users/showme(.:format) api/v1/users#showme {:format=>:json, :subdomain=>"api"}
api_v1_users GET /v1/users(.:format) api/v1/users#index {:format=>:json, :subdomain=>"api"}
POST /v1/users(.:format) api/v1/users#create {:format=>:json, :subdomain=>"api"}
api_v1_user GET /v1/users/:id(.:format) api/v1/users#show {:format=>:json, :subdomain=>"api"}
PATCH /v1/users/:id(.:format) api/v1/users#update {:format=>:json, :subdomain=>"api"}
PUT /v1/users/:id(.:format) api/v1/users#update {:format=>:json, :subdomain=>"api"}
DELETE /v1/users/:id(.:format) api/v1/users#destroy {:format=>:json, :subdomain=>"api"}
As described in http://api.rubyonrails.org/classes/ActionController/TestCase/Behavior.html which is referenced in https://www.relishapp.com/rspec/rspec-rails/docs/controller-specs, the first argument to post in your RSpec example is the name of the controller method to be called (i.e. :create in your case), not the route to that method.
I am implementing a RESTful API using RoR. Here my UsersController:
class ApplicationController < ActionController::API
[...]
end
module Api
module V1
class UsersController < ApplicationController
respond_to :json
def create
#user = User.new(user_params)
#user.save
respond_with #user, :location => api_v1_user_url(#user)
end
def show
#user = User.find(params[:id])
respond_with #user
end
end
end
end
If I omit the #user param from :location => api_v1_user_url(#user) in UsersController#create i.e. I have it like so:
respond_with #user, :location => api_v1_user_url
I get the following error:
ActionController::UrlGenerationError in Api::V1::UsersController#create
No route matches {:action=>"show", :controller=>"api/v1/users", :format=>"json"} missing required keys: [:id]
I just added the #user param to api_v1_user_url on a hunch after I saw this error and it works.
My question is what is going on here? I do not understand why I need to pass #user when most of the examples I've seen do not involve passing the object #user to the :location attribute.
The output of rake routes is a follows:
$ rake routes
Ignoring bcrypt-3.1.7 because its extensions are not built. Try: gem pristine bcrypt-3.1.7
Prefix Verb URI Pattern Controller#Action
api_v1_users GET /api/v1/users(.:format) api/v1/users#index {:format=>"json"}
POST /api/v1/users(.:format) api/v1/users#create {:format=>"json"}
new_api_v1_user GET /api/v1/users/new(.:format) api/v1/users#new {:format=>"json"}
edit_api_v1_user GET /api/v1/users/:id/edit(.:format) api/v1/users#edit {:format=>"json"}
api_v1_user GET /api/v1/users/:id(.:format) api/v1/users#show {:format=>"json"}
PATCH /api/v1/users/:id(.:format) api/v1/users#update {:format=>"json"}
PUT /api/v1/users/:id(.:format) api/v1/users#update {:format=>"json"}
DELETE /api/v1/users/:id(.:format) api/v1/users#destroy {:format=>"json"}
static_index GET /static/index(.:format) static#index
root GET / static#index
Another place I've seen this (just to name another one):
Embracing REST with mind, body and soul
The reason being is that api_v1_user_url or api_v1_user_path are for a single instance and you need to pass the id of that user it should look for and render. Hence why you got the error you go.
In the examples you probably saw api_v1_users_url or api_v1_users_path which renders all the users. Notice the users_ vs user_
I'm writing an API and am trying to login with the API and am getting uninitialized constant SessionsController error followed by the tip to "Try running rake routes for more information on available routes." when I try to go to the URL
http://localhost:3000/api/v1/login as a POST
My routes show that the route and action exist as shown:
api_v1_users GET /api/v1/users(.:format) api/v1/users#index {:format=>"json"}
POST /api/v1/users(.:format) api/v1/users#create {:format=>"json"}
new_api_v1_user GET /api/v1/users/new(.:format) api/v1/users#new {:format=>"json"}
edit_api_v1_user GET /api/v1/users/:id/edit(.:format) api/v1/users#edit {:format=>"json"}
api_v1_user GET /api/v1/users/:id(.:format) api/v1/users#show {:format=>"json"}
PUT /api/v1/users/:id(.:format) api/v1/users#update {:format=>"json"}
DELETE /api/v1/users/:id(.:format) api/v1/users#destroy {:format=>"json"}
new_api_v1_user_session GET /api/v1/login(.:format) sessions#new {:format=>"json"}
api_v1_user_session POST /api/v1/login(.:format) sessions#create {:format=>"json"}
destroy_api_v1_user_session DELETE /api/v1/logout(.:format) sessions#destroy {:format=>"json"}
api_v1_user_omniauth_authorize GET|POST /auth/:provider(.:format) authentications#passthru {:provider=>/twitter|facebook/, :format=>"json"}
api_v1_user_omniauth_callback GET|POST /auth/:action/callback(.:format) authentications#(?-mix:twitter|facebook) {:format=>"json"}
api_v1_user_password POST /api/v1/password(.:format) api/v1/passwords#create {:format=>"json"}
new_api_v1_user_password GET /api/v1/password/new(.:format) api/v1/passwords#new {:format=>"json"}
edit_api_v1_user_password GET /api/v1/password/edit(.:format) api/v1/passwords#edit {:format=>"json"}
PUT /api/v1/password(.:format) api/v1/passwords#update {:format=>"json"}
cancel_api_v1_user_registration GET /api/v1/cancel(.:format) registrations#cancel {:format=>"json"}
api_v1_user_registration POST /api/v1(.:format) registrations#create {:format=>"json"}
new_api_v1_user_registration GET /api/v1/sign_up(.:format) registrations#new {:format=>"json"}
edit_api_v1_user_registration GET /api/v1/edit(.:format) registrations#edit {:format=>"json"}
PUT /api/v1(.:format) registrations#update {:format=>"json"}
DELETE /api/v1(.:format) registrations#destroy {:format=>"json"}
When I try to access the account using the authentication_token url parameter, it works fine.
http://localhost:3000/api/v1/users/39?user_token=DxwspVzYSpsiLosd9xcE
Using that I can make PUT and GET requests no problem.
My routes look like this:
namespace :api, defaults: {format: 'json'} do
namespace :v1 do
resources :users
devise_for :users, :path => '', path_names: {sign_in: "login", sign_out: "logout"},
controllers: {omniauth_callbacks: "authentications", registrations: "registrations", sessions: "sessions"}
end
end
I assume it's a problem in my SessionsController, but can't figure out why the PUT request would work, but the POST wouldn't. Here is the SessionsController:
module Api
module V1
class SessionsController < Devise::SessionsController
before_filter :authenticate_user!, except: [:create, :destroy]
before_filter :ensure_params_exist
skip_before_filter :verify_authenticity_token
def create
resource = User.find_for_database_authentication(email: params[:user_login][:email])
return invalid_login_attempt unless resource
if resource.valid_password?(params[:user_login][:password])
sign_in("user", resource)
resource.ensure_authentication_token!
render 'api/v1/sessions/new.json.jbuilder', status: 201
return
end
invalid_login_attempt
end
def destroy
current_user.reset_authentication_token
render json: {success: true}
end
protected
def ensure_params_exist
return unless params[:user_login].blank?
render json: {success: false, message: "missing user_login parameter"}, status: 422
end
def invalid_login_attempt
render 'api/v1/sessions/invalid.json.jbuilder', status: 401
end
end
end
end
So in my routes file I had
namespace :api, defaults: {format: 'json'} do
namespace :v1 do
resources :users
devise_for :users, path: '', path_names: {sign_in: "login", sign_out: "logout"},
controllers: {omniauth_callbacks: "authentications", registrations: "registrations", sessions: "sessions"}
end
end
I removed sessions: "sessions" and everything works great. I'm not going to mark this as the correct answer because I don't know why this fixed the problem, or more specifically, why adding sessions: "sessions" was causing the error in the first place. Hopefully someone can explain this for future readers.
EDIT:
I realized much later that the problem was that I was referencing the sessions_controller, when I wanted to reference the api/v1/sessoins_controller There is no regular sessions_controller I just use the Devise one, but there is a sessions_controller in the API/V1 namespace. So that is why the problem was happening.
For future reference for anyone else who comes across this problem, the error is telling you the controller you're looking for doesn't exist as it should, so to troubleshoot, make sure you are looking for the controller in the correct place, and with the correct name.
In a similar case for an API app with rails 6 and devise 4.8.1 I used scope and devise_scopeto configure routes according to devises doc. devise_for was skip-ed.
In my case:
scope :api, defaults: { format: :json } do
scope :v1 do
devise_for :users, skip: :all
devise_scope :user do
post '/sign_in', to: 'devise/sessions#create'
delete '/sign_out', to: 'devise/sessions#destroy'
post '/sign_up', to: 'devise/registrations#create'
put '/account_update', to: 'devise/registrations#update'
delete '/account_delete', to: 'devise/registrations#destroy'
end
end
end