Commit 44c7cd62 authored by manas kashyap's avatar manas kashyap

New upstream version 0.5.0

parent c26d885b
state_machines
......@@ -6,6 +6,8 @@ rvm:
- 2.1
- 2.0.0
- 2.2
- 2.3.4
- 2.4.1
- jruby
- rbx-2
matrix:
......
* Fixed inconsistent use of :use_transactions
## 0.5.0
* Fix states being evaluated with wrong `owner_class` context
* Fixed state machine false duplication
* Fixed inconsistent use of :use_transactions
* Namespaced integrations are not registered by default anymore
......
[![Build Status](https://travis-ci.org/state-machines/state_machines.svg?branch=master)](https://travis-ci.org/state-machines/state_machines)
[![Code Climate](https://codeclimate.com/github/state-machines/state_machines.png)](https://codeclimate.com/github/state-machines/state_machines)
[![Code Climate](https://codeclimate.com/github/state-machines/state_machines.svg)](https://codeclimate.com/github/state-machines/state_machines)
# State Machines
State Machines adds support for creating state machines for attributes on any Ruby class.
*Please note that multiple integrations are available for [Active Model](https://github.com/state-machines/state_machines-activemodel), [Active Record](https://github.com/state-machines/state_machines-activerecord), [Mongoid](https://github.com/state-machines/state_machines-mongoid) and more in the [State Machines organisation](https://github.com/state-machines).* If you want to save state in your database, **you need one of these additional integrations**.
## Installation
Add this line to your application's Gemfile:
......@@ -40,10 +42,10 @@ class Vehicle
attr_accessor :seatbelt_on, :time_used, :auto_shop_busy
state_machine :state, initial: :parked do
before_transition parked: :any - :parked, do: :put_on_seatbelt
before_transition parked: any - :parked, do: :put_on_seatbelt
after_transition on: :crash, do: :tow
after_transition on: :repair, :do: :fix
after_transition on: :repair, do: :fix
after_transition any => :parked do |vehicle, transition|
vehicle.seatbelt_on = false
end
......@@ -83,7 +85,7 @@ class Vehicle
event :repair do
# The first transition that matches the state and passes its conditions
# will be used
transition stalled: parked, unless: :auto_shop_busy
transition stalled: :parked, unless: :auto_shop_busy
transition stalled: same
end
......@@ -217,7 +219,7 @@ vehicle.fire_events(:shift_down, :enable_alarm) # => true
vehicle.state_name # => :first_gear
vehicle.alarm_state_name # => :active
vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachines:InvalidTransition: Cannot run events in parallel: ignite, enable_alarm
vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachines:InvalidParallelTransition: Cannot run events in parallel: ignite, enable_alarm
# Human-friendly names can be accessed for states/events
Vehicle.human_state_name(:first_gear) # => "first gear"
......@@ -400,8 +402,8 @@ For example, transitions and callbacks can be defined like so:
class Vehicle
state_machine initial: :parked do
before_transition from: :parked, except_to: :parked, do: :put_on_seatbelt
after_transition to: :parked do |transition|
self.seatbelt = 'off' # self is the record
after_transition to: :parked do |vehicle, transition|
vehicle.seatbelt = 'off'
end
event :ignite do
......@@ -425,7 +427,7 @@ class Vehicle
...
state :parked do
transition to::idling, :on => [:ignite, :shift_up], if: :seatbelt_on?
transition to: :idling, :on => [:ignite, :shift_up], if: :seatbelt_on?
def speed
0
......
......@@ -133,7 +133,7 @@ module StateMachines
# vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
# vehicle.fire_events(:ignite, :disable_alarm) # => true
#
# vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::InvalidTranstion: Cannot run events in parallel: ignite, disable_alarm
# vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::InvalidParallelTransition: Cannot run events in parallel: ignite, disable_alarm
def fire_events!(*events)
run_action = [true, false].include?(events.last) ? events.pop : true
fire_events(*(events + [run_action])) || fail(StateMachines::InvalidParallelTransition.new(self, events))
......
require 'set'
module StateMachines
# Integrations allow state machines to take advantage of features within the
# context of a particular library. This is currently most useful with
......@@ -54,7 +52,6 @@ module StateMachines
alias_method :list, :integrations
# Attempts to find an integration that matches the given class. This will
# look through all of the built-in integrations under the StateMachines::Integrations
# namespace and find one that successfully matches the class.
......@@ -86,7 +83,7 @@ module StateMachines
# == Examples
#
# StateMachines::Integrations.match_ancestors([]) # => nil
# StateMachines::Integrations.match_ancestors(['ActiveRecord::Base']) # => StateMachines::Integrations::ActiveModel
# StateMachines::Integrations.match_ancestors([ActiveRecord::Base]) # => StateMachines::Integrations::ActiveModel
def match_ancestors(ancestors)
integrations.detect { |integration| integration.matches_ancestors?(ancestors) }
end
......@@ -103,7 +100,6 @@ module StateMachines
integrations.detect { |integration| integration.integration_name == name } || raise(IntegrationNotFound.new(name))
end
private
def add(integration)
......
......@@ -24,20 +24,17 @@ module StateMachines
# Whether the integration should be used for the given class.
def matches?(klass)
matches_ancestors?(klass.ancestors.map { |ancestor| ancestor.name })
matching_ancestors.any? { |ancestor| klass <= ancestor }
end
# Whether the integration should be used for the given list of ancestors.
def matches_ancestors?(ancestors)
(ancestors & matching_ancestors).any?
end
end
extend ClassMethods
def self.included(base) #:nodoc:
base.class_eval { extend ClassMethods }
base.extend ClassMethods
end
end
end
......
......@@ -419,7 +419,11 @@ module StateMachines
name = args.first || :state
# Find an existing machine
if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name]
machine = owner_class.respond_to?(:state_machines) &&
(args.first && owner_class.state_machines[name] || !args.first &&
owner_class.state_machines.values.first) || nil
if machine
# Only create a new copy if changes are being made to the machine in
# a subclass
if machine.owner_class != owner_class && (options.any? || block_given?)
......@@ -2046,14 +2050,12 @@ module StateMachines
# the method and is further along in the ancestor chain than this
# machine's helper module.
def owner_class_ancestor_has_method?(scope, method)
return false unless owner_class_has_method?(scope, method)
superclasses = owner_class.ancestors[1..-1].select { |ancestor| ancestor.is_a?(Class) }
if scope == :class
# Use singleton classes
current = (
class << owner_class;
self;
end)
current = owner_class.singleton_class
superclass = superclasses.first
else
current = owner_class
......@@ -2068,14 +2070,16 @@ module StateMachines
# Search for for the first ancestor that defined this method
ancestors.detect do |ancestor|
ancestor = (
class << ancestor;
self;
end) if scope == :class && ancestor.is_a?(Class)
ancestor = ancestor.singleton_class if scope == :class && ancestor.is_a?(Class)
ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
end
end
def owner_class_has_method?(scope, method)
target = scope == :class ? owner_class.singleton_class : owner_class
target.method_defined?(method) || target.private_method_defined?(method)
end
# Adds helper methods for accessing naming information about states and
# events on the owner class
def define_name_helpers
......
......@@ -45,7 +45,7 @@ module StateMachines
#
# paths.from_states # => [:parked, :idling, :first_gear, ...]
def from_states
map {|path| path.from_states}.flatten.uniq
flat_map(&:from_states).uniq
end
# Lists all of the states that can be transitioned to through the paths in
......@@ -55,7 +55,7 @@ module StateMachines
#
# paths.to_states # => [:idling, :first_gear, :second_gear, ...]
def to_states
map {|path| path.to_states}.flatten.uniq
flat_map(&:to_states).uniq
end
# Lists all of the events that can be fired through the paths in this
......@@ -65,7 +65,7 @@ module StateMachines
#
# paths.events # => [:park, :ignite, :shift_up, ...]
def events
map {|path| path.events}.flatten.uniq
flat_map(&:events).uniq
end
private
......
......@@ -2,7 +2,7 @@ module StateMachines
# A state defines a value that an attribute can be in after being transitioned
# 0 or more times. States can represent a value of any type in Ruby, though
# the most common (and default) type is String.
#
#
# In addition to defining the machine's value, a state can also define a
# behavioral context for an object when that object is in the state. See
# StateMachines::Machine#state for more information about how state-driven
......@@ -10,7 +10,7 @@ module StateMachines
class State
# The state machine for which this state is defined
attr_accessor :machine
attr_reader :machine
# The unique identifier for the state used in event and callback definitions
attr_reader :name
......@@ -38,7 +38,7 @@ module StateMachines
attr_accessor :matcher
# Creates a new state within the context of the given machine.
#
#
# Configuration options:
# * <tt>:initial</tt> - Whether this state is the beginning state for the
# machine. Default is false.
......@@ -86,6 +86,11 @@ module StateMachines
@context = StateContext.new(self)
end
def machine=(machine)
@machine = machine
@context = StateContext.new(self)
end
# Determines whether there are any states that can be transitioned to from
# this state. If there are none, then this state is considered *final*.
# Any objects in a final state will remain so forever given the current
......@@ -107,15 +112,15 @@ module StateMachines
end
# Generates a human-readable description of this state's name / value:
#
#
# For example,
#
#
# State.new(machine, :parked).description # => "parked"
# State.new(machine, :parked, :value => :parked).description # => "parked"
# State.new(machine, :parked, :value => nil).description # => "parked (nil)"
# State.new(machine, :parked, :value => 1).description # => "parked (1)"
# State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
#
#
# Configuration options:
# * <tt>:human_name</tt> - Whether to use this state's human name in the
# description or just the internal name
......@@ -129,9 +134,9 @@ module StateMachines
# The value that represents this state. This will optionally evaluate the
# original block if it's a lambda block. Otherwise, the static value is
# returned.
#
#
# For example,
#
#
# State.new(machine, :parked, :value => 1).value # => 1
# State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
# State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0@...>
......@@ -152,14 +157,14 @@ module StateMachines
# Determines whether this state matches the given value. If no matcher is
# configured, then this will check whether the values are equivalent.
# Otherwise, the matcher will determine the result.
#
#
# For example,
#
#
# # Without a matcher
# state = State.new(machine, :parked, :value => 1)
# state.matches?(1) # => true
# state.matches?(2) # => false
#
#
# # With a matcher
# state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
# state.matches?(nil) # => false
......@@ -170,7 +175,7 @@ module StateMachines
# Defines a context for the state which will be enabled on instances of
# the owner class when the machine is in this state.
#
#
# This can be called multiple times. Each time a new context is created,
# a new module will be included in the owner class.
def context(&block)
......@@ -184,7 +189,7 @@ module StateMachines
new_methods = context_methods.to_a.select { |(name, method)| old_methods[name] != method }
# Alias new methods so that the only execute when the object is in this state
new_methods.each do |(method_name, method)|
new_methods.each do |(method_name, _method)|
context_name = context_name_for(method_name)
context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
alias_method :"#{context_name}", :#{method_name}
......@@ -208,7 +213,7 @@ module StateMachines
# Calls a method defined in this state's context on the given object. All
# arguments and any block will be passed into the method defined.
#
#
# If the method has never been defined for this state, then a NoMethodError
# will be raised.
def call(object, method, *args, &block)
......@@ -239,9 +244,9 @@ module StateMachines
end
# Generates a nicely formatted description of this state's contents.
#
#
# For example,
#
#
# state = StateMachines::State.new(machine, :parked, :value => 1, :initial => true)
# state # => #<StateMachines::State name=:parked value=1 initial=true context=[]>
def inspect
......
......@@ -93,7 +93,7 @@ module StateMachines
machine.events.each { |event| order += event.known_states }
order += select { |state| state.context_methods.any? }.map { |state| state.name }
order += keys(:name) - machine.callbacks.values.flatten.map { |callback| callback.known_states }.flatten
order += keys(:name) - machine.callbacks.values.flatten.flat_map(&:known_states)
order += keys(:name)
order.uniq!
......
......@@ -290,7 +290,7 @@ module StateMachines
def pausable
begin
halted = !catch(:halt) { yield; true }
rescue Exception => error
rescue => error
raise unless @resume_block
end
......
......@@ -168,7 +168,7 @@ module StateMachines
def catch_exceptions
begin
yield
rescue Exception
rescue
rollback
raise
end
......@@ -210,7 +210,7 @@ module StateMachines
# Rollback only if exceptions occur during before callbacks
begin
super
rescue Exception
rescue
rollback unless @before_run
@success = nil # mimics ActiveRecord.save behavior on rollback
raise
......
module StateMachines
VERSION = '0.4.0'
VERSION = '0.5.0'
end
This diff is collapsed.
......@@ -12,9 +12,8 @@ Gem::Specification.new do |spec|
spec.homepage = 'https://github.com/state-machines/state_machines'
spec.license = 'MIT'
spec.required_ruby_version = '>= 1.9.3'
spec.required_ruby_version = '>= 2.0.0'
spec.files = `git ls-files -z`.split("\x0")
spec.test_files = spec.files.grep(/^test\//)
spec.require_paths = ['lib']
spec.add_development_dependency 'bundler', '>= 1.7.6'
......
......@@ -2,6 +2,6 @@ module VehicleIntegration
include StateMachines::Integrations::Base
def self.matching_ancestors
['Vehicle']
[Vehicle]
end
end
\ No newline at end of file
require_relative 'model_base'
class Driver < ModelBase
state_machine :status, :initial => :parked do
event :park do
transition :idling => :parked
end
event :ignite do
transition :parked => :idling
end
end
end
require_relative '../../files/models/vehicle'
class Motorcycle < Vehicle
def self.example_class_method(args={})
end
state_machine initial: :idling do
state :first_gear do
def decibels
1.0
end
example_class_method
end
end
end
require_relative '../test_helper'
require_relative '../files/models/driver'
class DriverNonstandardTest < MiniTest::Test
def setup
@driver = Driver.new
@events = Driver.state_machine.events
end
def test_should_have
assert_equal 1, @events.transitions_for(@driver).size
end
end
......@@ -43,4 +43,10 @@ class MotorcycleTest < MiniTest::Test
@motorcycle.shift_up
assert_equal 1.0, @motorcycle.decibels
end
def test_should_not_inherit_from_superclass_if_value_is_set
vehicle = Vehicle.new
@motorcycle.shift_up
assert_equal 0.0, vehicle.decibels
end
end
require_relative '../../test_helper'
class BranchWithMultipleExceptToRequirementsTest < StateMachinesTest
def setup
@object = Object.new
@branch = StateMachines::Branch.new(except_to: [:idling, :parked])
end
def test_should_match_if_not_included
assert @branch.matches?(@object, to: :first_gear)
end
def test_should_not_match_if_included
refute @branch.matches?(@object, to: :idling)
end
def test_should_be_included_in_known_states
assert_equal [:idling, :parked], @branch.known_states
end
end
......@@ -72,7 +72,7 @@ class EventWithMatchingDisabledTransitionsTest < StateMachinesTest
end
machine = StateMachines::Machine.new(klass, integration: :custom, messages: { invalid_transition: 'cannot transition via "%s" from "%s"' })
parked, idling = machine.state :parked, :idling
parked, _idling = machine.state :parked, :idling
parked.human_name = 'stopped'
machine.events << event = StateMachines::Event.new(machine, :ignite)
......
......@@ -21,7 +21,7 @@ class EventWithTransitionWithNilToStateTest < StateMachinesTest
transition = @event.transition_for(@object)
refute_nil transition
assert_equal 'idling', transition.from
assert_equal nil, transition.to
assert_nil transition.to
assert_equal :park, transition.event
end
......@@ -31,6 +31,6 @@ class EventWithTransitionWithNilToStateTest < StateMachinesTest
def test_should_not_change_the_current_state
@event.fire(@object)
assert_equal nil, @object.state
assert_nil @object.state
end
end
......@@ -17,11 +17,13 @@ class IntegrationMatcherTest < StateMachinesTest
end
def test_should_return_nil_if_no_match_found_with_ancestors
assert_nil StateMachines::Integrations.match_ancestors(['Fake'])
fake = Class.new
assert_nil StateMachines::Integrations.match_ancestors([fake])
end
def test_should_return_integration_class_if_match_found_with_ancestors
fake = Class.new
StateMachines::Integrations.register(VehicleIntegration)
assert_equal VehicleIntegration, StateMachines::Integrations.match_ancestors(['Fake', 'Vehicle'])
assert_equal VehicleIntegration, StateMachines::Integrations.match_ancestors([fake, Vehicle])
end
end
......@@ -6,7 +6,7 @@ class MachineWithCustomIntegrationTest < StateMachinesTest
include StateMachines::Integrations::Base
def self.matching_ancestors
['Vehicle']
[Vehicle]
end
end
......@@ -65,7 +65,7 @@ class MachineWithCustomIntegrationTest < StateMachinesTest
MachineWithCustomIntegrationTest::Custom.class_eval do
class << self; remove_method :matching_ancestors; end
def self.matching_ancestors
['Vehicle']
[Vehicle]
end
end
end
......
require_relative '../../test_helper'
class MachineCollectionFireWithValidationsTest < StateMachinesTest
module Custom
include StateMachines::Integrations::Base
def invalidate(object, _attribute, message, values = [])
(object.errors ||= []) << generate_message(message, values)
end
def reset(object)
object.errors = []
end
end
def setup
StateMachines::Integrations.register(MachineCollectionFireWithValidationsTest::Custom)
@klass = Class.new do
attr_accessor :errors
def initialize
@errors = []
super
end
end
@machines = StateMachines::MachineCollection.new
@machines[:state] = @state = StateMachines::Machine.new(@klass, :state, initial: :parked, integration: :custom)
@state.event :ignite do
transition parked: :idling
end
@machines[:alarm_state] = @alarm_state = StateMachines::Machine.new(@klass, :alarm_state, initial: :active, namespace: 'alarm', integration: :custom)
@alarm_state.event :disable do
transition active: :off
end
@object = @klass.new
end
def test_should_not_invalidate_if_transitions_exist
assert @machines.fire_events(@object, :ignite, :disable_alarm)
assert_equal [], @object.errors
end
def test_should_invalidate_if_no_transitions_exist
@object.state = 'idling'
@object.alarm_state = 'off'
refute @machines.fire_events(@object, :ignite, :disable_alarm)
assert_equal ['cannot transition via "ignite"', 'cannot transition via "disable"'], @object.errors
end
def test_should_run_failure_callbacks_if_no_transitions_exist
@object.state = 'idling'
@object.alarm_state = 'off'
@state_failure_run = @alarm_state_failure_run = false
@machines[:state].after_failure { @state_failure_run = true }
@machines[:alarm_state].after_failure { @alarm_state_failure_run = true }
refute @machines.fire_events(@object, :ignite, :disable_alarm)
assert @state_failure_run
assert @alarm_state_failure_run
end
def teardown
StateMachines::Integrations.reset
end
end
......@@ -18,6 +18,6 @@ class TransitionCollectionEmptyWithBlockTest < StateMachinesTest
end
def test_should_use_block_reslut_if_nil
assert_equal nil, @transitions.perform { nil }
assert_nil @transitions.perform { nil }
end
end
require_relative '../../test_helper'
class TransitionCollectionWithBeforeCallbackHaltTest < StateMachinesTest
class TransitionCollectionWithAfterCallbackHaltTest < StateMachinesTest
def setup
@klass = Class.new do
attr_reader :saved
......@@ -16,8 +16,8 @@ class TransitionCollectionWithBeforeCallbackHaltTest < StateMachinesTest
@machine.state :idling
@machine.event :ignite
@machine.before_transition { @before_count += 1; throw :halt }
@machine.before_transition { @before_count += 1 }
@machine.after_transition { @after_count += 1; throw :halt }
@machine.after_transition { @after_count += 1 }
@machine.around_transition { |block| @before_count += 1; block.call; @after_count += 1 }
......@@ -29,23 +29,19 @@ class TransitionCollectionWithBeforeCallbackHaltTest < StateMachinesTest
@result = @transitions.perform
end
def test_should_not_succeed
assert_equal false, @result
def test_should_succeed
assert_equal true, @result
end
def test_should_not_persist_state
assert_equal 'parked', @object.state
def test_should_persist_state
assert_equal 'idling', @object.state
end
def test_should_not_run_action
refute @object.saved
def test_should_run_before_callbacks
assert_equal 2, @before_count
end
def test_should_not_run_further_before_callbacks
assert_equal 1, @before_count
end
def test_should_not_run_after_callbacks
assert_equal 0, @after_count
def test_should_not_run_further_after_callbacks
assert_equal 2, @after_count
end
end
require_relative '../../test_helper'
class TransitionCollectionWithAfterCallbackHaltTest < StateMachinesTest
class TransitionCollectionWithBeforeCallbackHaltTest < StateMachinesTest
def setup
@klass = Class.new do
attr_reader :saved
......@@ -16,8 +16,8 @@ class TransitionCollectionWithAfterCallbackHaltTest < StateMachinesTest
@machine.state :idling
@machine.event :ignite
@machine.before_transition { @before_count += 1; throw :halt }
@machine.before_transition { @before_count += 1 }
@machine.after_transition { @after_count += 1; throw :halt }
@machine.after_transition { @after_count += 1 }
@machine.around_transition { |block| @before_count += 1; block.call; @after_count += 1 }
......@@ -29,19 +29,23 @@ class TransitionCollectionWithAfterCallbackHaltTest < StateMachinesTest
@result = @transitions.perform
end
def test_should_succeed
assert_equal true, @result
def test_should_not_succeed
assert_equal false, @result
end
def test_should_persist_state
assert_equal 'idling', @object.state