ScopieRails

Code Climate Build Status Coverage Status Dependency Status Gem Version

A has_scope alternative: scopie for Rails.

ScopieRails allows you to map incoming controller parameters to named scopes in your resources through OO design.

Motivation:

  • Dedicated class for scopes mapping, so that the logic is isolated and your controller is skinny.
  • Ability to override default mapping behavior by definition of a method having the same name as a scope in the scopie class.
  • Ability to use the object oriented approach to DRY your custom scopes mapping logic and reuse the scopie class.

Imagine the following model called graduations:

class Graduation < ActiveRecord::Base

  scope :featured, -> { where(featured: true) }
  scope :by_degree, -> (degree) { where(degree: degree) }
  scope :by_period, -> (started_at, ended_at) { where('started_at = ? AND ended_at = ?', started_at, ended_at) }

  scope :created_at_greater_than, ->(date) { where('created_at >= ?', date.beginning_of_day) }
  scope :created_at_less_than, ->(date) { where('created_at <= ?', date.end_of_day) }
  scope :updated_at_greater_than, ->(date) { where('updated_at >= ?', date.beginning_of_day) }
  scope :updated_at_less_than, ->(date) { where('updated_at <= ?', date.end_of_day) }

end

You can use those named scopes as filters by declaring them on your scopie:

class GraduationsScopie < ScopieRails::Base

  has_scope :featured, type: :boolean
  has_scope :by_degree, :by_period

  has_scope :created_at_greater_than, in: :created_at, as: :start_at
  has_scope :created_at_less_than, in: :created_at, as: :end_at

  has_scope :updated_at_greater_than, in: :updated_at, as: :start_at, type: :date
  has_scope :updated_at_less_than, in: :updated_at, as: :end_at, type: :date

  has_scope :page, default: 1
  has_scope :per, default: 30

  def by_period(scope, value, _hash)
    started_at = value[:started_at]
    ended_at = value[:ended_at]

    started_at && ended_at && scope.by_period(started_at, ended_at)
  end

  def created_at_greater_than(scope, value, _hash)
    scope.created_at_greater_than(parse_date(value))
  end

  def created_at_less_than(scope, value, _hash)
    scope.created_at_less_than(parse_date(value))
  end

  def updated_at_greater_than(scope, value, _hash)
    scope.updated_at_greater_than(value)
  end

  def updated_at_less_than(scope, value, _hash)
    scope.updated_at_less_than(value)
  end

  private

  def parse_date(value)
    Date.parse(value)
  end

end

Now, if you want to apply them to an specific resource, you just need to call apply_scopes:

class GraduationsController < ApplicationController
  include ScopieRails::Controller

  def index
    @graduations = apply_scopes(Graduation).all
  end
end

Then for each request:

/graduations
#=> acts like a normal request

/graduations?featured=true
#=> calls the named scope and bring featured graduations

/graduations?by_period[started_at]=20100701&by_period[ended_at]=20101013
#=> brings graduations in the given period

/graduations?created_at[start_at]=2016-06-01&created_at[end_at]=2016-06-02
#=> brings graduations created in the given period

/graduations?featured=true&by_degree=phd
#=> brings featured graduations with phd degree

Installation

Add scopie_rails to your Gemfile or install it from Rubygems.

gem 'scopie_rails'

Mutation testing

mutant --include lib --require scopie_rails --use rspec ScopieRails*

Options

Scopie supports several options:

  • :type - Coerces the type of the parameter sent. Available options: boolean, integer, float, date.

  • :only - In which actions the scope is applied.

  • :except - In which actions the scope is not applied.

  • :as - The key in the params hash expected to find the scope. Defaults to the scope name.

  • :in - The key in the params hash expected to contain a hash holding scope name as a value.

  • :allow_blank - Blank values are not sent to scopes by default. Set to true to overwrite.

  • :ignore_blank - Set to true to not apply the scope if blank value is given.

  • :default - Default value for the scope. Whenever supplied the scope is always called.

Rails

ScopieRails provides Rails integration for Scopie.

Among other things it adds a controller method to scopies.

Thanks

Scopie was inspired by has_scope and pundit.

Thanks to both.