skinny_controllers
An implementation of role-based policies and operations to help controllers lose weight.
The goal of this project is to help API apps be more slim, and separate logic as much as possible.
This gem is inspired by trailblazer, following similar patterns, yet allowing the structure of the rails app to not be entirely overhauled.
Please note that this is a work in progress, and that the defaults are subject to change. If you have an idea or suggestion for improved defaults, please submit an issue or pull request. :-)
Installation
gem 'skinny_controllers'
or
gem install skinny_controllers
Usage
In a controller:
include SkinnyControllers::Diet
# ...
# in your action
render json: model
and that's it!
The above does a multitude of assumptions to make sure that you can type the least amount code possible.
- Your controller name is based off your model name (configurable per controller)
- Any defined policies or operations follow the formats (though they don't have to exist):
class #{Model.name}Policymodule #{Model.name}Operations
- Your model responds to
find, andwhere - Your model responds to
is_accessible_to?. This can be changed atSkinnyControllers.accessible_to_method - If relying on the default / implicit operations for create and update, the params key for your model's changes much be formatted as
{ Model.name.underscore => { attributes }}` - If using strong parameters, SkinnyControllers will look for
{action}_{model}_paramsthen{model}_paramsand thenparams. See thestrong_parameters_spec.rbtest to see an example.
Your model name might be different from your resource name
Lets say you have a JSON API resource that you'd like to render that has some additional/subset of data.
Maybe the model is an Event, and the resource an EventSummary (which could do some aggregation of Event data).
The naming of all the objects should be as follows:
EventSummariesControllerEventSummaryOperations::*EventSummaryPolicy- and the model is still
Event
In EventSummariesController, you would make the following additions:
class EventSummariesController < ApiController # or whatever your superclass is
include SkinnyControllers::Diet
self.model_class = Event
def index
render json: model, each_serializer: EventSummariesSerializer
end
def show
render json: model, serializer: EventSummariesSerializer
end
end
Note that each_serializer and serializer is not part of SkinnyControllers, and is part of ActiveModel::Serializers.
What if your model is namespaced?
All you have to do is set the model_class, and model_key.
class ItemsController < ApiController # or whatever your superclass is
include SkinnyControllers::Diet
self.model_class = NameSpace::Item
self.model_key = 'item'
end
model_key specifies the key to look for params when creating / updating the model.
Note that while model_key doesn't have to be specified, it would default to name_space/item. So, just keep that in mind.
What if you want to call your own operations?
Sometimes, magic is scary. You can call anything you want manually (operations and policies).
Here is an example that manually makes the call to the Host Operations and passes the subdomain parameter in to filter the Host object on the subdomain.
def show
render json: host_from_subdomain, serializer: each_serializer
end
private
def host_from_subdomain
@host ||= HostOperations::Read.new(current_user, params, host_params).run
end
def host_params
params.permit(:subdomain)
end
For JSON-API
Strong parameters must be used on create/update actions.
Here is an example params method
private
def event_params
params
.require(:data)
.require(:attributes)
.permit(:name)
end
Note that we don't need the id under the data hash, because in a RESTful api, the id will be available to us through the top level params hash.
Defining Operations
Operations should be placed in app/operations of your rails app.
For operations concerning an Event, they should be under app/operations/event_operations/.
Using the example from the specs:
module EventOperations
class Read < SkinnyControllers::Operation::Base
def run
model if allowed?
end
end
end
alternatively, all operation verbs can be stored in the same file under (for example) app/operations/user_operations.rb
module UserOperations
class Read < SkinnyControllers::Operation::Base
def run
model if allowed?
end
end
class ReadAll < SkinnyControllers::Operation::Base
def run
model if allowed?
end
end
end
Creating
To achieve default functionality, this operation may be defined -- though, it is implicitly assumed to function this way if not defined.
module UserOperations
class Create < SkinnyControllers::Operation::Base
def run
return unless allowed?
@model = model_class.new(model_params)
@model.save
@model # or just `model`
end
end
end
Updating
module UserOperations
class Update < SkinnyControllers::Operation::Base
def run
return unless allowed?
model.update(model_params)
model
end
end
end
Deleting
Goal: Users should only be able to delete themselves
To achieve default functionality, this operation may be defined -- though, it is implicitly assumed to function this way if not defined.
module UserOperations
class Delete < SkinnyControllers::Operation::Base
def run
model.destroy if allowed?
end
end
end
And given that this method exists on the User model:
# realistically, you'd only want users to be able to access themselves
def is_accessible_to?(user)
self.id == user.id
end
Making a call to the destroy action on the UsersController will only succeed if the user trying to delete themselves. (Possibly to 'cancel their account')
Defining Policies
Policies should be placed in app/policies of your rails app.
These are where you define your access logic, and how to decide if a user has access to the object
class EventPolicy < SkinnyControllers::Policy::Base
def read?(o = object)
o.is_accessible_to?(user)
end
end
Globally Configurable Options
All of these can be set on SkinnyControllers,
e.g.:
SkinnyControllers.controller_namespace = 'API'
The following options are available:
| Option | Default | Note |
|---|---|---|
operations_namespace |
'' | Optional namespace to put all the operations in. |
operations_suffix |
'Operations' |
Default suffix for the operations namespaces. |
policy_suffix |
'Policy' |
Default suffix for policies classes. |
controller_namespace |
'' |
Global Namespace for all controllers (e.g.: 'API') |
allow_by_default |
true |
Default permission |
accessible_to_method |
is_accessible_to? |
method to call an the object that the user might be able to access |
accessible_to_scope |
accessible_to |
scope / class method on an object that the user might be able to access |
action_map |
see skinny_controllers.rb |
TODO
- Configurable Error Renderer
- Default to JSON API format errors?