Rabl Api and preventing code duplication - ruby-on-rails

I am working on a simple API for a rails project which should also be able for versioning.
I am using rabl-rails gem.
To prevent code duplication, I wanna be able to use my ControllerActions (ex. UserController#search) twice. One time for the normal WebUsers, and one for the API.
I only saw people writing controllers like this:
class Api::v1::UsersController
def list
#user = User.all
end
end
Do I have to Namespace Controllers for RABL?
Or is it possible to "route or delegate" my JSON requests to existing Controllers?
For example my regular UsersController Action "list" has actually this:
def list
respond_to do |format|
format.html
format.json { render #user }
end
end
views/users/list.json.rabl also exist and works well in this case.
Now I try to move the rabl files into
/api/v1/users/list.json.rabl
I provide a Route:
namespace :api, :defaults => {:format => :json} do
namespace :v1 do
match 'users/list', to: 'Users#list', as: 'users', via: [:get, :post]
end
end
At the moment I do not provide a Api::V1::UsersController.
What is the best approach to
provide a /api/v1/users/list route but
use the regular UsersController and
have the list.json.rabl view in the /api/v1/ folder?
I hope it's not to complicated explained...

I ended up with the idea, that an API should have his own logic. Some actions have view specific stuff (breadcrumbs, dropdown values, etc.) inside.
With my knowledge "search logic" can be extracted into a module which gets mixed into your ApplicationController class (inlcude). Then you can use this logic in both places: Api and regular web view.
My first attempt is here:
BaseController
# Base Controller for all Api requests
class Api::V1::BaseController < ApplicationController
respond_to :json
acts_as_token_authentication_handler_for User
end
Api SubControllers
class Api::V1::UsersController < Api::V1::BaseController
# Inherit from BaseController -------^^^^^^^^^^^^^^^^^^^^^^^
# your personal extracted logic
include SearchLogic
def list
#user = User.all
end
end
So

Related

How to render file in Rails 5 API?

I have a single-page application written in React with Ruby on Rails back-end (API mode). Rails is also serving static files. I'm pointing Rails router to public/index.html, so my SPA could manage his own routing with react-router. This is common practice in order to make direct links and refresh to work.
routes.rb
match '*all', to: 'application#index', via: [:get]
application_controller.rb
class ApplicationController < ActionController::API
def index
render file: 'public/index.html'
end
end
The problem is this doesn't work in API mode. It's just an empty response. If I change the parent class to ActionController::Base everything works as expected. But I don't want to inherit the bloat of full class, I need slim API version.
I've tried adding modules like ActionController::Renderers::All and AbstractController::Rendering without success.
If I change the parent class to ActionController::Base everything works as expected. But I don't want to inherit the bloat of full class, I need slim API version.
Yes, if you serve index from ApplicationController, changing its base class would affect all other controllers. This is not good. But what if you had a specialized controller to serve this page?
class StaticPagesController < ActionController::Base
def index
render file: 'public/index.html'
end
end
This way, you have only one "bloated" controller and the others remain slim and fast.
You could do
render text: File.read(Rails.root.join('public', 'index.html')), layout: false
I usually just redirect_to 'file path'.
def export
# When the route coming to get 'some_report/export', to: 'greate_controller#export'
# The file where you write or preparing, then you can redirect some path like : http://localhost:3000/public/tmpfile/report20210507.xlsx
# And it will just redirect the file for you
file_path = "public/tmpfile/report20210507.xlsx"
redirect_to "#{root_url}#{file_path}"
end
For this example
root_url = "http://localhost:3000/"
This should work, and allow you to keep inheriting from ActionController::API--
class ApplicationController < ActionController::API
def index
respond_to do |format|
format.html { render body: Rails.root.join('public/index.html').read }
end
end
end
The render logic changed for ActionController::API with Rails 5.

Rails file setup for a control panel?

How do you setup your views, controllers and routes?
One controller for everything the control panel does, or many?
Firstly, let's try to think how we would view the various panels. Let's say our control panel is pretty simple. We have one panel to show all the users who have signed-up and can CRUD them, and another panel to show all of the images that have uploaded, and we can carry up CRUD on those too.
Routes:
scope path: 'control_panel' do
get 'users', to: 'panels#users', as: :panel_show_users
get 'photos', to: 'panels#photos', as: :panel_show_photos
end
Controller:
class PanelsController < ApplicationController
def users
#users = User.all
end
def photos
#photos = Photo.all
end
end
View file structure:
panels
|_ users.html.erb
|_ photos.html.erb
Okay, now I don't see any problems with that, to simply access the panels and populate the views with data. Do you see any problems?
Here is where I'm sort of at a cross roads though. What should I do when I want to Created Update and Delete a user/photo? Should I put them all in the PanelsController?
class PanelsController < ApplicationController
before_action :protect
def users
#users = User.all
end
def update_user
#user = User.find(params[:id])
#user.update(user_params)
end
def photos
#photos = Photo.all
end
def update_photo
#photo = Photo.find(params[:id])
#photo.update(photo_params)
end
private
def protect
redirect_to root_url, error: 'You lack privileges!'
end
end
While this would result in a large PanelsController, it would feel good to be able to execute that protect action and just one controller hook. It would also mean the routes would be easy to setup:
scope path: 'control_panel' do
get 'users', to: 'panels#users', as: :panel_show_users
post 'user', to: 'panels#update', as: :panel_create_user
get 'photos', to: 'panels#photos', as: :panel_show_photos
post 'photos', to: 'panels#photos', as: :panel_create_photo
end
I should use resource routes here?
Like I say, this will result in a huge panels controller, so I was thinking it may be better to have a separate controller for each resource and then redirect to panels views?
Routes:
scope path: 'control_panel' do
resources :users
resources :photos
end
Controllers:
class UsersController < ApplicationController
def index
end
def show
end
def new
end
def create
end
def update
end
def destroy
end
end
class PhotosController < ApplicationController
def index
end
def show
end
def new
end
def create
end
def update
end
def destroy
end
end
Still some quirks though. I have my Users#index action there, but what if I have two routes that return an index of all users? In the control panel, but also, when people are searching for another user, for example. Should I have two routes in the User controller? def public_users and def control_panel_users? That may be the answer. Could setup a hook to run #users = User.all in both of them, but redirect to a different location, and not have the protect method redirect them.
How should I protect these routes from non-admins? Should I move my protect method into the the application controller? Wouldn't this be a bit fiddly to setup?
class ApplicationController < ActionController
before_action :protect
def protect end
end
class StaticController < ApplicationController
skip_before_action [:home, :about, :contact]
def home
end
def about
end
def contact
end
end
But that is my question. 1 control panel controller or no control panel controller.
I really wish there was more advanced tutorials out there :( Billions of books on CRUD, MVC and things, but nothing on advanced things like control panels and AJAX...
Don't have a control panel controller. And to protect stuff from non-admins, use namespacing - read more about it here: http://guides.rubyonrails.org/routing.html#controller-namespaces-and-routing
You can protect your 'admin'-namespaced controllers with authentication, and have the non-namespaced controllers open to the public (or open to non-admin users)
With regards to your def public_users and def control_panel_users question, you could just have two def index methods - one in the non-namespaced controller, and one in the admin-namespaced controller. They would each do different things.
So, you'd have 4 controllers in total:
2 non-namespaced, one for users, one for photos (containing all public stuff)
2 admin-namespaced, one for users, one for photos (containing all control panel stuff)
If you wanted, rather than using 'admin' as the namespace, you could use some other term you prefer - like 'panel'. 'Admin' is pretty conventional though.

How to DRY up controller code which involves views code as well?

In my Rails 3 app, I have a number of models with a boolean field disabled. In controllers for these models, I use a custom action disable to toggle the disabled field using Ajax.
Example (For client),
# routes.rb
resources :clients do
member do
get :toggle_disable, to: 'clients#disable', as: :disable
end
end
# clients_controller.rb
def disable
#client = Client.find(params[:id])
#client.update_attribute :disabled, !#client.disabled
render 'clients/update_client', format: :js
end
# update_client.js.erb
$('#client-<%= #client.id %>-details').html("<%= escape_javascript(render 'clients/client', client: #client) %>");
I have this code for at least ten resources in my application.
QUESTION
How do I go about DRYing up this code and add actions for these boolean fields dynamically? I could have gone with creating a parent controller or module but I am not sure how will I take care of the views code.
I should be able to do something like this
#clients_controller.rb
class ClientsController < ApplicationController
add_toggle_action :disable
end
Two main ways to share methods:
inheritance: define your action in ApplicationController
mixins: add your method in a module and include the module in the appropriate controllers
Since you want only some controllers to get the method, I'll head towards mixin.
Your controller action must use a view with a full path, not relative, something like:
render '/shared/clien/update', format: :js
Lastly, you'll have to define all routes.

Generics Example Question

I am fairly new to Ruby on Rails and as a C# developer, when I want to re-use code (for a repository class), I could put it into a base class of type <T> to be able to do something like this:
public virtual IEnumerable<T> GetAll()
{
return Context<T>.GetAll();
}
If I need to do any custom logic, I could, of course, override the method in my 'User' repository.
In Ruby, I am familiar that you can do this:
class UsersController < ApplicationController
This will allow access to all methods in ApplicationController and it's parent classes. When using scaffolding, it generates the following method in each of my child classes:
def index
#users = User.all
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #users }
end
end
What I end up with is 10 classes that have the same method, but the only difference is 'User.all', 'Post.all', etc.
How would I make this method generic so I can put it in my ApplicationController class?
Thanks for any assistance you can provide to a Ruby on Rails newbie.
The first thing to realize about the scaffolding code is that it can be abreviated, as such:
def index
#users = User.all
end
unless you intend to deliver the view in another format, like json, html, pdf, the respond_to block is unnecessary. If you still feel the need to dry up this method, you could do something like
# app/controllers/concerns/autoload_records.rb
module AutoloadRecords
included do
before_action :load_records, only: :index
before_action :load_record, only: [:create, :show, :edit, :update, :destroy]
end
private
def load_records
#records = model_class.all
end
def load_record
#record = model_class.find(params[:id])
end
def model_class
klass = self.class.to_s[/\A(\w+)sController\Z/,1] #=> get the name of the class from the controller Constant
Object.const_get(klass)
end
end
and write your controller like
class UsersController < ApplicationController
include AutoloadRecords
def index
#records # => #<ActiveRecord::Relation[...]>
end
def show
#record # => #<User ...>
end
def non_rest_action
#record # => nil
#records # => nil
end
end
Rather than doing an eval where you really don't want to be doing one. Check out Jose Valim's Inherited Resources gem. It provides the standard CRUD methods for all of your controllers and is quite sophisticated. It is also thoroughly tested so you don't have to worry about making sure your generic code operates as expected in all cases.
For details on how to use it see the GitHub page linked.
Maybe a simple solution could be to rely on mixins.
You define a module,
module MyModule
def my_index(klass)
#elements = klass.all
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #elements }
end
end
end
Then, you have in your controller,
include MyModule
def index
my_index(User)
end
Of course, you need to use #elements in your views. If you want a different variable name in each view you can do
def my_index(klass, var_name)
self.instance_variable_set(var_name, klass.all)
...
end
There are several rails plugins that help to reduce this kind of duplication. This one was covered in railscast episode 230.
https://github.com/josevalim/inherited_resources
Based on my experience, you rarely end up with 10 index action looking like #user = User.all. If you know in advance that some actions between different models will be identical - well then may be it makes sense to extract common logic. But then again may be these models are somehow connected? I wouldn't say in advance that Post and User will have identical index actions.
For a short method like this I wouldn't try to eleminate repetition because you may end up losing readability.

What are the best practices for designing a RESTful public API on Rails?

Rails comes with RESTful resources out of the box, but do you use those for your actual public API? If so, how would you accomplish versioning of your API i.e. example.com/api/v2/foo/bar?
Typically, APIs for my applications are indeed built on the same resources that make up the HTML interface. For some (not me), that might be just using the code that comes out of the scaffold generator—but regardless of whether I write it custom or let the generator handle it, there are very few instances where I expose resources only to the programmatic API and not to the end user view.
Versioning hasn't been a problem for the applications I've built so far, but I can think of two ways to implement it.
1) You could add routes with the prefix 'v1,' 'v2,' etc., that set a parameter that you can then access in the controller to specify the processing to occur:
in routes.rb:
map.resources :posts, :path_prefix => '/:version'
in posts_controller.rb
class PostsController < ApplicationController
def index
respond_to do |format|
format.xml do
if params[:version] == 'v1'
# ...
else
# ...
end
end
end
end
end
2) You might also consider adding a custom response format for each version
in initializers/mime_types.rb
Mime::Type.register_alias "application/xml", :v1
Mime::Type.register_alias "application/xml", :v2
in posts_controller.rb
class PostsController < ApplicationController
def index
respond_to do |format|
format.v1 do
# ...
end
format.v2 do
# ...
end
end
end
end
The former would give you URLs like example.com/v1/posts.xml and example.com/v2/posts.xml; the latter would give you URLs like example.com/posts.v1 and example.com/posts.v2

Resources