Giter Site home page Giter Site logo

state-machines / state_machines Goto Github PK

View Code? Open in Web Editor NEW
795.0 18.0 91.0 596 KB

Adds support for creating state machines for attributes on any Ruby class

Home Page: https://github.com/state-machines/state_machines

License: MIT License

Ruby 100.00%
state-machine ruby

state_machines's Introduction

Build Status Code Climate

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, Active Record, Mongoid and more in the State Machines organisation. 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:

gem 'state_machines'

And then execute:

$ bundle

Or install it yourself as:

$ gem install state_machines

Usage

Example

Below is an example of many of the features offered by this plugin, including:

  • Initial states
  • Namespaced states
  • Transition callbacks
  • Conditional transitions
  • State-driven instance behavior
  • Customized state values
  • Parallel events
  • Path analysis

Class definition:

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
    
    after_transition on: :crash, do: :tow
    after_transition on: :repair, do: :fix
    after_transition any => :parked do |vehicle, transition|
      vehicle.seatbelt_on = false
    end

    after_failure on: :ignite, do: :log_start_failure

    around_transition do |vehicle, transition, block|
      start = Time.now
      block.call
      vehicle.time_used += Time.now - start
    end

    event :park do
      transition [:idling, :first_gear] => :parked
    end

    event :ignite do
      transition stalled: same, parked: :idling
    end

    event :idle do
      transition first_gear: :idling
    end

    event :shift_up do
      transition idling: :first_gear, first_gear: :second_gear, second_gear: :third_gear
    end

    event :shift_down do
      transition third_gear: :second_gear, second_gear: :first_gear
    end

    event :crash do
      transition all - [:parked, :stalled] => :stalled, if: ->(vehicle) {!vehicle.passed_inspection?}
    end

    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: same
    end

    state :parked do
      def speed
        0
      end
    end

    state :idling, :first_gear do
      def speed
        10
      end
    end

    state all - [:parked, :stalled, :idling] do
      def moving?
        true
      end
    end

    state :parked, :stalled, :idling do
      def moving?
        false
      end
    end
  end

  state_machine :alarm_state, initial: :active, namespace: :'alarm' do
    event :enable do
      transition all => :active
    end

    event :disable do
      transition all => :off
    end

    state :active, :value => 1
    state :off, :value => 0
  end

  def initialize
    @seatbelt_on = false
    @time_used = 0
    @auto_shop_busy = true
    super() # NOTE: This *must* be called, otherwise states won't get initialized
  end

  def put_on_seatbelt
    @seatbelt_on = true
  end

  def passed_inspection?
    false
  end

  def tow
    # tow the vehicle
  end

  def fix
    # get the vehicle fixed by a mechanic
  end

  def log_start_failure
    # log a failed attempt to start the vehicle
  end
end

Note the comment made on the initialize method in the class. In order for state machine attributes to be properly initialized, super() must be called. See StateMachines:MacroMethods for more information about this.

Using the above class as an example, you can interact with the state machine like so:

vehicle = Vehicle.new           # => #<Vehicle:0xb7cf4eac @state="parked", @seatbelt_on=false>
vehicle.state                   # => "parked"
vehicle.state_name              # => :parked
vehicle.human_state_name        # => "parked"
vehicle.parked?                 # => true
vehicle.can_ignite?             # => true
vehicle.ignite_transition       # => #<StateMachines:Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
vehicle.state_events            # => [:ignite]
vehicle.state_transitions       # => [#<StateMachines:Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
vehicle.speed                   # => 0
vehicle.moving?                 # => false

vehicle.ignite                  # => true
vehicle.parked?                 # => false
vehicle.idling?                 # => true
vehicle.speed                   # => 10
vehicle                         # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true>

vehicle.shift_up                # => true
vehicle.speed                   # => 10
vehicle.moving?                 # => true
vehicle                         # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>

# A generic event helper is available to fire without going through the event's instance method
vehicle.fire_state_event(:shift_up) # => true

# Call state-driven behavior that's undefined for the state raises a NoMethodError
vehicle.speed                   # => NoMethodError: super: no superclass method `speed' for #<Vehicle:0xb7cf4eac>
vehicle                         # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>

# The bang (!) operator can raise exceptions if the event fails
vehicle.park!                   # => StateMachines:InvalidTransition: Cannot transition state via :park from :second_gear

# Generic state predicates can raise exceptions if the value does not exist
vehicle.state?(:parked)         # => false
vehicle.state?(:invalid)        # => IndexError: :invalid is an invalid name

# Namespaced machines have uniquely-generated methods
vehicle.alarm_state             # => 1
vehicle.alarm_state_name        # => :active

vehicle.can_disable_alarm?      # => true
vehicle.disable_alarm           # => true
vehicle.alarm_state             # => 0
vehicle.alarm_state_name        # => :off
vehicle.can_enable_alarm?       # => true

vehicle.alarm_off?              # => true
vehicle.alarm_active?           # => false

# Events can be fired in parallel
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: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"
Vehicle.human_alarm_state_name(:active)             # => "active"

Vehicle.human_state_event_name(:shift_down)         # => "shift down"
Vehicle.human_alarm_state_event_name(:enable)       # => "enable"

# States / events can also be references by the string version of their name
Vehicle.human_state_name('first_gear')              # => "first gear"
Vehicle.human_state_event_name('shift_down')        # => "shift down"

# Available transition paths can be analyzed for an object
vehicle.state_paths                                       # => [[#<StateMachines:Transition ...], [#<StateMachines:Transition ...], ...]
vehicle.state_paths.to_states                             # => [:parked, :idling, :first_gear, :stalled, :second_gear, :third_gear]
vehicle.state_paths.events                                # => [:park, :ignite, :shift_up, :idle, :crash, :repair, :shift_down]

# Possible states can be analyzed for a class
Vehicle.state_machine.states.to_a                   # [#<StateMachines::State name=:parked value="parked" initial=true>, #<StateMachines::State name=:idling value="idling" initial=false>, ...]
Vehicle.state_machines[:state].states.to_a          # [#<StateMachines::State name=:parked value="parked" initial=true>, #<StateMachines::State name=:idling value="idling" initial=false>, ...]

# Find all paths that start and end on certain states
vehicle.state_paths(:from => :parked, :to => :first_gear) # => [[
                                                          #       #<StateMachines:Transition attribute=:state event=:ignite from="parked" ...>,
                                                          #       #<StateMachines:Transition attribute=:state event=:shift_up from="idling" ...>
                                                          #    ]]
# Skipping state_machine and writing to attributes directly
vehicle.state = "parked"
vehicle.state                   # => "parked"
vehicle.state_name              # => :parked

# *Note* that the following is not supported (see StateMachines:MacroMethods#state_machine):
# vehicle.state = :parked

Additional Topics

Explicit vs. Implicit Event Transitions

Every event defined for a state machine generates an instance method on the class that allows the event to be explicitly triggered. Most of the examples in the state_machine documentation use this technique. However, with some types of integrations, like ActiveRecord, you can also implicitly fire events by setting a special attribute on the instance.

Suppose you're using the ActiveRecord integration and the following model is defined:

class Vehicle < ActiveRecord::Base
  state_machine initial: :parked do
    event :ignite do
      transition parked: :idling
    end
  end
end

To trigger the ignite event, you would typically call the Vehicle#ignite method like so:

vehicle = Vehicle.create    # => #<Vehicle id=1 state="parked">
vehicle.ignite              # => true
vehicle.state               # => "idling"

This is referred to as an explicit event transition. The same behavior can also be achieved implicitly by setting the state event attribute and invoking the action associated with the state machine. For example:

vehicle = Vehicle.create        # => #<Vehicle id=1 state="parked">
vehicle.state_event = 'ignite'  # => 'ignite'
vehicle.save                    # => true
vehicle.state                   # => 'idling'
vehicle.state_event             # => nil

As you can see, the ignite event was automatically triggered when the save action was called. This is particularly useful if you want to allow users to drive the state transitions from a web API.

See each integration's API documentation for more information on the implicit approach.

Symbols vs. Strings

In all of the examples used throughout the documentation, you'll notice that states and events are almost always referenced as symbols. This isn't a requirement, but rather a suggested best practice.

You can very well define your state machine with Strings like so:

class Vehicle
  state_machine initial: 'parked' do
    event 'ignite' do
      transition 'parked' => 'idling'
    end

    # ...
  end
end

You could even use numbers as your state / event names. The important thing to keep in mind is that the type being used for referencing states / events in your machine definition must be consistent. If you're using Symbols, then all states / events must use Symbols. Otherwise you'll encounter the following error:

class Vehicle
  state_machine do
    event :ignite do
      transition parked: 'idling'
    end
  end
end

# => ArgumentError: "idling" state defined as String, :parked defined as Symbol; all states must be consistent

There is an exception to this rule. The consistency is only required within the definition itself. However, when the machine's helper methods are called with input from external sources, such as a web form, state_machine will map that input to a String / Symbol. For example:

class Vehicle
  state_machine initial: :parked do
    event :ignite do
      transition parked: :idling
    end
  end
end

v = Vehicle.new     # => #<Vehicle:0xb71da5f8 @state="parked">
v.state?('parked')  # => true
v.state?(:parked)   # => true

Note that none of this actually has to do with the type of the value that gets stored. By default, all state values are assumed to be string -- regardless of whether the state names are symbols or strings. If you want to store states as symbols instead you'll have to be explicit about it:

class Vehicle
  state_machine initial: :parked do
    event :ignite do
      transition parked: :idling
    end

    states.each do |state|
      self.state(state.name, :value => state.name.to_sym)
    end
  end
end

v = Vehicle.new     # => #<Vehicle:0xb71da5f8 @state=:parked>
v.state?('parked')  # => true
v.state?(:parked)   # => true

Syntax flexibility

Although state_machine introduces a simplified syntax, it still remains backwards compatible with previous versions and other state-related libraries by providing some flexibility around how transitions are defined. See below for an overview of these syntaxes.

Verbose syntax

In general, it's recommended that state machines use the implicit syntax for transitions. However, you can be a little more explicit and verbose about transitions by using the :from, :except_from, :to, and :except_to options.

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 |vehicle, transition|
      vehicle.seatbelt = 'off'
    end

    event :ignite do
      transition from: :parked, to: :idling
    end
  end
end

Transition context

Some flexibility is provided around the context in which transitions can be defined. In almost all examples throughout the documentation, transitions are defined within the context of an event. If you prefer to have state machines defined in the context of a state either out of preference or in order to easily migrate from a different library, you can do so as shown below:

class Vehicle
  state_machine initial: :parked do
    ...

    state :parked do
      transition to: :idling, :on => [:ignite, :shift_up], if: :seatbelt_on?

      def speed
        0
      end
    end

    state :first_gear do
      transition to: :second_gear, on: :shift_up

      def speed
        10
      end
    end

    state :idling, :first_gear do
      transition to: :parked, on: :park
    end
  end
end

In the above example, there's no need to specify the from state for each transition since it's inferred from the context.

You can also define transitions completely outside the context of a particular state / event. This may be useful in cases where you're building a state machine from a data store instead of part of the class definition. See the example below:

class Vehicle
  state_machine initial: :parked do
    ...

    transition parked: :idling, :on => [:ignite, :shift_up]
    transition first_gear: :second_gear, second_gear: :third_gear, on: :shift_up
    transition [:idling, :first_gear] => :parked, on: :park
    transition all - [:parked, :stalled]: :stalled, unless: :auto_shop_busy?
  end
end

Notice that in these alternative syntaxes:

  • You can continue to configure :if and :unless conditions
  • You can continue to define from states (when in the machine context) using the all, any, and same helper methods

Static / Dynamic definitions

In most cases, the definition of a state machine is static. That is to say, the states, events and possible transitions are known ahead of time even though they may depend on data that's only known at runtime. For example, certain transitions may only be available depending on an attribute on that object it's being run on. All of the documentation in this library define static machines like so:

class Vehicle
  state_machine :state, initial: :parked do
    event :park do
      transition [:idling, :first_gear] => :parked
    end

    ...
  end
end

However, there may be cases where the definition of a state machine is dynamic. This means that you don't know the possible states or events for a machine until runtime. For example, you may allow users in your application to manage the state machine of a project or task in your system. This means that the list of transitions (and their associated states / events) could be stored externally, such as in a database. In a case like this, you can define dynamically-generated state machines like so:

class Vehicle
  attr_accessor :state

  # Make sure the machine gets initialized so the initial state gets set properly
  def initialize(*)
    super
    machine
  end

  # Replace this with an external source (like a db)
  def transitions
    [
      {parked: :idling, on: :ignite},
      {idling: :first_gear, first_gear: :second_gear, on: :shift_up}
      # ...
    ]
  end

  # Create a state machine for this vehicle instance dynamically based on the
  # transitions defined from the source above
  def machine
    vehicle = self
    @machine ||= Machine.new(vehicle, initial: :parked, action: :save) do
      vehicle.transitions.each {|attrs| transition(attrs)}
    end
  end

  def save
    # Save the state change...
    true
  end
end

# Generic class for building machines
class Machine
  def self.new(object, *args, &block)
    machine_class = Class.new
    machine = machine_class.state_machine(*args, &block)
    attribute = machine.attribute
    action = machine.action

    # Delegate attributes
    machine_class.class_eval do
      define_method(:definition) { machine }
      define_method(attribute) { object.send(attribute) }
      define_method("#{attribute}=") {|value| object.send("#{attribute}=", value) }
      define_method(action) { object.send(action) } if action
    end

    machine_class.new
  end
end

vehicle = Vehicle.new                   # => #<Vehicle:0xb708412c @state="parked" ...>
vehicle.state                           # => "parked"
vehicle.machine.ignite                  # => true
vehicle.machine.state                   # => "idling"
vehicle.state                           # => "idling"
vehicle.machine.state_transitions       # => [#<StateMachines:Transition ...>]
vehicle.machine.definition.states.keys  # => :first_gear, :second_gear, :parked, :idling

As you can see, state_machine provides enough flexibility for you to be able to create new machine definitions on the fly based on an external source of transitions.

Dependencies

Ruby versions officially supported and tested:

  • Ruby (MRI) 2.6.0+
  • JRuby
  • Rubinius

For graphing state machine:

For documenting state machines:

TODO

  • Add matchers/assertions for rspec and minitest

Contributing

  1. Fork it ( https://github.com/state-machines/state_machines/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

state_machines's People

Contributors

23shortstop avatar alouanemed avatar brandoncc avatar byroot avatar danhodge avatar danielepalombo avatar eric-guo avatar gagalago avatar ghiculescu avatar huoxito avatar ipoval avatar jeff-hamm avatar julitrows avatar kfalconer avatar majioa avatar nebtrx avatar olleolleolle avatar petergoldstein avatar philihp avatar rafaelfranca avatar rosskevin avatar seuros avatar tonymarklove avatar v-kolesnikov avatar zmokizmoghi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

state_machines's Issues

Unable to call the calling method of the state machine from within the state machine

Sorry if this is in the wrong place.

I have the following state machine and I'm trying to send an email after the state changes:

state_machine :state, initial: :unapproved do
 event :reject_it do
   transition [:approved, :unapproved] => :rejected
  PurchaseRequestsMailer.purchase_request_rejected(self).send
 end
end

My problem is that self refers to the statemachine object, not the object that is calling the state machine, is there anyway to access that object that's calling the state machine?

Exception within transition not raised

When writing tests for some after_transition callbacks I haven't seen exceptions being raised within the callback.

Example:

after_transition, :on => :finish, :do => :finish_it

def finish_it
  foo
end

I'd expect a NameError to be raised but the test continues execution. Is this intended behavior?

Localization / input collection

Hi. I try to use state_machines for my project. Could you please explain, are there collection_helpers for forms and an integration with I18n?

Thank you.

Validation with state as constant aren't working

Hi,
Writing this issue because I use your gem and I have one small issue :

My state are constant, like that :
STATES = [ STATE_INTERNAL_DELIVERY = 'internal_delivery', STATE_DELIVERABLES_PENDING_REVIEW = 'deliverables_pending_review', STATE_FINISHING_STEP = 'finishing_step' ].freeze

and I'm trying to do a validation with :
state STATE_FINISHING_STEP, STATE_DELIVERABLES_PENDING_REVIEW, STATE_INTERNAL_DELIVERY do validate :validate_delivery_possible end

Which will run only on the last state.

But if I do this :

state 'deliverables_pending_review', 'internal_delivery', 'finishing_step' do validate :validate_delivery_possible end

It's running as expected.

Do you know why ?

Multiple initial states

Hi,

It is possible to have multiple initial states? I'd like a statement machine to work like this:

unsubmitted -> pending_approval -> approved

with events submit and approve that move between these three states

But I'd also like to have a state called 'template'. For an object created with state template it should not be able to transition to any other state. I'd happy to explicitly state the desired initial state at creation.

I attempted something like this, but it didn't work:

  state_machine :state, initial: [:unsubmitted, :template] do
    event :submit do
      transition :unsubmitted => :pending_approval
    end

    event :approve do
      transition :pending_approval => :approved
    end

Is this possible? I'd rather not do this, as I need to create a 'template' object atomically.

  state_machine :state, initial: :unsubmitted do
    event :submit do
      transition :unsubmitted => :pending_approval
    end

    event :approve do
      transition :pending_approval => :approved
    end

    event :make_template do
      transition :unsubmitted => :template
    end
  end

Although, that said, perhaps the event only needs to exist to make the state machine aware of the state, and I can do this in ActiveRecord: MyObject.create!(state: :template, ...) and it's good enough.

after_transition syntax or bug?

This fired my method:

after_transition all => :paid, do: :process_payment

This did not:

after_transition on: :paid, do: :process_payment

Bug or am I misunderstand the :on as a shortcut for all?

Can not access the class

So I am using Virtus with State Machine and it seems with this approach state machine can not access the class.

Any thoughts?

Calling state_paths() results in infinite loop

Afternoon Folks,

I have a machine defined which allows fairly broad transitions to/from various states as follows:

state_machine :state, initial: :draft do
	# When the incident is being investigated by the team.
	transition all => :under_investigation, :on => :under_investigation
	# Transition back to an open state.
	transition all => :open, :on => :open
	# When the issue is resolved, but taking time to recover.
	transition all => :in_recovery, :on => :in_recovery
	#ย Transition to a closed state.
	# Don't allow posting direct to closed, must be open first.
	transition all - [:draft] => :closed, :on => :close
end

However, when calling state_paths() on this object it just appears to hang or get caught in some form of an infinite loop. If I remove either :in_recovery or :under_investigation state/transition the problem appears to go away.

Any suggestions on what might be causing this? Am I doing something fundamentally wrong? Or is this a bug in the gem somewhere?

This seems to be the case running various 0.4.x and 0.5.x builds of state_machines. (May also be an issue further back)

Appreciate any insight.

Rob

Possible events calculation

first issue is to show buttons for available events with the object. so, i solved it with code:

.actions
  -@mission.state_paths(from: @mission.state, deep: false, guard: false).map{|a|a[0]}.uniq.map(&:event).map do |event|
    -begin
      =render "#{event.to_s}", mission: @mission, formats: [:html]
    -rescue
      =button event.to_s, '#', disabled: true

it works just fine until i add loops into events logic, for example, active mission can have event "take" which doesn't switch state, or some actions allow going back in actions. state_paths returns thousands of variants and takes several seconds, which makes it unusable

is there any method to quickly calculate next possible event?

not sure of an issue... maybe just lake of knownloedge and effective information in my situation.

on rails-4.2 with ruby-2.2.2, after installed the_teacher/the_comments, i have problem to submit comment due to server error 500 point on active_machine error.
here is the error:
"NoMethodError (protected method `around_validation' called for #StateMachine::Machine:0x0000000624a990):"

then after looking around this to fix, i see a patch find by this one (the_teacher), but would like to upgrade rails to something really works fine without patches things. Then i start to read some posts/issues around this. And try to install this gem.
I'm not sure to real understand what's happen due to big datas posted (i sink under all of these datas). So i don't know how to do to really use one of your fix for fix state_machine (i never find any clear instruction for do that... so i'm not an expert, for sure).
I would be very happy to read a pragmatic instructions article around "how to fix state-machine".
Or if this is an issue, to know how to find a nice solution.
Thanks for all.

Method exception doesn't halt transitions

I have the following defined:
before_transition ready_to_save: :screen_name_saved, do: :create_screen_name

I would expect that when create_screen_name raises an exception that the transitions would be halted.

fire_state_event!

I can't find the bang version of fire_state_event (throw error instead of returning false). What is the best way to add this feature?

Transition handlers are called called twice on a single event

Perhaps I am missing something here, but I cannot see any real reason why the :after_transition handler in this code is called twice in this very simple example:

class StateMachineActivity 
  state_machine :state, initial: :created do
    after_transition to: :finished do |activity, transition|
      puts "Finished."
    end
    event :next do
      transition :created => :finished 
    end
  end
end

In the console, test with

>> a = StateMachineActivity.new
#<StateMachineActivity:0x00000001300150 @state="created">
>> a.next
Finished.
Finished.
true

I would expect Finished to be printed only once. Inspecting the call stack shows callbacks for the after_transition are registered TWICE, but I cannot determine why or where.

Suggestions? Ideas?

State persistence

Can we make the state (string or integer) persistent to ActiveModel?

State predicates should not override existing methods

When defining a state machine, Machine#define_state_predicate first checks to see whether the method it would add is already defined or not. However, when defining a state, State#add_predicate does no such check.

We had a machine with a destroyed check work fine until we started to upgrade to Rails 5.2, because that version of Rails adds a check for destroyed? in its create_or_update method.

Ideally, there would be a warning just like there is when defining a state already defined on another state machine on the same model.

edit: from this test case, it looks like this should already work?

Should state machine methods override existing class methods?

I think this concept could be argued either way so I am presenting it as a question than an issue. In this trivial example, the class has a method importance. When using the state machine, I expected the importance method to be overridden by the state machine when state == :on, but that does not happen.

Should the state machine override the method when the state applies?

class MyModel
  def importance
     99
  end

  state_machine do
    state :on do
       def importance
         1
       end
    end
    state :off do
    end
  end
end
> m.state = :on
> m.importance
99     <---- expected to be 1, the state machine method is not being used

Thank you!

ActiveRecord

Is ActiveRecord supported like in the 'state_machine' gem?
State does not seem to be persisted with a database field of 'state'...
Am I missing something?

Repetetive DSL in some cases with separate after_transition vs. including do: in the transition

It appears that I cannot specify do: on a transition definition such as

      event :expire do
        transition :active => :expired, do: :process_superceded, if: [:has_next?, :ended?]
        transition :trial => :trial_expired, do: :process_trial_expiration, if: :ended?
        transition :active => :grace, do: :process_grace, if: :within_grace_period?
        transition [:active, :grace] => :expired, do: :process_expired, if: :exceeded_grace_period?
      end

If that's the case, I'll have to copy a very similar block to create after_transitions. This appears to be the only way:

      event :expire do
        transition :active => :expired, if: [:has_next?, :ended?]
        transition :trial => :trial_expired, if: :ended?
        transition :active => :grace, if: :within_grace_period?
        transition [:active, :grace] => :expired, if: :exceeded_grace_period?
      end

      after_transition :active => :expired, do: :process_superceded
      after_transition :trial => :trial_expired
      after_transition :active => :grace, do: :process_grace
      after_transition [:active, :grace] => :expired

This seems quite verbose and prone to error upon changes since I'm explicitly mirroring state transitions defined above. Am I correct in my understanding? Is there a reason not to allow do: on the transition?

Actually, there's a nuance that I can capture with the desired syntax, that I haven't with the second syntax: only run process_superceded when [:has_next?, :ended?], so do I need to copy those conditions too? The unexpected behavior comes with the potential for :active => :expired without meeting those conditions.

upgrade path from state_machine?

Hi,

Thanks for this great project. I didn't know about it until a couple days ago.

I think many of your users will be transitioning here from the state_machine gem. Do you think you could add a couple lines to the readme explaining what the upgrade path is? Is it a seamless transition?

If you answer in this issue then I'll be happy to submit a PR.

Thanks!

human_state_name is not working on the initial state

Consider this state machine configuration...

class MyClass
  state_machine :state, initial: :pending do
    state :pending, human_name: "To Do"
    state :in_progress, human_name: "In Progress"
  end
end

Attempting to fetch the human state name...

irb > MyClass.human_state_name :pending
=> "pending"

Other states work fine. The initial state is not being translated. Thanks.

Doesn't seem to respect conditional validations

I have a model with a conditional validation

validates :nda_accepted,
  presence: true,
  unless: :draft?

but if I try to transition it fails.

>> Rfq.last.valid?
=> true
>> Rfq.last.delete_draft!
StateMachines::InvalidTransition: Cannot transition state via :delete_draft from :draft (Reason(s): Nda accepted can't be blank)
from /Users/mmoen/code/UnderpantsGnome/britehub-app/vendor/bundle/ruby/2.2.0/gems/state_machines-0.4.0/lib/state_machines/event.rb:224:in `block in add_actions'

.state returning nil

I just switched over from pluginaweek's repo, and now all tests fail because state is returning nil.

state_machine :state, initial: :inactive do

    before_transition any => :active, do: :archive_currently_active_adjustment
    after_transition  any => :active, do: :update_employee_current_salary!

    event :activate do
      transition any => :active
    end

    event :archive do
      transition any => :historical
    end

    event :queue do
      transition any => :pending
    end

  end

def initialize
    super() # NOTE: This *must* be called, otherwise states won't get initialized
  end

Here's a sample from the console:

SalaryAdjustment Load (13.1ms)  SELECT  "salary_adjustments".* FROM "salary_adjustments" WHERE "salary_adjustments"."deleted_at" IS NULL  ORDER BY effective_date ASC LIMIT 1
 => #<SalaryAdjustment id: 48750, employee_id: 7304, scenario_id: nil, created_at: "2012-09-17 17:32:40", updated_at: "2012-09-21 15:29:08", memo: "6mo review", effective_date: "12-12-0001", position_title: "Teller", date_in_position: "08-18-2011", status: "inactive", new_wages: #<BigDecimal:7fea35ccbce0,'0.9E1',9(18)>, state: "historical", deleted_at: nil>

You can can that state is set to 'historical'. But...

2.2.2 :002 > SalaryAdjustment.last.state
  SalaryAdjustment Load (9.8ms)  SELECT  "salary_adjustments".* FROM "salary_adjustments" WHERE "salary_adjustments"."deleted_at" IS NULL  ORDER BY effective_date ASC LIMIT 1
 => nil

Find the required states for an event without an instance

Hi!

How can I find which states can an event transition from without an instance of the underlying model?

I have this code working that uses the machine states, their branches and their state_requirements values, but it seems too involved and coupled to the library.

Thanks.

Implicit event transitions not possible - NoMethodError: undefined method `status_event='

I have some ActveRecord based model like this:

class MyModel < ActiveRecord::Base
  state_machine :status, initial: :initial do
    event :order_purchased do
      transition [:initial, :ordered] => :ordered
    end

    state :initial
    state :ordered
  end
end

I want to implicitly trigger the order_purchased event and update other attributes at the same time. When using state_machine this worked like so:

my_model.update_attributes status_event: :order_purchased, other_attribute: 'value'

With state_machines, however, I get an error:

ActiveRecord::UnknownAttributeError: unknown attribute: status_event

Also trying something like given in the README of state_machines (copied from state_machine) like the following:

my_model.status_event = 'order_purchased'

Does yield

NoMethodError: undefined method `status_event=' for #<MyModel:0x4c7a0338>

Ruby 2.7 kwarg deprecation warnings on generated bang methods

Hello!

I use this gem along with the AR extension on a Rails app on Ruby 2.7. We override some of the transition methods for the state machine with kwargs, and we've started seeing the infamous Ruby 2.7 kwarg deprecation warnings when calling the unsafe versions of the transition methods.

# on the Car model with the state machine

def ignite(driver:)
  update(driver: driver)
  super()
end

Calling something like car.ignite!(driver: current_user) from somewhere in the code will issue a warning:

/Users/julio/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/state_machines-0.5.0/lib/state_machines/event.rb:224: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/Users/julio/code/state_machine_test/app/models/car.rb:9: warning: The called method `ignite' is defined here

I've created this small Rails app with some test code that reproduces the issue: https://github.com/julitrows/state_machine_test

Doing some diggning, it seems to go down to the StateMachines::Event class:

https://github.com/state-machines/state_machines/blob/master/lib/state_machines/event.rb#L224-L227

machine.define_helper(:instance, "#{qualified_name}!") do |machine, object, *args|
  object.send(qualified_name, *args) || raise(StateMachines::InvalidTransition.new(object, machine, name))
end

It seems that object.send(qualified_name, *args) should be sending object.send(qualified_name, **args) in this particular case, but it probably would need to first detect what's *args (an array or a hash) before doing one or the other, to cover all cases.

NOTE I think this does not happen for the other helper definitions in that file.

I'm happy to send a PR to fix this, but I'm having trouble finding a test or tests in the gem codebase that would help reproduce the issue on the gem itself so I can start fixing.

Any help or pointers would be appreciated!

Thanks!

Add `with_state` method

Migrating from state_machine, I am missing the .with_state option. Could it be included to this gem to make it more compatible?

Syntax:

Model.with_state([:new, :confirmed]

queries for objects WHERE state IN (?), ["new", "confirmed"]

Issue with Rails 4.2

I cannot seem to make it work with ActiveRecord. The initial state is never set correctly. I am using Ruby 2.1.x and Rails 4.2.x. Gem is 0.4.0

I have a Model that has a state field. State machine goes from pending, to starting to running to done

So pretty simple. But something is not working right (was working with 4.0 and the older version of state_machine (the one your forked from).)

So I am upgrading rails and finding that the old one is unmaintained and trying yours. But still not working properly for me

 f = FeedCrawlingRun.new
=> #<FeedCrawlingRun:0x007fc7b4ebccf0
 id: nil,
 started_at: nil,
 stopped_at: nil,
 state: nil,
 statistics: {},
 downloaded_files: {},
 feed_id: nil,
 created_at: nil,
 updated_at: nil>

Notice that state is nil . if I try to created it, it is never initialize correctly either

but if I do a f.starting? or f.pending? , it show the correct state. The state also shows with f.state.. But it is never saved to DB...

what am I missing?

How to specify conditional state changes in dynamic state definitions

I had a previous state machine like the following

state_machine :state, :initial => :awaiting_quote, use_transactions: false do
  event :submit_quote do
    transition :awaiting_quote => :awaiting_quote_acceptance, 
              if: :line_item_details_complete?
  end
  event :accept_quote do
    transition :awaiting_quote_acceptance => :awaiting_final_invoice
  end
  event :decline_quote do
    transition :awaiting_quote_acceptance => :awaiting_quote
  end
  event :submit_final_invoice do
    transition :awaiting_final_invoice => :awaiting_order_acceptance,
              if: :attachment?
  end
  event :accept_order do
    transition :awaiting_order_acceptance => :order_placed
  end
end

as you can see 2 of the state changes are dependent on line_item_details_complete? and attachment? methods in my model

Now I am trying to implement the same state machine using dynamic state definitions

  # Replace this with an external source (like a db)
  def transitions
    [
      { :awaiting_quote => :awaiting_quote_acceptance, :on => :submit_quote },
      { :awaiting_quote_acceptance => :awaiting_final_invoice, :on => :accept_quote },
      { :awaiting_quote_acceptance => :awaiting_quote, :on => :decline_quote },
      { :awaiting_final_invoice => :awaiting_order_acceptance, :on => :submit_final_invoice },
      { :awaiting_order_acceptance => :order_placed, :on => :accept_order }
      # ...
    ]
  end

  # Create a state machine for this quotation instance dynamically based on the
  # transitions defined from the source above
  def machine
    quotation = self
    @machine ||= Machine.new(quotation, :initial => :awaiting_quote, :action => :save_state) do
      quotation.transitions.each { |attrs| transition(attrs) }
    end
  end

  # Save the state change
  def save_state
    self.save!
  end

Now how do I use the same conditional state changes in this approach? Where should I add the if: option? Thank you

Can't access save from after_transition with datamapper

I am using datamapper as my ORM and have an after_transition method doing a save but getting this error:

NameError: undefined local variable or method `save_customer' for #<StateMachines::Machine:0x007f889dc07da8>

Any help would be great.

Store state in a file?

I want to use this in a CLI workflow based App. I don't have any need for a database so I was hoping I can store the state in a file. Does this gem allow me to store the state in a file?

And how would I do so?

gems/state_machines-0.5.0/lib/state_machines/integrations/base.rb:27:in `<=': compared with non class/module (TypeError)

This occurs when trying to run specs in my rails engine gem where I use state_machines-activerecord, which uses state_machines. No ida why it happens?

Inspecting the output of the method like so:

        # Whether the integration should be used for the given class.
        def matches?(klass)
          matching_ancestors.any? { |ancestor|
            puts "#{klass.inspect} >< #{ancestor.inspect}"
            klass <= ancestor
          }
        end

I get:

DataCollecting::CollectorRun (call 'DataCollecting::CollectorRun.connection' to establish a connection) >< "ActiveRecord::Base"

DataCollecting is one of the classes I use for state_machines

Pluginaweek attribution

Thanks for bringing back life to this gem. state_machine was a major reason why we delayed upgrading to Rails 4.2 and with state_machines we have upgraded without any issues :)

The license and gemspec does include reference to the author of state_machine but I think it's fair that the original project https://github.com/pluginaweek/state_machine is attributed in the readme. Right now there is no way of telling that this project was originally forked from state_machine.

I can make a pull request but just wanted to open an issue for any input about this and also about the wording if the pull request is welcome. I think this pull request should also include some info about state_machines not only being a project that works with Rails 4 but also that it's a drop-in upgrade from state_machine. That's useful info for users who have found articles about state_machine but is unable to get it to work.

Again: thanks for your work. I'm sure there are many businesses depending on this gem, if the project is taking up a lot of your time I can suggest putting up some info on how businesses can help contribute to the development.

with_state approach

Hi, I'm trying to migrate my app to use this gem now, I was using with_state of the last gem, did you guys have removed this?

ex:

Coupon.with_state(:active)
Coupon.with_states(:active, :completed)

question regarding state machine collections

I'm working on updating the state_machine_rspec gem to use state_machines and I've come across a critical difference in behaviour. In the old pluginaweek/state_machine the class method #state_machine returned a collection of state_machines, and the collection was inherited from Hash. In the state_machine_rspec gem there's a line which relies on [] to get a named machine from the collection. I can't find the equivalent method in the new state-machines/state_machines.

The difference is that in the new style the class method #state_machine returns a single state_machine and there doesn't seem to be a way to get one of possibly many named state machines.

This is blocking me from updating the state_machine_rspec gem (updated to state_machines_rspec gem) for the new state-machines/state_machine.

Any idea how I can solve this issue?

Question: after_transition do: []

Can I assume that most of the DSL will allow arrays i.e.

after_transition on: :paid, do: [:process_payment, :notify_paid]

I don't see anything like that in the docs but it appears to be acceptable in the code.

Also, the readme references documentation, other than code docs is there something else I can read to get a full understanding? Is there a better place to ask questions?

And...thanks for taking this over.

Passing event arguments to guards

TLDR;

Currently, arguments passed to an event are not passed to the guard, only the model object. I'm wondering if there is a reasoning behind this (i.e. is it poor practice), or if this is something that I could PR?

Rationale

For our subscription model, currently, we have two events start_trial and start_active. We just found out we need start_future_active and the nomenclature was a dead giveaway that we instead need one start event with not necessarily deterministic paths through the state machine.

I say not necessarily because there is one deterministic case where we want to start but we want to force it to skip the trial (trial is the default start behavior). In looking at this, I've found that a guard cannot inspect the arguments passed to an event, and that seems like exactly what we need. For example:

subscription.start(true) # skip trial plain arg

Details

Relevant existing code

event.rb

    def fire(object, *args)
      machine.reset(object)

      if transition = transition_for(object) # guards are checked here
        transition.perform(*args)            # args are passed here after guard check for execution
      else
        on_failure(object)
        false
      end
    end

Existing implementation that needs refactored

event :start_trial do
  transition :uninitialized => :trial, if: :trial_enabled?
end

event :start_active do
  transition [:uninitialized, :trial, :trial_expired, :system_grace, :grace] => :active
end

# now we need start_future - this needs to be refactored

With PR proposal 1

This would appear to be simple to implement based on the code involved:

class Subscription
  state_machine :state, initial: :uninitialized do

    # plain arg example (proposal #1)
    event :start do
      transition :uninitialized => :trial, if: ->(subscription, args) { subscription.trial_enabled? && args.length > 0 && args[0] != true }
      transition [:uninitialized, :trial, :trial_expired, :system_grace, :grace] => :active
      transition :uninitialized => :future_active, if: future_dated?
    end
  end
end

With PR proposal 2

This would require more significant changes, but still not too much. The transition would be initialized first then passed to the guard:

class Subscription
  state_machine :state, initial: :uninitialized do

    # transition example (proposal #2)
    event :start do
      transition :uninitialized => :trial, if: ->(subscription, transition) { subscription.trial_enabled? && transition.args[0] != true }
      transition [:uninitialized, :trial, :trial_expired, :system_grace, :grace] => :active
      transition :uninitialized => :future_active, if: future_dated?
    end
  end
end

Conclusion

@seuros - do you think this is a good addition? Is there an argument against allowing this?

/cc @bmcdaniel11

get value for state name?

My setup:

class Vehicle
  state_machine :state do
    state :parked, value: 0
  end
end

My goal is to turn :parked back into 0, for example:

Vehicle.state_value(:parked) # => 0

Can't find a method for it in the docs or some minor code inspection.... any ideas? Thanks!

Single boolean argument to event method is not passed as transition.args to after_transition method

state_machine :state, initial: :uninitialized do
     event :cancel do
        transition [:trial, :active, :grace] => :canceled
      end

      after_transition on: :cancel, do: :process_cancel
end

def process_cancel(transition)
    terminate = transition.args[0] || false
end
  1. subscription.cancel!(true) yields transition.args empty
  2. subscription.cancel!(true, 'foo') yields transition.args length 2 # [true, 'foo']
  3. subscription.cancel!('foo') yields transition.args length 1 # ['foo']

Bundle install on El Capitan

When I'm trying to install this gem with OSX El Capitan I get this error:

Gem::Installer::ExtensionBuildError: ERROR: Failed to build gem native extension.

    /Users/jankeesvw/.rbenv/versions/2.0.0-p645/bin/ruby extconf.rb
checking for rb_trap_immediate in ruby.h,rubysig.h... no
checking for rb_thread_blocking_region()... yes
checking for inotify_init() in sys/inotify.h... no
checking for __NR_inotify_init in sys/syscall.h... no
checking for writev() in sys/uio.h... yes
checking for rb_wait_for_single_fd()... yes
checking for rb_enable_interrupt()... no
checking for rb_time_new()... yes
checking for sys/event.h... yes
checking for sys/queue.h... yes
creating Makefile

make "DESTDIR="
compiling binder.cpp
In file included from binder.cpp:20:
./project.h:107:10: fatal error: 'openssl/ssl.h' file not found
#include <openssl/ssl.h>
         ^
1 error generated.
make: *** [binder.o] Error 1


Gem files will remain installed in /Users/jankeesvw/.rbenv/versions/2.0.0-p645/lib/ruby/gems/2.0.0/gems/eventmachine-1.0.3 for inspection.
Results logged to /Users/jankeesvw/.rbenv/versions/2.0.0-p645/lib/ruby/gems/2.0.0/gems/eventmachine-1.0.3/ext/gem_make.out
An error occurred while installing eventmachine (1.0.3), and Bundler cannot continue.
Make sure that `gem install eventmachine -v '1.0.3'` succeeds before bundling.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.