How to route to a specific custom Rails 5 API version? - ruby-on-rails

I'm developing an application which backend is being written in rails 5 api (beta version).
My API will have some versions, and I'm using this approach to address versioning:
https://github.com/iamvery/rails-api-example/blob/master/config/routes.rb
Rails.application.routes.draw do
def api_version(version, &routes)
api_constraint = ApiConstraint.new(version: version)
scope(module: "v#{version}", constraints: api_constraint, &routes)
end
api_version(1) do
resources :articles, only: :index
end
api_version(2) do
resources :articles, only: :index
end
end
The thing is when I don't specify a version, it shows me an (obviuos) error (ActionController::RoutingError: No route matches [GET] \...).
But I'd like to route using latest api version instead throwing an error.

Your routes.rb file
Rails.application.routes.draw do
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
# Then for a new version create a new scope
end
end
Create a new api_constraints.rb file in the app/lib directory
class ApiConstraints
def initialize(options)
#version = options[:version]
#default = options[:default]
end
def matches?(req)
#default || req.headers['Accept'].include?("application/vnd.marketplace.v#{#version}")
end
end

I would add a root route, and use a simple redirect, like this:
root to: redirect('/api/v2')
I believe this could be done dynamically, by a little more change, something like this:
#versions = []
def api_version(version)
#versions << versions
# The rest of your code..
end
root to: redirect("/v#{#versions.max}")
I hope this helps.

Related

How to accurately define Controller name in Rails?

Apologies for the basic question, but I'm trying to create an endpoint so I can a TranslationApi in my backend via my VueJs frontend via Fetch, so I need to make an endpoint I can insert. I'm attempting to create a route to make that happen, however when I run bin/rails routes | grep CcApiController I receive the following error:
ArgumentError: 'CcApiController' is not a supported controller name. This can lead to potential routing problems. See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use
I've read the documentation linked, but I'm not managing to fix this, can someone explain where I'm going wrong here? I'll link the files I've changed below:
cc_apis_controller.rb
module Panel
class CcApisController < CcenterBaseController
def index
run Ccenter::Adapters::Zendesk::TranslationApi.call(2116449)
end
end
end
panel_routes.rb
def draw_api_routes
resources: CcApiController
end
routes.rb
Rails.application.routes.draw do
resources :CcApiController, only: [:index]
end
API method I need to create Route for:
def make_request
response = Faraday.post('https://api.deepl.com/v2/translate', auth_key: '', text: #final_ticket, target_lang: 'DE', source_lang: 'EN')
if response.status == 200
body = response.body
message_element = body.split('"')[-2]
return message_element
else
raise InvalidResponseError unless response.success?
end
end
The answer to that is pretty simple.
Route names are snake_case, and match the controller's file name just omit the _controller suffix. In your case it is
Rails.application.routes.draw do
namespace :panel do
resources :cc_apis, only: %i[index]
end
end
For more info check this documentation and this article.

Rails | nested routes if nil

I have used friendly_id and globalize gem. So I can generate routes as;
/search/france/weekly/toyota-95
Here is my routes;
namespace :search do
resources :car_countries, path: '', only: [] do
resources :rental_types, path: '', only: [] do
resources :car_types, path: '', only: [:show] do
end
end
end
end
But the thing is now I would like to also get city either;
/search/nice/weekly/toyota-95
or
/search/france/nice/weekly/toyota-95
The problem is I want to have both with city name and without city name (only country). They should go to same controller which is at the end car_types.
So if I add car_cities to routes, I get error when there is no city but only country.
namespace :search do
resources :car_countries, path: '', only: [] do
resources :car_cities, path: '', only: [] do
resources :rental_types, path: '', only: [] do
resources :car_types, path: '', only: [:show] do
end
end
end
end
resources :car_countries, path: '', only: [] do
resources :rental_types, path: '', only: [] do
resources :car_types, path: '', only: [:show] do
end
end
end
end
How can I do that?
As Gerry says, take a look at route globbing http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments. I'd suggest to send everything to a single controller action and either do stuff there or delegate it to a search model/service object (depends on your taste).
Example:
# in config/routes.rb
get 'search/*q' => 'searches#show'
# in app/controllers/searches_controller.rb
class SearchesController < ApplicationController
def search
# This should work for your simple use case but it will become pretty confusing if you add more filters.
search_params = params[:search].split('/')
if search_params.count == 4
country, city, rental_type, car_type = search_params
else
country, rental_type, car_type = search_params
end
# Do whatever with these variables, e.g. Car.for_country(country)...
end
end
A more stable solution would be to make use of the fact that rental types are probably a closed set (daily, weekly, ...) and use segment constraints for this part in the routes:
# config/routes.rb
scope to: 'searches#show', constraints: { rental_type: /(daily|weekly|monthly)/ } do
get '/search/:country/:rental_type/:car_type'
get '/search/:country/:city/:rental_type/:car_type'
end
This should differentiate the two URLs based on the fact that :city can never match the rental type constraint.
Yet another option would be to use a full blown constraints object (http://guides.rubyonrails.org/routing.html#advanced-constraints):
# config/routes.rb
class SearchConstraint
def initialize
# assuming that all your objects have the friendly_id on the name field
#country_names = %w[austria germany]
#rental_type_names = %w[daily weekly monthly]
#car_type_names = %w[toyota-prius vw-golf]
#city_names = %w[innsbruck munich berlin]
end
def matches?(request)
checks = []
# only check for parts if they're actually there
checks << #country_names.include?(request.parameters[:country]) if request.parameters[:country].present?
checks << #rental_type_names.include?(request.parameters[:rental_type]) if request.parameters[:rental_type].present?
checks << #car_type_names.include?(request.parameters[:car_type]) if request.parameters[:car_type].present?
checks << #city_names.include?(request.parameters[:city]) if request.parameters[:city].present?
checks.all? # or, if you want it more explicit: checks.all? { |result| result == true }
end
end
scope to: 'searches#show', constraints: SearchConstraint.new do
get '/search/:country/:rental_type/:car_type'
get '/search/:country/:city/:rental_type/:car_type'
end
Note that this last approach is probably the cleanest and least hacky approach (and it's quite easy to test) but it also comes at the cost if involving the database in every request to these particular URLs and the URLs fail hard if there's an issue with the database connection.
Hope that helps.

Ruby API - Accept parameters and execute script

I have created a rails project that has some code that I would like to execute as an API. I am using the rails-api gem.
The file is located in app/controllers/api/stats.rb.
I would like to be able to execute that script and return json output by visiting a link such as this - http://sampleapi.com/stats/?location=USA?state=Florida.
How should I configure my project so that when I visit that link it runs my code?
the file should be called stats_controller.rb app/controllers/api/stats_controller.rb
you can create an index method where you can add your code
class API::StatsController < ApplicationController
def index
#your code here
render json: your_result
end
end
in the file config/routes.rb you should add
get 'stats' => 'api/stats#index', as: 'stats'
To access the params in the url you can do it in your index method with params[:location] ,params[:state]
Here's how I would think of this:
in app/controllers/api/stats_controller.rb
module Api
class StatsController
def index
# your code implementation
# you can also fetch/filter your query strings here params[:location] or params[:state]
render json: result # dependent on if you have a view
end
end
end
in config/routes.rb
# the path option changes the path from `/api` to `/` so in this case instead of /api/stats you get /stats
namespace :api, path: '/', defaults: { format: :json } do
resources :stats, only: [:index] # or other actions that should be allowed here
end
Let me know if this works

Versioning Rails API

I have searched around for a couple of days trying to figure out if I am appropriately versioning my rails api. I haven't quite found a good answer that helps me feel comfortable with my current approach. Thus, I have decided to take to stackoverflow to get my fellow Rails mates opinions. Lets look at some code:
I first started out by adding a namespace :api with a subdomain of 'api' to my api in routes.rb. I also added a scope for v1.
require 'api_constraints'
HostApi::Application.routes.draw do
# Api Definition
namespace :api, defaults: { format: :json }, constraints: { subdomain: 'api' }, path: '/' do
# Scoping Api Version
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
# resources here
end
end
end
I want to give those who hit the api to get back the current default version unless they specify a different version through headers. To handle this, I created an api_constraints in my lib directory.
lib/api_constraints.rb & lib/spec/api_constraints_spec.rb for testing.
class ApiConstraints
def initialize(options)
#version = options[:version]
#default = options[:default]
end
def matches?(request)
#default || request.headers["Accept"].include?("application/vnd.host_api.v#{#version}")
end
end
My test's pass and everything feels good. What I am curious about is when I start adding version 2,3,4; Can they be scoped the same way as v1 is scoped? Example:
require 'api_constraints'
HostApi::Application.routes.draw do
# Api Definition
namespace :api, defaults: { format: :json }, constraints: { subdomain: 'api' }, path: '/' do
# Scoping Api Version
scope module: :v1, constraints: ApiConstraints.new(version: 1) do
# resources here
end
scope module: :v2, constraints: ApiConstraints.new(version: 2, default: true) do
# resources here
end
end
end
I would imagine that changing passing default true to ApiConstraints in v2 scope would now set it as the default response when a version isn't requested through headers. Am I understanding this correctly? Would there be a better approach to handling the version than this? Thoughts, ideas, opinions greatly appreciated.
Side question. I did also change Rails.application.routes.draw in the routes.rb file, to HostApi::Application.routes.draw. This is commonly what I have seen others do, but I am unsure what the benefit to doing this is. If someone could help elaborate I would be very thankful. Thank you in advance for anyone who takes the time to help me understand this or simply share their thoughts.
I've used the same ApiConstraints in my rails-api-base project and it crashes when you try matches? with a non-default version without specifying the Accept header.
I added the following case to my tests (which crashes):
it 'returns false when not default and no Accept header' do
request = double(host: 'api.host_api.dev')
expect(api_constraints_v1.matches?(request)).to be false
end
And I fixed ApiConstraints:
def matches?(req)
#default ||
(req.respond_to?('headers') &&
req.headers.key?('Accept') &&
req.headers['Accept'].include?("application/vnd.host_api.v#{#version}"))
end
Hope it helps!

Rails UrlGeneration Error on form_for after upgrade to 4.2

I just upgraded rails from 4.0 to 4.2 and it seems to have caused an error in one of my form_fors. Now when I visit the new view, I get:
Error:
ActionController::UrlGenerationError at /billing/providers/new
No route matches {:action=>"show", :controller=>"billing/provider_agencies", :id=>nil} missing required keys: [:id]
View (error from this line):
= form_for #provider_agency, url: billing_provider_agency_path(#provider_agency) do |f|
...
Routes:
namespace :billing do
resources :provider_agencies, path: "providers" do
resources :invoices
end
end
Controller:
class Billing::ProviderAgenciesController < BillingController
before_action :set_provider_agency, only: [:show, :edit, :update, :destroy]
def index
...
end
def show
#users = #provider_agency.users
end
def destroy
...
end
def new
#provider_agency = Agency::Provider.new
end
def create
...
end
def edit
...
end
def update
...
end
protected
def provider_agency_params
params.require(:agency_provider).permit(:id, :name,...
...
])
end
def set_provider_agency
#provider_agency = #agency.agencies.find(params[:id])
end
end
I had to define the url in the form_for because of the way I namespace the resources in my routes. Defining that url in the form_for used to work in any view including new. Now the new view seems to be calling the show action, and looking for the #provider_agency that hasn't been created/saved yet. To be clear, this breaks the new view but all other view's still work.
I went back to check the commits and none of these files have changed, the error simply begins when I upgrade to Rail 4.2.
Any ideas are greatly appreciated, thanks!
EDIT:
Here is the relevant portion of my rake routes too:
billing_provider_agencies GET /billing/providers(.:format) billing/provider_agencies#index
POST /billing/providers(.:format) billing/provider_agencies#create
new_billing_provider_agency GET /billing/providers/new(.:format) billing/provider_agencies#new
edit_billing_provider_agency GET /billing/providers/:id/edit(.:format) billing/provider_agencies#edit
billing_provider_agency GET /billing/providers/:id(.:format) billing/provider_agencies#show
PATCH /billing/providers/:id(.:format) billing/provider_agencies#update
PUT /billing/providers/:id(.:format) billing/provider_agencies#update
DELETE /billing/providers/:id(.:format) billing/provider_agencies#destroy
I had a similar issue.
I modified the form_for syntax little bit.
For nested model,old syntax:
form_for(#child, url: parent_child_path(#parent,#child))
to newer syntax
form_for([#parent,#child])
For independent model
form_for(#model, url: model_path(#model))
to
form_for(#model)
Refer here

Resources