Rails API versioning - Invoke a controller action if it exists? - ruby-on-rails

Right now I have two API namespaces, Api::V1 and Api::V2
I plan on implementing V2 slowly over time rather than all in one shot. However, since part of it exists, I'd like it if the client could send all HTTP requests to the V2 URL and let the server deal with it if that particular endpoint is not yet implemented.
Is there a way to route V2 requests to the V1 controller if the V2 controller does not have an action?
Simple Example:
routes:
namespace :api do
namespace :v1 do
resources :items, only: [:index]
end
namespace :v2 do
resources :items, only: :create
end
end
will produce the following endpoints:
GET /api/v1/items
POST /api/v2/items
The goal is to send a GET request to /api/v2/items and have it invoke Api::V1:ItemsController#index since the V2 controller does not have this method yet.

namespace :api do
namespace :v1, path: "v2" do
# All endpoints that are on v1 of API
end
namespace :v2 do
# All endpoints that are on v2 of API
end
end
If you run rake routes here you'll see that they all match the route "api/v2/____#_____" but the ones in the top block invoke Api::V1 actions and the bottom block invoke Api::V2 actions, so youll have to move them from the top to the bottom as you implement those endpoints

I also have a versioned API. I haven't bumped to the next version yet. But I thought I'd share what I plan to do. This might be a little hairballed. And I have a feeling that this will be more useful to me (to think through my plan) than it is to you. So, sorry about that. But, here goes...
Before I start, I should say that I take a different sort of approach to my controller actions. In my apps, I like to delegate my controller actions to plain old ruby objects that I call 'managers'. Every controller has a 'manager_base'. So, in my controllers, I have something like this:
class ApplicationController < ActionController::Base
private
def do_action(action=nil)
action ||= caller[0][/`.*'/][1..-2]
manager.send("manage_#{action}", self, cookies, request)
end
def manager
base = self.class.name.split('::')
base.pop
base << "#{controller_name.camelize}Managers::ManagerBase"
base.join('::').constantize
end
end
class Api::V1::FooController < ApplicationController
def index
do_action
render_result
end
end
And then I also have:
class ManagerBase
class << self
def manage_index(controller, cookies, request)
sub_out("Index", controller, cookies, request)
end
def manage(controller, cookies, request)
new(controller, cookies, request).manage
end
private
def sub_out(method, controller, cookies, request)
sub_manager(method).manage(controller, cookies, request)
end
end # Class Methods
def initialize(controller, cookies, request)
#controller = controller
#cookies = cookies
#request = request
end
end
class Api::V1::FooManagers::ManagerBase < ManagerBase
class << self
private
def sub_manager(method)
"Api::V1::FooManagers::#{method}Manager".constantize
end
end # Class Methods
end
class Api::V1::FooManagers::IndexManager < Api::V1::FooManagers::ManagerBase
def manage
... do stuff
end
end
If you follow the bouncing ball, here's how my application flow goes:
index gets called on Api::V1::FooController
index calls do_action (inherited from ApplicationController) which, in turn calls manager (also inherited from ApplicationController)
manager returns the Api::V1::FooManagers::ManagerBase class
do_action then calls manage_index on Api::V1::FooManagers::ManagerBase
manage_index calls sub_out which in turn calls sub_manager
sub_manager returns the Api::V1::FooManagers::IndexManager
sub_out then calls manage on Api::V1::FooManagers::IndexManager
manage (the class method - inherited from ManagerBase) creates a new instance of Api::V1::FooManagers::IndexManager and then calls manage (the instance method) on the new instance.
As may or may not be apparent, when I move to Api::V2, I have two opportunities to 'hook' back into the Api::V1 versions of my managers (which is equivalent to using my V1 controller methods - your original question).
First, if I haven't implemented Api::V2::FooManagers::ManagerBase yet, I can cause ApplicationController.manager to fall back to the last implemented version of ManagerBase (i.e., Api::V1::FooManagers::ManagerBase). In which case I'll be using all of the Api::V1::FooManager sub managers (like IndexManager).
Second, if I've implemented Api::V2::FooManagers::ManagerBase but have not yet implemented Api::V2::FooManagers::IndexManager, then I can cause Api::V2::FooManagers#sub_manager to fall back to Api::V1::FooManagers::IndexManager.
Okay, I'm going to stop now. Thanks for the opportunity to think this through out loud. Apologies if it's a totally useless, hot mess.

Related

What is the best way to apply a function to a set of routes in rails?

I am looking to create a filtering system where I can apply a set of sanity checks to a subset of routes on a REST API. What is the most railsy way to create the mapping of routes to functions which need to be called on that route? I am looking to create a one to many mapping where a function can be mapped to more than one route. Currently I am creating a module where I am mapping the routes and checking the request after it comes in. Is there a native rails way to do this?
Don't know about best way but one way would be to create a controller with this sanity check and have your controllers inherit from it.
class SanityController < ApplicationController
before_action :sanity_check
def sanity_check
# some world class sanity checks
end
end
class OtherController < SanityController
end
You can also skip certain actions if needed
class OtherController < SanityController
skip_before_action :sanity_check, only: :index
end

Rails - Add a 'GET' sub-route to a main route

Currently I have a route called requests that may have GET/POST endpoints. But another requirement is to achieve the following format: /api/requests/sync.
I tried the following in routes.rb:
Rails.application.routes.draw do
resources :requests do
get "sync"
end
end
But this gives me the following format:
/requests/:request_id/sync
How can I create a sub-route as requests/sync without having it as a sub-route of /:request_id/sync?
Check out the guide. Specifically, collection routes. You'll do something like:
Rails.application.routes.draw do
resources :requests do
collection do
get "sync"
end
end
end
Which will give you requests/sync
To pick up on the sync_controller question...
Personally, not knowing much about what you're actually up to, I would keep sync as an action on the requests_controller. Something like:
class RequestsController < ApplicationController
...
def sync
...
end
...
end
While sync is not one of the standard RESTful actions, it seems more natural to me than creating a new controller. In general, but not always, I think of controllers as being noun-oriented (e.g., 'request', in your case) and actions being verb-oriented. "Sync" seems way more verb-y to me than noun-y.
You could do something along the lines of what Cyzanfar suggests. But, I would suggest you ask yourself:
Do you need all the standard actions for your would-be sync_controller?
Is there some meaningful reason to inherit from Requests::RequestsController?
Do you even need Requests::RequestsControler or could you just do RequestsController and then have Requests::SyncController inherit from RequestsController (which seems less tortured to me)?
And probably other important questions I'm not thinking about on-the-fly.
Here is another way to achieve this with namespacing your controllers so that you can have a distinct controller for sync and requests where the request controller will act as the parent (base) controller.
routes.rb
namespace :requests do
resources :sync
end
requests/requests_controller.rb
class Requests::RequestsController < ApplicationController
end
requests/sync_controller.rb
class Requests::SyncController < Requests::RequestsController
end
Now you'll have the nested CRUD paths under requests
/requests/sync/new
/requests/sync/index
/requests/sync/create
...

Rails enable specify middlewares for specify route just like Phoenix pipeline

In phoenix framework with pipeline we can enable specify middlewares for some route, for example:
defmodule HelloPhoenix.Router do
use HelloPhoenix.Web, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", HelloPhoenix do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
end
scope "/api", HelloPhoenix do
pipe_through :api
end
end
if request from /api, will only trigger plug :accepts, ["json"] middleware
if request from /, will trigger session, flash, ...etc middlewares
how to achieve this on rails, If I am using grape for build api and rails for build web page and enable difference middleware for each other?
Unlike Phoenix applications, you cannot (easily) change what middleware is used for a request within Rails. The best way to get this kind of behaviour in a Rails application would be to make the controllers for a particular route inherit from a common base controller, and then define the behaviour for these particular controllers in that base controller.
Using the above example of the /api routes going through different "middleware", you can have a controller like this:
module API
class BaseController < ActionController::API
# things unique to API routes go here
end
end
Inside this controller, you could write some before_action callbacks that ensure things like:
Inbound requests are JSON only
Requests are authenticated with a particular token
Then you can inherit from this controller for all API controllers:
module API
class PostsController < API::BaseController
end
end
With your regular controllers you can do all the regular things:
class ApplicationController < ActionController::Base
# things unique to non-api routes go here
end
And then:
class PostsController < ApplicationController
end
You could of course segment it more; maybe you might have another route like /accounts/1/posts where you have to be authenticated as the user of that account to see the posts. You can take the same approach:
module Accounts
class BaseController < ActionController::Base
# find account, check if the user is authenticated, etc.
end
end
And:
module Accounts
class PostsController < Accounts::BaseController
end
end
So in summary: Rails doesn't let you do this on a routing level (easily), but you can accomplish the same thing at the controller level.
The solution I was looking for was something more in this direction:
application.rb:
# after Bundler.require(...)
require_relative '../lib/engines/website/lib/website'
lib/engines/website/lib/website.rb:
require_relative "website/engine"
module Website; end
lib/engines/website/lib/website/engine.rb:
module Website
class Engine < ::Rails::Engine
middleware.use ActionDispatch::Cookies
middleware.use ActionDispatch::Session::CookieStore
middleware.use ActionDispatch::Flash
end
end
config/routes.rb:
mount Website::Engine => "/website"
And everything for the website goes in the typical directory structure under the engine directory:
lib
engines
website
app
assets
...
controllers
...
views
...
config
routes.rb
lib
website
website.rb
Reference: Build 2 middleware stacks in Rails app
I'm pretty sure that you can achieve this result by configuring Rack middleware stack, but it won't be configurable in a rails application.
Sometimes it's affordable to have unnecessary middleware for some routes.
Almost all apps that I saw are using same middleware for every request.
And yes, Phoenix behavior is better here, Rails behavior done in earliest versions, so it's hard to change something now.

Rails: How to POST internally to another controller action?

This is going to sound strange, but hear me out...I need to be able to make the equivalent of a POST request to one of my other controllers. The SimpleController is basically a simplified version of a more verbose controller. How can I do this appropriately?
class VerboseController < ApplicationController
def create
# lots of required params
end
end
class SimpleController < ApplicationController
def create
# prepare the params required for VerboseController.create
# now call the VerboseController.create with the new params
end
end
Maybe I am over-thinking this, but I don't know how to do this.
Inter-controller communication in a Rails app (or any web app following the same model-adapter-view pattern for that matter) is something you should actively avoid. When you are tempted to do so consider it a sign that you are fighting the patterns and framework your app is built on and that you are relying on logic has been implemented at the wrong layer of your application.
As #ismaelga suggested in a comment; both controllers should invoke some common component to handle this shared behavior and keep your controllers "skinny". In Rails that's often a method on a model object, especially for the sort of creation behavior you seem to be worried about in this case.
You shouldn't be doing this. Are you creating a model? Then having two class methods on the model would be much better. It also separates the code much better. Then you can use the methods not only in controllers but also background jobs (etc.) in the future.
For example if you're creating a Person:
class VerboseController < ApplicationController
def create
Person.verbose_create(params)
end
end
class SimpleController < ApplicationController
def create
Person.simple_create(params)
end
end
Then in the Person-model you could go like this:
class Person
def self.verbose_create(options)
# ... do the creating stuff here
end
def self.simple_create(options)
# Prepare the options as you were trying to do in the controller...
prepared_options = options.merge(some: "option")
# ... and pass them to the verbose_create method
verbose_create(prepared_options)
end
end
I hope this can help a little. :-)

Determine the domain in an ActiveRecord model

I am in the middle of migrating my application from using subdirectories for userspace to subdomains (ie. domain.com/~user to user.domain.com). I've got a method in my user class currently to get the "home" URL for each user:
class User
def home_url
"~#{self.username}"
# How I'd like to do it for subdomains:
#"http://#{self.username}.#{SubdomainFu.host_without_subdomain(request.host)}"
end
end
I'd like to update this for subdomains, but without hardcoding the domain into the method. As you can see, I am using the subdomain-fu plugin, which provides some methods that I could use to do this, except that they need access to request, which is not available to the model.
I know it's considered bad form to make request available in a model, so I'd like to avoid doing that, but I'm not sure if there's a good way to do this. I could pass the domain along every time the model is initialized, I guess, but I don't think this is a good solution, because I'd have to remember to do so every time a class is initialized, which happens often.
The model shouldn't know about the request, you're right. I would do something like this:
# app/models/user.rb
class User
def home_url(domain)
"http://#{username}.#{domain}"
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def domain
SubdomainFu.host_without_subdomain(request.host)
end
# Make domain available to all views too
helper_method :domain
end
# where you need it (controller or view)
user.home_url(domain)
If there is such a thing as a canonical user home URL, I would make a configurable default domain (e.g. YourApp.domain) that you can use if you call User#home_url without arguments. This allows you to construct a home URL in places where, conceptually, the "current domain" does not exist.
While molf's answer is good, it did not solve my specific problem as there were some instances where other models needed to call User#home_url, and so there would be a lot of methods I'd have to update in order to pass along the domain.
Instead, I took inspiration from his last paragraph and added a base_domain variable to my app's config class, which is the set in a before_filter in ApplicationController:
module App
class << self
attr_accessor :base_domain
end
end
class ApplicationController < ActionController::Base
before_filter :set_base_domain
def set_base_domain
App.base_domain = SubdomainFu.host_without_subdomain(request.host)
end
end
And thus, when I need to get the domain in a model, I can just use App.base_domain.

Resources