Skip to content

Instantly share code, notes, and snippets.

@the-architect
Created September 15, 2011 16:09
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save the-architect/1219672 to your computer and use it in GitHub Desktop.
Save the-architect/1219672 to your computer and use it in GitHub Desktop.
Automatically create scopes for all available state_machine states in Rails 3.1
class Contact < ActiveRecord::Base
state_machine :state, :initial => :imported do
state :imported
state :contacted
state :verified
state :prospect
state :deleted
end
include StateMachineScopes
state_machine_scopes :state
end
module StateMachineScopes
class MethodAlreadyDefinedError < StandardError; end
class StateMachineNotFoundError < StandardError; end
class NoStateMachineNotFoundError < StandardError; end
module ClassMethods
def all_states(machine = :state)
machine = machine.to_sym
if state_machines.key?(machine)
state_machines[machine.to_sym].states.keys.compact.sort
else
raise StateMachineNotFoundError.new(%Q{"#{self.name}" does not seem to have a state machine called "#{machine}".})
end
end
def state_machine_scopes(*which)
options = which.last.is_a?(Hash) ? which.pop : {}
prefix = options.delete(:prefix)
if which.any?
which.each do |machine_name|
define_scopes_for(machine_name, prefix)
end
else
state_machines.keys.each do |machine_name|
define_scopes_for(machine_name, prefix)
end
end
end
private
def define_scopes_for(machine_name, prefix)
state_machines[machine_name.to_sym].states.keys.compact.each do |state|
scope_name = [prefix, machine_name, state].compact.join('_')
debugger if scope_name.to_s == 'state'
if self.respond_to? scope_name
raise MethodAlreadyDefinedError.new("Already defined method called \"#{scope_name}\"")
end
self.class_eval do
scope :"#{scope_name}", where(:state => state)
end
end
end
end
def self.included(klass)
if klass.respond_to?(:state_machines)
klass.extend(ClassMethods)
else
raise NoStateMachineNotFoundError.new(%Q{"#{klass.name}" does not seem to have any state machines.})
end
end
end
require 'spec_helper'
describe StateMachineScopes do
class TestBase
class << self
def where(*args); end
def scope(name, *args)
scopes << name
end
def scopes
@scopes ||= []
@scopes
end
end
end
before :each do
class ClassWithStateMachine < TestBase
state_machine do
state :initialized
state :ready
end
end
class ClassWithNamedStateMachine < TestBase
state_machine :mystatus do
state :initialized
state :ready
end
end
class ClassWithMultipleStateMachine < TestBase
state_machine :hello do
state :world
state :endofworld
end
state_machine :mystatus do
state :initialized
state :ready
end
end
end
after :each do
Object.send(:remove_const, :ClassWithStateMachine)
Object.send(:remove_const, :ClassWithNamedStateMachine)
Object.send(:remove_const, :ClassWithMultipleStateMachine)
end
context "inclusion" do
it "throws error if no state machines are found" do
expect do
Class.new.send :include, StateMachineScopes
end.to raise_error(StateMachineScopes::NoStateMachineNotFoundError)
end
class ClassWithStateMachineMethod
class << self
def state_machines; end
end
end
it "should have state_machine_scope method" do
ClassWithStateMachineMethod.send :include, StateMachineScopes
ClassWithStateMachineMethod.should respond_to(:state_machine_scopes)
end
end
context "build state machine scopes" do
context "all states" do
it "finds all states for default state machine name" do
ClassWithStateMachine.send :include, StateMachineScopes
ClassWithStateMachine.all_states.should eql [:initialized, :ready]
end
it "finds all states for specific state machine name" do
ClassWithMultipleStateMachine.send :include, StateMachineScopes
ClassWithMultipleStateMachine.all_states(:hello).should eql [:endofworld, :world]
end
it "throws error if state machine is not defined" do
ClassWithMultipleStateMachine.send :include, StateMachineScopes
expect do
ClassWithMultipleStateMachine.all_states(:dostuff)
end.to raise_error(StateMachineScopes::StateMachineNotFoundError)
end
end
context "without arguments" do
it "for default state machine name" do
ClassWithStateMachine.send :include, StateMachineScopes
ClassWithStateMachine.state_machine_scopes
ClassWithStateMachine.scopes.should eql [:state_initialized, :state_ready]
end
it "for named state machine" do
ClassWithNamedStateMachine.send :include, StateMachineScopes
ClassWithNamedStateMachine.state_machine_scopes
ClassWithNamedStateMachine.scopes.should eql [:mystatus_initialized, :mystatus_ready]
end
end
context "with arguments" do
it "only specified state machines" do
ClassWithMultipleStateMachine.send :include, StateMachineScopes
ClassWithMultipleStateMachine.state_machine_scopes(:hello)
ClassWithMultipleStateMachine.scopes.should eql [:hello_world, :hello_endofworld]
end
it "adds prefix" do
ClassWithMultipleStateMachine.send :include, StateMachineScopes
ClassWithMultipleStateMachine.state_machine_scopes(:prefix => :test)
ClassWithMultipleStateMachine.scopes.should eql [:test_hello_world, :test_hello_endofworld, :test_mystatus_initialized, :test_mystatus_ready]
end
it "ignored other options" do
ClassWithMultipleStateMachine.send :include, StateMachineScopes
ClassWithMultipleStateMachine.state_machine_scopes(:foo => :bar)
ClassWithMultipleStateMachine.scopes.should eql [:hello_world, :hello_endofworld, :mystatus_initialized, :mystatus_ready]
end
it "adds prefix and creates only for specified state machines" do
ClassWithMultipleStateMachine.send :include, StateMachineScopes
ClassWithMultipleStateMachine.state_machine_scopes(:hello, :prefix => 'test')
ClassWithMultipleStateMachine.scopes.should eql [:test_hello_world, :test_hello_endofworld]
end
end
end
end
@the-architect
Copy link
Author

now with specs ;)

@ruby-fu-ninja
Copy link

thanks for this :)

@the-architect
Copy link
Author

glad you found use for it :)

@brookr
Copy link

brookr commented May 1, 2012

So great! Thanks for sharing. Looks like on line 41:

 #state_machine_scopes.rb
           scope :"#{scope_name}", where(:state => state)

Should be:

          scope :"#{scope_name}", where(machine_name => state)

(My machine uses :status)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment