Skip to content

An implementation of role-based policies and operations to help controllers lose weight.

License

Notifications You must be signed in to change notification settings

jerome-arzel/skinny_controllers

 
 

Repository files navigation

skinny_controllers

Gem Version Build Status Code Climate Test Coverage Dependency Status

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.

  1. Your controller name is based off your model name (configurable per controller)
  2. Any defined policies or operations follow the formats (though they don't have to exist):
  • class #{Model.name}Policy
  • module #{Model.name}Operations
  1. Your model responds to find, and where
  2. Your model responds to is_accessible_to?. This can be changed at SkinnyControllers.accessible_to_method
  3. 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 }}``
  4. If using strong parameters, SkinnyControllers will look for {action}_{model}_params then {model}_params and then params. See the strong_parameters_spec.rb test 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:

  • EventSummariesController
  • EventSummaryOperations::*
  • 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 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, host_params).run
end

def host_params
  params.permit(:subdomain)
end

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?

About

An implementation of role-based policies and operations to help controllers lose weight.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Ruby 91.7%
  • HTML 4.1%
  • CSS 2.8%
  • JavaScript 1.4%