zeevallin / arcane

Rails strong parameters, made object oriented.
MIT License
50 stars 4 forks source link

!!! PROJECT IS NO LONGER BEING MAINTAINED !!!

Arcane

Build Status Coverage Status Gem Version Dependency Status

Easy to use parameter management, extending Strong Parameters, making them a first class citizen. Arcane provides you with helpers which guide you in leveraging regular Ruby classes and object oriented design patterns to organise and easily harnass the power of strong parameters in Rails 3 and 4.

Arcane magic is real and reliable, no cheap tricks. Inspired by Pundit

Breaking Changes - Check CHANGELOG.md

Is it production ready? Yes.

Installation

Add this line to your application's Gemfile:

gem "arcane", "~> 1.2.0"

If you are still on Rails 3, add this to your Gemfile as well:

gem "strong_parameters", "~> 0.2.0"

And then execute:

$ bundle

To install arcane, execute:

rails g arcane:install

Usage

This is how easy it is to use:

def create
  @article = Article.new params.for(Article).refine
end

def update
  @article.find(params[:id])
  @article.update_attributes params.for(@article).as(current_user).refine
end

def destroy
  @article.find(params[:id])
  @article.update_attributes params.for(@article).on(:destroy).refine
end

Include the Arcane Module

Though, we need to do a couple of things before we can get started. First of all include Arcane in your controller. This will extend strong parameters with all the arcane methods you saw above in the example.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Arcane
end

Create your first Refinery

Before you can use the parameter methods, you need a Refinery for the model you want to pass parameters to. Simply create the directory /app/refineries in your Rails project. Create a Refinery your model, in this case Article. /app/refineries/article_refinery.rb. Create a class in that file called ArticleRefinery. You can also use the rails generator below:

rails g arcane:refinery article

Methods defined in the refinery should reflect the controller method for clarity, but can be anything you want it to be. These methods must return an array containing the same parameters you would otherwise send to strong parameters.

It can be initiated using a Struct which accepts an object and a user. Arcane will automatically send current_user, if present to the refinery as well as the object you want to apply the parameters on.

# app/refineries/article_refinery.rb
class ArticleRefinery < Struct.new(:object,:user)
  def create
    [:title] + update
  end
  def update
    [:content]
  end
end

Note, for convenience sake there is a class called Arcane::Refinery which you can use instead of the struct. This class gives you the basic functionality of a refinery for free.

class CommentRefinery < Arcane::Refinery; end

Using Arcane in your controller

Next up, using the Arcane methods. There first three are; for, as, on and can all be called on an instance of rails params, chained, and in any order you want. The fourth one, refine you call to pull your parameters through a refinery.

refined_params = params.for(@article).as(current_user).on(:update).refine

Here's an example of how a controller can look with Arcane paramters.

class GameController < ApplicationController
  def create
    @game = Game.create(params.for(Game).as(user_from_location).refine)
  end

  def update
    @game.find(params[:id])
    @game.update_attributes params.for(@game).as(current_user).refine
  end

  def update_many
    @games = Game.find(params[:ids])
    @games.each do |game|
      game.update_attributes(params.for(@game).on(:update).refine)
    end
  end

private

  def user_from_location
    # ...
  end
end

Features

Invokable anywhere.

As arcane extends ActionController::Parameters you can invoke in anywhere and start toying around with the arcane methods. This is good if you have some other way of getting data in to your application outside the context of a controller.

@user, @post = User.find(1), Post.find(24)

my_params = ActionController::Parameters.new({ post: { content: "Hello" } })
my_params.for(@post).as(@user).on(:create)

If you want to delegate some behaviour to your model, you can always create your own constructor.

class Post < ActiveRecord::Base
  def self.refined_create(params)
    create(params.for(self).refine)
  end
end

class PostsController
  def create
    @post = Post.refined_create(params)
  end
end

Automatic method detection.

If you have specified no refinery action in your chain to params, Arcane tries to find out for itself what method to use. Arcane uses the action key in the rails parameters to determine the refinery method.

class CommentsController < ApplicationController
  def update
    @comment = Comment.find(params[:id])
    @comment.update_attributes params.for(@comment)
  end
end

class CommentRefinery < Arcane::Refinery
  def update
    [:email,:name,:text]
  end
end

Easily manage conditional parameters

As an application grows larger, you usually want to filter what parameters based on some rules, usually by checking the role of your current user or the state of the object you want to set the parameters on. Arcane easily helps you do this.

class UserRefinery < Arcane::Refinery
  def update
    params = [ { abuse_reports: AbuseReportRefinery.new(AbuseReport.new,user).create } ]
    params += [:suspended]   if user.admin?
    params += [:name,:email] if user.admin? or user == object
  end
end

Default parameters.

You are able to specify a default method in your refinery which will be prioritized if no the method you call does not exist. If default is not specified it will be as the refinery returned an empty array.

class AmbiguityRefinery < Arcane::Refinery
  def default
    [:data]
  end
end

Custom root requirement.

You are able to disable or change the root requirement. Let's say you have a sessions endpoint where you don't have your username and password parameters wrapped in a root. Now you can use the root class method and set it to nil or false and it will automatically not require it.

class SessionRefinery < Arcane::Refinery
  def self.root
    false
  end
end

Or if you have a MeRefinery for allowing certain parameters on a /me.json which is still a user object. You can just set a root class method on your refinery and it will use this to determine if the requirements.

class MeRefinery < UserRefinery
  def self.root
    :user
  end
end

Refinery inheritence.

Say you have quite similar needs between two different models, one of them might even have inherited from the other. As arcane's refineries are just regular ruby models you can easily inherit from one to another and it will just work.

class Square
  attr :height, :width
end

class Cube < Square
  attr :depth
end

class SquareRefinery
  def create
    [:height,:width]
  end
end

class CubeRefinery
  def create
    [:depth] + super
  end
end

Building HATEOAS API templates

As your refinery is a class that accepts an object and a user you can invoke it on it's own. This is really good if you want your application to generate endpoints under different scenarios dynamically.


track_params = TrackRefinery.new(Track.new,User.new).create
# => [:title,:description,:duration]

track_url("{track}") + "?" + "{#{track_params.join(",")}}"
# => "http://www.music.com/tracks/{track}?{title,desc,duration}"

Requirements

Currently this gem is only supported for Rails and with any of these ruby versions:

To-do

Contributing

1. Fork us

2. Create your feature branch

git checkout -b my-new-feature

3. Running specs

# Rails 3
env BUNDLE_GEMFILE=./gemfiles/rails3.gemfile bundle exec rspec
# Rails 4
env BUNDLE_GEMFILE=./gemfiles/rails4.gemfile bundle exec rspec

4. Commit your changes

git commit -am 'Add some feature'

5. Push to the branch

git push origin my-new-feature

6. Create new Pull Request