From f52f45652dac65bdede9b9ba4a7c8c3639cee9e6 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 1 Mar 2014 21:57:59 +0000 Subject: [PATCH 01/47] Fix docs. --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ca82345..007909c 100644 --- a/README.md +++ b/README.md @@ -462,9 +462,9 @@ You can further narrow down the listener to only watch state exit changes using All callbacks get the `TransitionEvent` object with the following attributes. -* name # the event name -* from # the state transitioning from -* to # the state transitioning to +* `name # the event name` +* `from # the state transitioning from` +* `to # the state transitioning to` followed by the rest of arguments that were passed to the event method. @@ -590,9 +590,9 @@ For more complex example see [Integration](#6-integration) section. By default, the **FiniteMachine** will throw an exception whenever the machine is in invalid state or fails to transition. -* FiniteMachine::TransitionError -* FiniteMachine::InvalidStateError -* FiniteMachine::InvalidCallbackError +* `FiniteMachine::TransitionError` +* `FiniteMachine::InvalidStateError` +* `FiniteMachine::InvalidCallbackError` You can attach specific error handler inside the `handlers` scope by passing the name of the error and actual callback to be executed when the error happens inside the `handle` method. The `handle` receives a list of exception class or exception class names, and an option `:with` with a name of the method or a Proc object to be called to handle the error. As an alternative, you can pass a block. @@ -664,7 +664,7 @@ class Car @gears ||= FiniteMachine.define do initial :neutral - target: context + target context events { event :start, :neutral => :one From 7f2d510faf40ec437fbedfed4c90e90a921e4fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20F=C3=B6hring?= Date: Mon, 3 Mar 2014 14:42:35 +0100 Subject: [PATCH 02/47] Add docs badge to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 007909c..e7b8ac4 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,12 @@ [![Gem Version](https://badge.fury.io/rb/finite_machine.png)][gem] [![Build Status](https://secure.travis-ci.org/peter-murach/finite_machine.png?branch=master)][travis] [![Code Climate](https://codeclimate.com/github/peter-murach/finite_machine.png)][codeclimate] +[![Inline docs](http://inch-pages.github.io/github/peter-murach/finite_machine.png)][inchpages] [gem]: http://badge.fury.io/rb/finite_machine [travis]: http://travis-ci.org/peter-murach/finite_machine [codeclimate]: https://codeclimate.com/github/peter-murach/finite_machine +[inchpages]: http://inch-pages.github.io/github/peter-murach/finite_machine A minimal finite state machine with a straightforward and intuitive syntax. You can quickly model states and add callbacks that can be triggered synchronously or asynchronously. From f60c65f9e0a19f64f28c4ca996d7b438d1821cd2 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Mon, 3 Mar 2014 23:18:08 +0000 Subject: [PATCH 03/47] Tiny docs fix. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 007909c..6864559 100644 --- a/README.md +++ b/README.md @@ -705,7 +705,7 @@ class Account < ActiveRecord::Base def manage context = self - @machine ||= FiniteMachine.define do + @manage ||= FiniteMachine.define do target context initial context.state From 09c1f4c900cbe186722fcd3fe5e1376eb5d41e88 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sun, 9 Mar 2014 22:39:59 +0000 Subject: [PATCH 04/47] Change to clearly hook in errors. --- lib/finite_machine/catchable.rb | 9 ++++++++- lib/finite_machine/dsl.rb | 8 +++----- lib/finite_machine/state_machine.rb | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/finite_machine/catchable.rb b/lib/finite_machine/catchable.rb index 169b0e6..81b735e 100644 --- a/lib/finite_machine/catchable.rb +++ b/lib/finite_machine/catchable.rb @@ -6,11 +6,18 @@ module FiniteMachine module Catchable def self.included(base) - base.class_eval do + base.module_eval do attr_threadsafe :error_handlers end end + # Initialize errors storage + # + # @api public + def init_catchable + self.error_handlers = [] + end + # Rescue exception raised in state machine # # @param [Array[Exception]] exceptions diff --git a/lib/finite_machine/dsl.rb b/lib/finite_machine/dsl.rb index 3224d84..5bb6f48 100644 --- a/lib/finite_machine/dsl.rb +++ b/lib/finite_machine/dsl.rb @@ -131,15 +131,13 @@ def event(name, attrs = {}, &block) class ErrorsDSL < GenericDSL - def initialize(machine) - super(machine) - machine.error_handlers = [] - end - # Add error handler # # @param [Array] exceptions # + # @example + # handle InvalidStateError, with: :log_errors + # # @api public def handle(*exceptions, &block) machine.handle(*exceptions, &block) diff --git a/lib/finite_machine/state_machine.rb b/lib/finite_machine/state_machine.rb index 709fbfa..326f71e 100644 --- a/lib/finite_machine/state_machine.rb +++ b/lib/finite_machine/state_machine.rb @@ -41,6 +41,7 @@ class StateMachine # # @api private def initialize(*args, &block) + init_catchable @subscribers = Subscribers.new(self) @events = EventsDSL.new(self) @errors = ErrorsDSL.new(self) From 05cd4143d281bcb9ed1ed910e3b81c269bbb7923 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 14:13:37 +0000 Subject: [PATCH 05/47] Document target. --- lib/finite_machine/dsl.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/finite_machine/dsl.rb b/lib/finite_machine/dsl.rb index 5bb6f48..7bcc155 100644 --- a/lib/finite_machine/dsl.rb +++ b/lib/finite_machine/dsl.rb @@ -58,8 +58,14 @@ def initial(value) machine.events.call(&event) end - def target(value) - machine.env.target = value + # Attach state machine to an object. This allows state machine + # to initiate events in the context of a particular object. + # + # @param [Object] object + # + # @api public + def target(object) + machine.env.target = object end # Define terminal state From 91cc1937879d81e117fdba0c14600020e35a6714 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 14:54:23 +0000 Subject: [PATCH 06/47] Add ruby files. --- .ruby-gemset | 1 + .ruby-version | 1 + 2 files changed, 2 insertions(+) create mode 100644 .ruby-gemset create mode 100644 .ruby-version diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 0000000..0507c07 --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +finite_machine diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..227cea2 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.0.0 From 65f476fc1eb1b0a06b3584df8dd35b4bf1e03f7a Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 14:54:52 +0000 Subject: [PATCH 07/47] Change dev dependencies. --- Gemfile | 12 ++++++++++++ finite_machine.gemspec | 4 +--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 8ceeb2c..ea44828 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,15 @@ source 'https://rubygems.org' # Specify your gem's dependencies in finite_machine.gemspec gemspec + +group :development do + gem 'rake', '~> 10.1.0' + gem 'rspec', '~> 2.14.1' + gem 'yard', '~> 0.8.7' +end + +group :metrics do + gem 'coveralls', '~> 0.7.0' + gem 'simplecov', '~> 0.8.2' + gem 'yardstick', '~> 0.9.9' +end diff --git a/finite_machine.gemspec b/finite_machine.gemspec index e2c641c..aa869f4 100644 --- a/finite_machine.gemspec +++ b/finite_machine.gemspec @@ -18,7 +18,5 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] - spec.add_development_dependency "bundler", "~> 1.3" - spec.add_development_dependency "rake" - spec.add_development_dependency "rspec" + spec.add_development_dependency "bundler", "~> 1.5" end From 0b5dc7e4a55b8beb305eb295edde5a793809d110 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 14:58:30 +0000 Subject: [PATCH 08/47] Add coverage stats. --- spec/spec_helper.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3c72cf6..c417c38 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,20 @@ # encoding: utf-8 +if RUBY_VERSION > '1.9' and (ENV['COVERAGE'] || ENV['TRAVIS']) + require 'simplecov' + require 'coveralls' + + SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ + SimpleCov::Formatter::HTMLFormatter, + Coveralls::SimpleCov::Formatter + ] + + SimpleCov.start do + command_name 'spec' + add_filter 'spec' + end +end + require 'finite_machine' RSpec.configure do |config| From 019ac22cf6f0b5573c0fd5831de5b6f6d67f31a5 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 14:58:47 +0000 Subject: [PATCH 09/47] Add coverage task. --- Rakefile | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Rakefile b/Rakefile index b1fa6ea..7a9aabb 100644 --- a/Rakefile +++ b/Rakefile @@ -31,6 +31,16 @@ end desc 'Run all specs' task ci: %w[ spec ] +desc 'Measure code coverage' +task :coverage do + begin + original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true' + Rake::Task['spec'].invoke + ensure + ENV['COVERAGE'] = original + end +end + desc 'Load gem inside irb console' task :console do require 'irb' From 5ccd9499ad59af40b4b89ff1b40f1b55ecb27d5c Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 15:02:42 +0000 Subject: [PATCH 10/47] Add coverage badge. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e44fca8..ed90652 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ [![Gem Version](https://badge.fury.io/rb/finite_machine.png)][gem] [![Build Status](https://secure.travis-ci.org/peter-murach/finite_machine.png?branch=master)][travis] [![Code Climate](https://codeclimate.com/github/peter-murach/finite_machine.png)][codeclimate] +[![Coverage Status](https://coveralls.io/repos/peter-murach/finite_machine/badge.png)][coverage] [![Inline docs](http://inch-pages.github.io/github/peter-murach/finite_machine.png)][inchpages] [gem]: http://badge.fury.io/rb/finite_machine [travis]: http://travis-ci.org/peter-murach/finite_machine [codeclimate]: https://codeclimate.com/github/peter-murach/finite_machine +[coverage]: https://coveralls.io/r/peter-murach/finite_machine [inchpages]: http://inch-pages.github.io/github/peter-murach/finite_machine A minimal finite state machine with a straightforward and intuitive syntax. You can quickly model states and add callbacks that can be triggered synchronously or asynchronously. From 281c11a21478faa61c3d44237c1ac460b0a918da Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 17:40:01 +0000 Subject: [PATCH 11/47] Fix docs. --- lib/finite_machine/dsl.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/finite_machine/dsl.rb b/lib/finite_machine/dsl.rb index 7bcc155..9a166bd 100644 --- a/lib/finite_machine/dsl.rb +++ b/lib/finite_machine/dsl.rb @@ -48,7 +48,7 @@ def initialize(machine) # Define initial state # - # @params [String, Hash] value + # @param [String, Hash] value # # @api public def initial(value) @@ -100,7 +100,7 @@ def handlers(&block) # Parse initial options # - # @params [String, Hash] value + # @param [String, Hash] value # # @return [Array[Symbol,String]] # @@ -144,6 +144,8 @@ class ErrorsDSL < GenericDSL # @example # handle InvalidStateError, with: :log_errors # + # @return [Array[Exception]] + # # @api public def handle(*exceptions, &block) machine.handle(*exceptions, &block) From 24057339095b540e491421d1f7acb245c5ec4a63 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 17:40:38 +0000 Subject: [PATCH 12/47] Check lookup calls. --- spec/unit/target_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/spec/unit/target_spec.rb b/spec/unit/target_spec.rb index a058a96..453da60 100644 --- a/spec/unit/target_spec.rb +++ b/spec/unit/target_spec.rb @@ -53,6 +53,23 @@ def engine expect(car.reverse_lights).to be_false end + it "propagates method call" do + fsm = FiniteMachine.define do + initial :green + events { + event :slow, :green => :yellow + } + + callbacks { + on_enter_yellow do |event| + uknown_method + end + } + end + expect(fsm.current).to eql(:green) + expect { fsm.slow }.to raise_error(StandardError) + end + it "references machine methods inside callback" do called = [] fsm = FiniteMachine.define do From c25b01a39e36aacf4aa092d7ed74e859b48c4c4d Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 17:56:24 +0000 Subject: [PATCH 13/47] Test handlers error conditions. --- spec/unit/handlers_spec.rb | 53 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/spec/unit/handlers_spec.rb b/spec/unit/handlers_spec.rb index e0020f2..521ef41 100644 --- a/spec/unit/handlers_spec.rb +++ b/spec/unit/handlers_spec.rb @@ -96,4 +96,57 @@ def raise_error expect(fsm.current).to eql(:green) expect(called).to eql(['invalid_state_error']) end + + it 'allows for empty block handler' do + called = [] + fsm = FiniteMachine.define do + initial :green + + events { + event :slow, :green => :yellow + event :stop, :yellow => :red + } + + handlers { + handle FiniteMachine::InvalidStateError do + called << 'invalidstate' + end + } + end + + expect(fsm.current).to eql(:green) + fsm.stop + expect(fsm.current).to eql(:green) + expect(called).to eql([ + 'invalidstate' + ]) + end + + it 'requires error handler' do + expect { FiniteMachine.define do + initial :green + + events { + event :slow, :green => :yellow + } + + handlers { + handle 'UnknownErrorType' + } + end }.to raise_error(ArgumentError, /error handler/) + end + + it 'checks handler class to be Exception' do + expect { FiniteMachine.define do + initial :green + + events { + event :slow, :green => :yellow + } + + handlers { + handle Object do end + } + end }.to raise_error(ArgumentError, /Object isn't an Exception/) + end end From f3abb5d6338cb85ba55d44800d3e0e1f9a596c40 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 18:39:03 +0000 Subject: [PATCH 14/47] Change spec settings. --- .rspec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.rspec b/.rspec index 5f16476..2333464 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,4 @@ --color --format progress +--order random +--warnings From eabed17ea98af5c36d1e057f456d5728262b0606 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 18:58:47 +0000 Subject: [PATCH 15/47] Ensure proper event definition. --- lib/finite_machine/transition.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/finite_machine/transition.rb b/lib/finite_machine/transition.rb index 5d8f258..a97be71 100644 --- a/lib/finite_machine/transition.rb +++ b/lib/finite_machine/transition.rb @@ -1,7 +1,6 @@ # encoding: utf-8 module FiniteMachine - # Class describing a transition associated with a given event class Transition include Threadable @@ -72,7 +71,12 @@ def define # @api private def define_event _transition = self - machine.class.__send__(:define_method, name) do |*args, &block| + _name = name + + machine.singleton_class.class_eval do + undef_method(_name) if method_defined?(_name) + end + machine.send(:define_singleton_method, name) do |*args, &block| transition(_transition, *args, &block) end end @@ -107,6 +111,5 @@ def inspect def raise_not_enough_transitions(attrs) raise NotEnoughTransitionsError, "please provide state transitions for '#{attrs.inspect}'" end - end # Transition end # FiniteMachine From 264be8e80f886a0bc78d7339b437f90dfa696c35 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 19:00:54 +0000 Subject: [PATCH 16/47] Ensure tighter tests. --- spec/unit/callable/call_spec.rb | 4 ++++ spec/unit/callbacks_spec.rb | 2 ++ spec/unit/events_spec.rb | 5 +++++ spec/unit/if_unless_spec.rb | 2 +- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/spec/unit/callable/call_spec.rb b/spec/unit/callable/call_spec.rb index ccd5989..b7bb565 100644 --- a/spec/unit/callable/call_spec.rb +++ b/spec/unit/callable/call_spec.rb @@ -8,6 +8,10 @@ Car = Class.new do attr_reader :result + def initialize + @engine_on = false + end + def turn_engine_on @result = 'turn_engine_on' @engine_on = true diff --git a/spec/unit/callbacks_spec.rb b/spec/unit/callbacks_spec.rb index 7de2d85..1d60e12 100644 --- a/spec/unit/callbacks_spec.rb +++ b/spec/unit/callbacks_spec.rb @@ -47,6 +47,8 @@ } end + expect(fsm.current).to eql(:green) + expect(called).to eql([ 'on_exit_none', 'on_exit', diff --git a/spec/unit/events_spec.rb b/spec/unit/events_spec.rb index cd2a85d..20a46bd 100644 --- a/spec/unit/events_spec.rb +++ b/spec/unit/events_spec.rb @@ -216,6 +216,7 @@ event :stop, :green => :yellow event :stop, :yellow => :red event :stop, :red => :pink + event :cycle, [:yellow, :red, :pink] => :green } end @@ -229,6 +230,10 @@ expect(fsm.current).to eql(:red) fsm.stop expect(fsm.current).to eql(:pink) + fsm.cycle + expect(fsm.current).to eql(:green) + fsm.stop + expect(fsm.current).to eql(:yellow) end it "returns values for events" do diff --git a/spec/unit/if_unless_spec.rb b/spec/unit/if_unless_spec.rb index 4988d8b..4149b6d 100644 --- a/spec/unit/if_unless_spec.rb +++ b/spec/unit/if_unless_spec.rb @@ -125,7 +125,7 @@ def engine_on? target car events { - event :start, :neutral => :one, if: proc {|car| car.engine_on? } + event :start, :neutral => :one, if: proc {|_car| _car.engine_on? } event :shift, :one => :two } end From 59d123f49bd31ad7cfb10605822e3a30349a37b2 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 19:58:07 +0000 Subject: [PATCH 17/47] Change to allow for default value, add docs. --- lib/finite_machine/threadable.rb | 39 +++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/finite_machine/threadable.rb b/lib/finite_machine/threadable.rb index 9d145ca..c1d219e 100644 --- a/lib/finite_machine/threadable.rb +++ b/lib/finite_machine/threadable.rb @@ -7,15 +7,30 @@ module Threadable module InstanceMethods @@sync = Sync.new + # Exclusive lock + # + # @return [nil] + # + # @api public def sync_exclusive(&block) @@sync.synchronize(:EX, &block) end + # Shared lock + # + # @return [nil] + # + # @api public def sync_shared(&block) @@sync.synchronize(:SH, &block) end end + # Module hook + # + # @return [nil] + # + # @api private def self.included(base) base.extend ClassMethods base.module_eval do @@ -23,17 +38,35 @@ def self.included(base) end end + private_class_method :included + module ClassMethods include InstanceMethods + # Defines threadsafe attributes for a class + # + # @example + # attr_threadable :errors, :events + # + # @example + # attr_threadable :errors, default: [] + # + # @return [nil] + # + # @api public def attr_threadsafe(*attrs) + opts = attrs.last.is_a?(::Hash) ? attrs.pop : {} + default = opts.fetch(:default, nil) attrs.flatten.each do |attr| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{attr}(*args) - if args.empty? + value = args.shift + if value + self.#{attr} = value + elsif instance_variables.include?(:@#{attr}) sync_shared { @#{attr} } - else - self.#{attr} = args.shift + elsif #{!default.nil?} + sync_shared { instance_variable_set(:@#{attr}, #{default}) } end end alias_method '#{attr}?', '#{attr}' From ab69f4ae77ea600a042175529afd13a9c0948269 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 19:58:38 +0000 Subject: [PATCH 18/47] Remove init catchable. --- lib/finite_machine/catchable.rb | 9 +-------- lib/finite_machine/state_machine.rb | 1 - 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/finite_machine/catchable.rb b/lib/finite_machine/catchable.rb index 81b735e..31e3db4 100644 --- a/lib/finite_machine/catchable.rb +++ b/lib/finite_machine/catchable.rb @@ -7,17 +7,10 @@ module Catchable def self.included(base) base.module_eval do - attr_threadsafe :error_handlers + attr_threadsafe :error_handlers, default: [] end end - # Initialize errors storage - # - # @api public - def init_catchable - self.error_handlers = [] - end - # Rescue exception raised in state machine # # @param [Array[Exception]] exceptions diff --git a/lib/finite_machine/state_machine.rb b/lib/finite_machine/state_machine.rb index 326f71e..709fbfa 100644 --- a/lib/finite_machine/state_machine.rb +++ b/lib/finite_machine/state_machine.rb @@ -41,7 +41,6 @@ class StateMachine # # @api private def initialize(*args, &block) - init_catchable @subscribers = Subscribers.new(self) @events = EventsDSL.new(self) @errors = ErrorsDSL.new(self) From 8efe1557493362119a25284892151ae3ce3fd16e Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 21:15:11 +0000 Subject: [PATCH 19/47] Delegate to observer. --- lib/finite_machine/state_machine.rb | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/finite_machine/state_machine.rb b/lib/finite_machine/state_machine.rb index 709fbfa..6a6fedd 100644 --- a/lib/finite_machine/state_machine.rb +++ b/lib/finite_machine/state_machine.rb @@ -96,6 +96,9 @@ def is?(state) # Retrieve all states # + # @example + # fsm.states # => [:yellow, :green, :red] + # # @return [Array[Symbol]] # # @api public @@ -191,14 +194,34 @@ def transition(_transition, *args, &block) SUCCEEDED end + # Forward the message to target, observer or self + # + # @param [String] method_name + # + # @param [Array] args + # + # @return [self] + # + # @api private def method_missing(method_name, *args, &block) if env.target.respond_to?(method_name.to_sym) - env.target.send(method_name.to_sym, *args, &block) + env.target.public_send(method_name.to_sym, *args, &block) + elsif observer.respond_to?(method_name.to_sym) + observer.public_send(method_name.to_sym, *args, &block) else super end end + # Test if a message can be handled by state machine + # + # @param [String] method_name + # + # @param [Boolean] include_private + # + # @return [Boolean] + # + # @api private def respond_to_missing?(method_name, include_private = false) env.target.respond_to?(method_name.to_sym) end From 847ad1943c3a2aa14bb0f2357d5ffefacbc0b87f Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 21:17:33 +0000 Subject: [PATCH 20/47] Change to fix observer method lookup. --- lib/finite_machine/observer.rb | 45 +++++++++++++++------- spec/unit/callbacks_spec.rb | 68 +++++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 14 deletions(-) diff --git a/lib/finite_machine/observer.rb b/lib/finite_machine/observer.rb index b2c061d..edf8b07 100644 --- a/lib/finite_machine/observer.rb +++ b/lib/finite_machine/observer.rb @@ -67,19 +67,6 @@ def on_exit(*args, &callback) listen_on :exit, *args, &callback end - def method_missing(method_name, *args, &block) - _, event_name, callback_name = *method_name.to_s.match(/^(on_\w+?)_(\w+)$/) - if callback_names.include?(callback_name.to_sym) - send(event_name, callback_name.to_sym, *args, &block) - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - _, callback_name = *method_name.to_s.match(/^(on_\w+?)_(\w+)$/) - callback_names.include?(callback_name.to_sym) - end TransitionEvent = Struct.new(:from, :to, :name) do def build(_transition) @@ -131,5 +118,37 @@ def ensure_valid_callback_name!(name) end end + # Forward the message to observer + # + # @param [String] method_name + # + # @param [Array] args + # + # @return [self] + # + # @api private + def method_missing(method_name, *args, &block) + _, event_name, callback_name = *method_name.to_s.match(/^(on_\w+?)_(\w+)$/) + if callback_names.include?(callback_name.to_sym) + public_send(event_name, callback_name.to_sym, *args, &block) + else + super + end + end + + # Test if a message can be handled by observer + # + # @param [String] method_name + # + # @param [Boolean] include_private + # + # @return [Boolean] + # + # @api private + def respond_to_missing?(method_name, include_private = false) + _, event_name, callback_name = *method_name.to_s.match(/^(on_\w+?)_(\w+)$/) + callback_names.include?(callback_name.to_sym) + end + end # Observer end # FiniteMachine diff --git a/spec/unit/callbacks_spec.rb b/spec/unit/callbacks_spec.rb index 1d60e12..f9dbf08 100644 --- a/spec/unit/callbacks_spec.rb +++ b/spec/unit/callbacks_spec.rb @@ -473,5 +473,71 @@ expect { fsm.slow }.to raise_error(RuntimeError) end - xit "executes callbacks with multiple 'from' transitions" + it "executes callbacks with multiple 'from' transitions" do + called = [] + fsm = FiniteMachine.define do + initial :green + + events { + event :stop, :green => :yellow + event :stop, :yellow => :red + } + + callbacks { + on_enter_stop do |event| + called << 'on_enter_stop' + end + } + end + expect(fsm.current).to eql(:green) + fsm.stop + expect(fsm.current).to eql(:yellow) + fsm.stop + expect(fsm.current).to eql(:red) + expect(called).to eql([ + 'on_enter_stop', + 'on_enter_stop' + ]) + end + + it "allows to define callbacks on machine instance" do + called = [] + fsm = FiniteMachine.define do + initial :green + + events { + event :slow, :green => :yellow + event :stop, :yellow => :red + event :ready, :red => :yellow + event :go, :yellow => :green + } + end + + fsm.on_enter_yellow do |event| + called << 'on_enter_yellow' + end + + expect(fsm.current).to eql(:green) + fsm.slow + expect(called).to eql([ + 'on_enter_yellow' + ]) + end + + it "raises error for unknown callback" do + expect { FiniteMachine.define do + initial :green + + events { + event :slow, :green => :yellow + event :stop, :yellow => :red + event :ready, :red => :yellow + event :go, :yellow => :green + } + + callbacks { + on_enter_unknown do |event| end + } + end }.to raise_error(NoMethodError) + end end From a7e9135f35ca25d84ef11ab1be732bac1bd57009 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 21:32:58 +0000 Subject: [PATCH 21/47] Document callbacks extension. --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index ed90652..72b965d 100644 --- a/README.md +++ b/README.md @@ -590,6 +590,26 @@ fm.back # => Go Piotr! For more complex example see [Integration](#6-integration) section. +### 4.8 Definig callbacks + +When defining callbacks you are not limited to the `callbacks` helper. After **FiniteMachine** instance is created you can register callbacks the same way as before by calling `on` and supplying the type of notification and state/event you are interested in. + +```ruby +fm = FiniteMachine.define do + initial :red + + events { + event :ready, :red => :yellow + event :go, :yellow => :green + event :stop, :green => :red + } +end + +fm.on_enter_yellow do |event| + ... +end +``` + ## 5 Errors By default, the **FiniteMachine** will throw an exception whenever the machine is in invalid state or fails to transition. From 2a2522d63f8e1a3e00a078110528cc5388b79b41 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 22:19:36 +0000 Subject: [PATCH 22/47] Change to public method. --- lib/finite_machine/event.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/finite_machine/event.rb b/lib/finite_machine/event.rb index 11e3fdd..df5964b 100644 --- a/lib/finite_machine/event.rb +++ b/lib/finite_machine/event.rb @@ -29,7 +29,7 @@ def initialize(state, transition, *data, &block) def notify(subscriber, *args, &block) if subscriber.respond_to? MESSAGE - subscriber.__send__(MESSAGE, self, *args, &block) + subscriber.public_send(MESSAGE, self, *args, &block) end end From d7325d000cc2a9c673e02e638f06ab7f12df15ca Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 22:20:05 +0000 Subject: [PATCH 23/47] Add unregister and docs. --- lib/finite_machine/hooks.rb | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/finite_machine/hooks.rb b/lib/finite_machine/hooks.rb index 1bfb0f2..fe999f7 100644 --- a/lib/finite_machine/hooks.rb +++ b/lib/finite_machine/hooks.rb @@ -10,6 +10,9 @@ class Hooks # Initialize a collection of hoooks # + # @example + # Hoosk.new(machine) + # # @api public def initialize(machine) @collection = Hash.new do |events_hash, event_type| @@ -25,13 +28,43 @@ def initialize(machine) # @param [String] name # @param [Proc] callback # + # @example + # hooks.register :enterstate, :green do ... end + # + # @return [Hash] + # # @api public def register(event_type, name, callback) @collection[event_type][name] << callback end + # Unregister callback + # + # @param [String] event_type + # @param [String] name + # @param [Proc] callback + # + # @example + # hooks.unregister :enterstate, :green do ... end + # + # @return [Hash] + # + # @api public + def unregister(event_type, name, callback) + @collection[event_type][name].shift + end + # Return all hooks matching event and state # + # @param [String] event_type + # @param [String] event_state + # @param [Event] event + # + # @example + # hooks.call(:entersate, :green, Event.new) + # + # @return [Hash] + # # @api public def call(event_type, event_state, event) @collection[event_type][event_state].each do |hook| From 060457604a9f456cd78eea749fd010f3c9f0ee51 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 15 Mar 2014 22:37:18 +0000 Subject: [PATCH 24/47] Add once callbacks. --- lib/finite_machine/observer.rb | 33 ++++++++++++++++++++++++----- spec/unit/callbacks_spec.rb | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/lib/finite_machine/observer.rb b/lib/finite_machine/observer.rb index edf8b07..3229338 100644 --- a/lib/finite_machine/observer.rb +++ b/lib/finite_machine/observer.rb @@ -28,7 +28,7 @@ def call(&block) instance_eval(&block) end - # Register callback for a given event. + # Register callback for a given event # # @param [Symbol] event_type # @param [Symbol] name @@ -42,6 +42,17 @@ def on(event_type = ANY_EVENT, name = ANY_STATE, &callback) end end + # Unregister callback for a given event + # + # @api public + def off(event_type = ANY_EVENT, name = ANY_STATE, &callback) + sync_exclusive do + hooks.unregister event_type, name, callback + end + end + + module Once; end + def listen_on(type, *args, &callback) name = args.first events = [] @@ -67,6 +78,17 @@ def on_exit(*args, &callback) listen_on :exit, *args, &callback end + def once_on_enter(*args, &callback) + listen_on :enter, *args, &callback.extend(Once) + end + + def once_on_transition(*args, &callback) + listen_on :transition, *args, &callback.extend(Once) + end + + def once_on_exit(*args, &callback) + listen_on :exit, *args, &callback.extend(Once) + end TransitionEvent = Struct.new(:from, :to, :name) do def build(_transition) @@ -93,6 +115,7 @@ def trigger(event, *args, &block) ANY_STATE_HOOK, ANY_EVENT_HOOK].each do |event_state| hooks.call(event_type, event_state, event) do |hook| run_callback(hook, event) + off(event_type, event_state, &hook) if hook.is_a?(Once) end end end @@ -128,9 +151,9 @@ def ensure_valid_callback_name!(name) # # @api private def method_missing(method_name, *args, &block) - _, event_name, callback_name = *method_name.to_s.match(/^(on_\w+?)_(\w+)$/) + _, event_name, callback_name = *method_name.to_s.match(/^(\w*?on_\w+?)_(\w+)$/) if callback_names.include?(callback_name.to_sym) - public_send(event_name, callback_name.to_sym, *args, &block) + public_send(event_name, :"#{callback_name}", *args, &block) else super end @@ -146,8 +169,8 @@ def method_missing(method_name, *args, &block) # # @api private def respond_to_missing?(method_name, include_private = false) - _, event_name, callback_name = *method_name.to_s.match(/^(on_\w+?)_(\w+)$/) - callback_names.include?(callback_name.to_sym) + *_, callback_name = *method_name.to_s.match(/^(\w*?on_\w+?)_(\w+)$/) + callback_names.include?(:"#{callback_name}") end end # Observer diff --git a/spec/unit/callbacks_spec.rb b/spec/unit/callbacks_spec.rb index f9dbf08..438a58c 100644 --- a/spec/unit/callbacks_spec.rb +++ b/spec/unit/callbacks_spec.rb @@ -540,4 +540,42 @@ } end }.to raise_error(NoMethodError) end + + it "triggers callbacks only once" do + called = [] + fsm = FiniteMachine.define do + initial :green + + events { + event :slow, :green => :yellow + event :go, :yellow => :green + } + + callbacks { + once_on_enter_green do |event| called << 'once_on_enter_green' end + once_on_enter_yellow do |event| called << 'once_on_enter_yellow' end + + once_on_transition_green do |event| called << 'once_on_transition_green' end + once_on_transition_yellow do |event| called << 'once_on_transition_yellow' end + + once_on_exit_green do |event| called << 'once_on_exit_green' end + once_on_exit_yellow do |event| called << 'once_on_exit_yellow' end + } + end + expect(fsm.current).to eql(:green) + fsm.slow + expect(fsm.current).to eql(:yellow) + fsm.go + expect(fsm.current).to eql(:green) + fsm.slow + expect(fsm.current).to eql(:yellow) + expect(called).to eql([ + 'once_on_transition_green', + 'once_on_enter_green', + 'once_on_exit_green', + 'once_on_transition_yellow', + 'once_on_enter_yellow', + 'once_on_exit_yellow' + ]) + end end From 412e9f178cc12c2354a65076c3e4513602078ad6 Mon Sep 17 00:00:00 2001 From: Laust Rud Jacobsen Date: Mon, 17 Mar 2014 09:49:46 +0100 Subject: [PATCH 25/47] Typo fix in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 72b965d..d219162 100644 --- a/README.md +++ b/README.md @@ -590,7 +590,7 @@ fm.back # => Go Piotr! For more complex example see [Integration](#6-integration) section. -### 4.8 Definig callbacks +### 4.8 Defining callbacks When defining callbacks you are not limited to the `callbacks` helper. After **FiniteMachine** instance is created you can register callbacks the same way as before by calling `on` and supplying the type of notification and state/event you are interested in. From 902caed514e0b9c545b16329efcfbac436e5514e Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 14:46:49 +0000 Subject: [PATCH 26/47] Ensure threadsafe event notifications. --- lib/finite_machine/subscribers.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/finite_machine/subscribers.rb b/lib/finite_machine/subscribers.rb index 8957f51..d306fdb 100644 --- a/lib/finite_machine/subscribers.rb +++ b/lib/finite_machine/subscribers.rb @@ -1,15 +1,18 @@ # encoding: utf-8 +require 'monitor' + module FiniteMachine # A class responsibile for storage of event subscribers class Subscribers include Enumerable + include MonitorMixin def initialize(machine) + super() @machine = machine @subscribers = [] - @mutex = Mutex.new end def each(&block) @@ -29,7 +32,7 @@ def subscribe(*observers) end def visit(event) - each { |subscriber| event.notify subscriber } + each { |subscriber| synchronize { event.notify subscriber } } end def reset From 9ee97241c1c3d66612265f32e29d9d5cb410d004 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 14:47:28 +0000 Subject: [PATCH 27/47] Factor out tasks. --- Rakefile | 49 +++------------------------------------------ tasks/console.rake | 10 +++++++++ tasks/coverage.rake | 11 ++++++++++ tasks/spec.rake | 29 +++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 tasks/console.rake create mode 100644 tasks/coverage.rake create mode 100644 tasks/spec.rake diff --git a/Rakefile b/Rakefile index 7a9aabb..d2df74e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,51 +1,8 @@ -require "bundler/gem_tasks" - -begin - require 'rspec/core/rake_task' - - desc 'Run all specs' - RSpec::Core::RakeTask.new(:spec) do |task| - task.pattern = 'spec/{unit,integration}{,/*/**}/*_spec.rb' - end - - namespace :spec do - desc 'Run unit specs' - RSpec::Core::RakeTask.new(:unit) do |task| - task.pattern = 'spec/unit{,/*/**}/*_spec.rb' - end +# encoding: utf-8 - desc 'Run integration specs' - RSpec::Core::RakeTask.new(:integration) do |task| - task.pattern = 'spec/integration{,/*/**}/*_spec.rb' - end - end +require "bundler/gem_tasks" -rescue LoadError - %w[spec spec:unit spec:integration].each do |name| - task name do - $stderr.puts "In order to run #{name}, do `gem install rspec`" - end - end -end +FileList['tasks/**/*.rake'].each(&method(:import)) desc 'Run all specs' task ci: %w[ spec ] - -desc 'Measure code coverage' -task :coverage do - begin - original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true' - Rake::Task['spec'].invoke - ensure - ENV['COVERAGE'] = original - end -end - -desc 'Load gem inside irb console' -task :console do - require 'irb' - require 'irb/completion' - require File.join(__FILE__, '../lib/finite_machine') - ARGV.clear - IRB.start -end diff --git a/tasks/console.rake b/tasks/console.rake new file mode 100644 index 0000000..709e9c4 --- /dev/null +++ b/tasks/console.rake @@ -0,0 +1,10 @@ +# encoding: utf-8 + +desc 'Load gem inside irb console' +task :console do + require 'irb' + require 'irb/completion' + require File.join(__FILE__, '../lib/finite_machine') + ARGV.clear + IRB.start +end diff --git a/tasks/coverage.rake b/tasks/coverage.rake new file mode 100644 index 0000000..49b4ded --- /dev/null +++ b/tasks/coverage.rake @@ -0,0 +1,11 @@ +# encoding: utf-8 + +desc 'Measure code coverage' +task :coverage do + begin + original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true' + Rake::Task['spec'].invoke + ensure + ENV['COVERAGE'] = original + end +end diff --git a/tasks/spec.rake b/tasks/spec.rake new file mode 100644 index 0000000..f285041 --- /dev/null +++ b/tasks/spec.rake @@ -0,0 +1,29 @@ +# encoding: utf-8 + +begin + require 'rspec/core/rake_task' + + desc 'Run all specs' + RSpec::Core::RakeTask.new(:spec) do |task| + task.pattern = 'spec/{unit,integration}{,/*/**}/*_spec.rb' + end + + namespace :spec do + desc 'Run unit specs' + RSpec::Core::RakeTask.new(:unit) do |task| + task.pattern = 'spec/unit{,/*/**}/*_spec.rb' + end + + desc 'Run integration specs' + RSpec::Core::RakeTask.new(:integration) do |task| + task.pattern = 'spec/integration{,/*/**}/*_spec.rb' + end + end + +rescue LoadError + %w[spec spec:unit spec:integration].each do |name| + task name do + $stderr.puts "In order to run #{name}, do `gem install rspec`" + end + end +end From 55e009ffd24aa866a78d6f21ab4a28b5cd90beb8 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 14:47:53 +0000 Subject: [PATCH 28/47] Remove comment. --- Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile b/Gemfile index ea44828..063bee2 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,5 @@ source 'https://rubygems.org' -# Specify your gem's dependencies in finite_machine.gemspec gemspec group :development do From 85c6e0820140f71d7edb5a865e4e8d47e477d5e9 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 20:46:29 +0000 Subject: [PATCH 29/47] Fix callable to correctly forward arguments. --- lib/finite_machine/callable.rb | 10 +++++----- spec/unit/callable/call_spec.rb | 24 +++++++++++++++++++++--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/finite_machine/callable.rb b/lib/finite_machine/callable.rb index 3960cc8..61be124 100644 --- a/lib/finite_machine/callable.rb +++ b/lib/finite_machine/callable.rb @@ -1,7 +1,6 @@ # encoding: utf-8 module FiniteMachine - # A generic interface for executing strings, symbol methods or procs. class Callable @@ -21,7 +20,7 @@ def initialize(object) # # @api public def invert - lambda { |*args, &block| !self.call(*args, &block) } + lambda { |*args, &block| !call(*args, &block) } end # Execute action @@ -32,14 +31,15 @@ def invert def call(target, *args, &block) case object when Symbol - target.__send__(@object.to_sym) + target.public_send(object.to_sym, *args, &block) when String - value = eval "lambda { #{@object} }" + string = args.empty? ? "-> { #{object} }" : "-> { #{object}(*#{args}) }" + value = eval string target.instance_exec(&value) when ::Proc object.arity.zero? ? object.call : object.call(target, *args) else - raise ArgumentError, "Unknown callable #{@object}" + raise ArgumentError, "Unknown callable #{object}" end end end # Callable diff --git a/spec/unit/callable/call_spec.rb b/spec/unit/callable/call_spec.rb index b7bb565..50a204a 100644 --- a/spec/unit/callable/call_spec.rb +++ b/spec/unit/callable/call_spec.rb @@ -17,7 +17,7 @@ def turn_engine_on @engine_on = true end - def set_engine(value) + def set_engine(value = :on) @result = "set_engine(#{value})" @engine = value.to_sym == :on end @@ -58,12 +58,30 @@ def engine_on? end end + context 'when string with arguments' do + let(:object) { 'set_engine' } + + it 'executes method with arguments' do + instance.call(target, :off) + expect(target.result).to eql('set_engine(off)') + end + end + context 'when symbol' do - let(:object) { :engine_on? } + let(:object) { :set_engine } it 'executes method on target' do instance.call(target) - expect(target.result).to eql('engine_on') + expect(target.result).to eql('set_engine(on)') + end + end + + context 'when symbol with arguments' do + let(:object) { :set_engine } + + it 'executes method on target' do + instance.call(target, :off) + expect(target.result).to eql('set_engine(off)') end end From 691c4f62b45c7171eb5449e2e9f87a9c2c2e2c3d Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 20:50:18 +0000 Subject: [PATCH 30/47] Add async message proxy. --- lib/finite_machine/async_proxy.rb | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lib/finite_machine/async_proxy.rb diff --git a/lib/finite_machine/async_proxy.rb b/lib/finite_machine/async_proxy.rb new file mode 100644 index 0000000..3c9fe6f --- /dev/null +++ b/lib/finite_machine/async_proxy.rb @@ -0,0 +1,27 @@ +# encoding: utf-8 + +module FiniteMachine + # An asynchronous messages proxy + class AsyncProxy + + attr_reader :context + + # Initialize an AsynxProxy + # + # @param [Object] context + # the context this proxy is associated with + # + # @api private + def initialize(context) + @context = context + end + + # Delegate asynchronous event to event queue + # + # @api private + def method_missing(method_name, *args, &block) + @event_queue = FiniteMachine.event_queue + @event_queue << AsyncCall.build(@context, Callable.new(method_name), *args, &block) + end + end # AsyncProxy +end # FiniteMachine From 1adf9f7bba410ba1d7fb0699119dcc363327ad4a Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 20:50:59 +0000 Subject: [PATCH 31/47] Add async call for event dispatch. --- lib/finite_machine/async_call.rb | 47 ++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 lib/finite_machine/async_call.rb diff --git a/lib/finite_machine/async_call.rb b/lib/finite_machine/async_call.rb new file mode 100644 index 0000000..19e7718 --- /dev/null +++ b/lib/finite_machine/async_call.rb @@ -0,0 +1,47 @@ +# encoding: utf-8 + +module FiniteMachine + # An asynchronouse call representation + class AsyncCall + include Threadable + + attr_threadsafe :context + + attr_threadsafe :callable + + attr_threadsafe :arguments + + attr_threadsafe :block + + # Build asynchronous call instance + # + # @param [Object] context + # @param [Callable] callable + # @param [Array] args + # @param [#call] block + # + # @example + # AsyncCall.build(self, Callable.new(:method), :a, :b) + # + # @return [self] + # + # @api public + def self.build(context, callable, *args, &block) + instance = new + instance.context = context + instance.callable = callable + instance.arguments = *args + instance.block = block + instance + end + + # Dispatch the event to the context + # + # @return [nil] + # + # @api private + def dispatch + callable.call(context, *arguments, block) + end + end # AsyncCall +end # FiniteMachine From b5ca3b2595d86db7fbcc68515a37b55bb6304159 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 20:51:44 +0000 Subject: [PATCH 32/47] Include new classes. --- lib/finite_machine.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/finite_machine.rb b/lib/finite_machine.rb index 8b755e3..89d04e8 100644 --- a/lib/finite_machine.rb +++ b/lib/finite_machine.rb @@ -7,6 +7,8 @@ require "finite_machine/threadable" require "finite_machine/callable" require "finite_machine/catchable" +require "finite_machine/async_proxy" +require "finite_machine/async_call" require "finite_machine/event" require "finite_machine/hooks" require "finite_machine/transition" From 81104d4a94fd67ec6491e9f189eca561906bc9aa Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 20:55:19 +0000 Subject: [PATCH 33/47] Add events queue. --- lib/finite_machine/event_queue.rb | 123 ++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 lib/finite_machine/event_queue.rb diff --git a/lib/finite_machine/event_queue.rb b/lib/finite_machine/event_queue.rb new file mode 100644 index 0000000..e0fa9fd --- /dev/null +++ b/lib/finite_machine/event_queue.rb @@ -0,0 +1,123 @@ +# encoding: utf-8 + +module FiniteMachine + + # A class responsible for running asynchronous events + class EventQueue + include Enumerable + + # Initialize an event queue + # + # @example + # EventQueue.new + # + # @api public + def initialize + @queue = Queue.new + @mutex = Mutex.new + @dead = false + run + end + + # Retrieve the next event + # + # @return [AsyncCall] + # + # @api private + def next_event + @queue.pop + end + + # Add asynchronous event to the event queue + # + # @example + # event_queue << AsyncCall.build(...) + # + # @param [AsyncCall] event + # + # @return [nil] + # + # @api public + def <<(event) + @mutex.lock + begin + @queue << event + ensure + @mutex.unlock rescue nil + end + end + + # Check if there are any events to handle + # + # @example + # event_queue.empty? + # + # @api public + def empty? + @queue.empty? + end + + # Check if the event queue is alive + # + # @example + # event_queue.alive? + # + # @return [Boolean] + # + # @api public + def alive? + !@dead + end + + # Join the event queue from current thread + # + # @param [Fixnum] timeout + # + # @example + # event_queue.join + # + # @return [nil, Thread] + # + # @api public + def join(timeout) + @thread.join timeout + end + + # Shut down this event queue and clean it up + # + # @example + # event_queue.shutdown + # + # @return [Boolean] + # + # @api public + def shutdown + @mutex.lock + begin + @queue.clear + @dead = true + ensure + @mutex.unlock rescue nil + end + true + end + + private + + # Run all the events + # + # @return [Thread] + # + # @api private + def run + @thread = Thread.new do + Thread.current.abort_on_exception = true + until(@dead) do + event = next_event + Thread.exit unless event + event.dispatch + end + end + end + end # EventQueue +end # FiniteMachine From 37618456024fa88bc512528c4b170f0b130982c6 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 20:55:57 +0000 Subject: [PATCH 34/47] Expose events queue. --- lib/finite_machine.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/finite_machine.rb b/lib/finite_machine.rb index 89d04e8..71e5b97 100644 --- a/lib/finite_machine.rb +++ b/lib/finite_machine.rb @@ -10,6 +10,7 @@ require "finite_machine/async_proxy" require "finite_machine/async_call" require "finite_machine/event" +require "finite_machine/event_queue" require "finite_machine/hooks" require "finite_machine/transition" require "finite_machine/dsl" @@ -62,4 +63,8 @@ def self.define(*args, &block) StateMachine.new(*args, &block) end + def self.event_queue + Thread.current[:finite_machine_event_queue] ||= FiniteMachine::EventQueue.new + end + end # FiniteMachine From 353a8ae47e47d7668ed4c023a0080297b53eab5c Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 20:56:42 +0000 Subject: [PATCH 35/47] Add async call to state machine. --- lib/finite_machine/state_machine.rb | 16 ++++++++++++ spec/unit/async_events_spec.rb | 40 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 spec/unit/async_events_spec.rb diff --git a/lib/finite_machine/state_machine.rb b/lib/finite_machine/state_machine.rb index 6a6fedd..37c33ec 100644 --- a/lib/finite_machine/state_machine.rb +++ b/lib/finite_machine/state_machine.rb @@ -70,6 +70,22 @@ def notify(event_type, _transition, *data) end end + alias_method :sync, :method_missing + + # Explicitly invoke event on proxy or delegate to proxy + # + # @return [AsyncProxy] + # + # @api public + def async(method_name = nil, *args, &block) + @async_proxy = AsyncProxy.new(self) + if method_name + @async_proxy.method_missing method_name, *args, &block + else + @async_proxy + end + end + # Get current state # # @return [String] diff --git a/spec/unit/async_events_spec.rb b/spec/unit/async_events_spec.rb new file mode 100644 index 0000000..87230c7 --- /dev/null +++ b/spec/unit/async_events_spec.rb @@ -0,0 +1,40 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe FiniteMachine, 'async_events' do + + it 'runs events asynchronously' do + called = [] + fsm = FiniteMachine.define do + initial :green + + events { + event :slow, :green => :yellow + event :stop, :yellow => :red + event :ready, :red => :yellow + event :go, :yellow => :green + } + + callbacks { + on_enter :yellow do |event, a| called << "on_enter_yellow_#{a}" end + on_enter :red do |event, a| called << "on_enter_red_#{a}" end + } + end + + expect(fsm.current).to eql(:green) + fsm.async.slow(:foo) + FiniteMachine.event_queue.join 0.01 + expect(fsm.current).to eql(:yellow) + expect(called).to eql([ + 'on_enter_yellow_foo' + ]) + fsm.async.stop(:bar) + FiniteMachine.event_queue.join 0.01 + expect(fsm.current).to eql(:red) + expect(called).to eql([ + 'on_enter_yellow_foo', + 'on_enter_red_bar' + ]) + end +end From 1e994b5bb073ce0982a33508bef8ba4ee3f1a685 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 21:44:17 +0000 Subject: [PATCH 36/47] Document addition of async and once_on --- README.md | 158 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 92 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index d219162..87cae6a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [coverage]: https://coveralls.io/r/peter-murach/finite_machine [inchpages]: http://inch-pages.github.io/github/peter-murach/finite_machine -A minimal finite state machine with a straightforward and intuitive syntax. You can quickly model states and add callbacks that can be triggered synchronously or asynchronously. +A minimal finite state machine with a straightforward and intuitive syntax. You can quickly model states and add callbacks that can be triggered synchronously or asynchronously. The machine is event driven with a focus on passing synchronous and asynchronous messages to trigger state transitions. ## Features @@ -22,6 +22,7 @@ A minimal finite state machine with a straightforward and intuitive syntax. You * ability to check reachable states * ability to check for terminal state * conditional transitions +* sync and async transitions * sync and async callbacks (TODO - only sync) * nested/composable states (TODO) @@ -250,9 +251,9 @@ method on the **FiniteMachine** instance. As a second parameter `event` accepts in the form of `:from` and `:to` hash keys or by using the state names themselves as key value pairs. ```ruby - event :start, from: :neutral, to: :first - or - event :start, :neutral => :first +event :start, from: :neutral, to: :first +or +event :start, :neutral => :first ``` Once specified, the **FiniteMachine** will create custom methods for transitioning between each state. @@ -267,18 +268,35 @@ The following methods trigger transitions for the example state machine. In order to transition to the next reachable state, simply call the event's name on the **FiniteMachine** instance. ```ruby - fm.ready - fm.current # => :yellow +fm.ready +fm.current # => :yellow ``` Furthermore, you can pass additional parameters with the method call that will be available in the triggered callback. ```ruby - fm.go('Piotr!') - fm.current # => :green +fm.go('Piotr!') +fm.current # => :green +``` + +### 2.2 Asynchronous transitions + +By default the transitions will be fired synchronosuly. + +```ruby +fm.ready +or +fm.sync.ready +fm.current # => :yellow ``` -### 2.2 single event with multiple from states +In order to fire the event transition asynchronously use the `async` scope like so + +```ruby +fm.async.ready # => executes in separate Thread +``` + +### 2.3 single event with multiple from states If an event transitions from multiple states to the same state then all the states can be grouped into an array. Altenatively, you can create separte events under the same name for each transition that needs combining. @@ -306,49 +324,49 @@ Each event takes an optional `:if` and `:unless` options which act as a predicat You can associate the `:if` and `:unless` options with a Proc object that will get called right before transition happens. Proc object gives you ability to write inline condition instead of separate method. ```ruby - fm = FiniteMachine.define do - initial :green +fm = FiniteMachine.define do + initial :green - events { - event :slow, :green => :yellow, if: -> { return false } - } - end - fm.slow # doesn't transition to :yellow state - fm.current # => :green + events { + event :slow, :green => :yellow, if: -> { return false } + } +end +fm.slow # doesn't transition to :yellow state +fm.current # => :green ``` You can also execute methods on an associated object by passing it as an argument to `target` helper. ```ruby - class Car - def turn_engine_on - @engine_on = true - end +class Car + def turn_engine_on + @engine_on = true + end - def turn_engine_off - @engine_on = false - end + def turn_engine_off + @engine_on = false + end - def engine_on? - @engine_on - end + def engine_on? + @engine_on end +end - car = Car.new - car.turn_engine_on +car = Car.new +car.turn_engine_on - fm = FiniteMachine.define do - initial :neutral +fm = FiniteMachine.define do + initial :neutral - target car + target car - events { - event :start, :neutral => :one, if: "engine_on?" - } - end + events { + event :start, :neutral => :one, if: "engine_on?" + } +end - fm.start - fm.current # => :one +fm.start +fm.current # => :one ``` ### 3.2 Using a Symbol @@ -356,15 +374,15 @@ You can also execute methods on an associated object by passing it as an argumen You can also use a symbol corresponding to the name of a method that will get called right before transition happens. ```ruby - fsm = FiniteMachine.define do - initial :neutral +fsm = FiniteMachine.define do + initial :neutral - target car + target car - events { - event :start, :neutral => :one, if: :engine_on? - } - end + events { + event :start, :neutral => :one, if: :engine_on? + } +end ``` ### 3.3 Using a String @@ -372,15 +390,15 @@ You can also use a symbol corresponding to the name of a method that will get ca Finally, it's possible to use string that will be evaluated using `eval` and needs to contain valid Ruby code. It should only be used when the string represents a short condition. ```ruby - fsm = FiniteMachine.define do - initial :neutral +fsm = FiniteMachine.define do + initial :neutral - target car + target car - events { - event :start, :neutral => :one, if: "engine_on?" - } - end + events { + event :start, :neutral => :one, if: "engine_on?" + } +end ``` ### 3.4 Combining transition conditions @@ -388,16 +406,16 @@ Finally, it's possible to use string that will be evaluated using `eval` and nee When multiple conditions define whether or not a transition should happen, an Array can be used. Furthermore, you can apply both `:if` and `:unless` to the same transition. ```ruby - fsm = FiniteMachine.define do - initial :green +fsm = FiniteMachine.define do + initial :green - events { - event :slow, :green => :yellow, - if: [ -> { return true }, -> { return true} ], - unless: -> { return true } - event :stop, :yellow => :red - } - end + events { + event :slow, :green => :yellow, + if: [ -> { return true }, -> { return true} ], + unless: -> { return true } + event :stop, :yellow => :red + } +end ``` The transition only runs when all the `:if` conditions and none of the `unless` conditions are evaluated to `true`. @@ -462,7 +480,15 @@ This method is executed after a given event or state change happens. If you prov You can further narrow down the listener to only watch state exit changes using `on_exit_state` callback. Similarly, use `on_exit_event` to only watch for event exit changes. -### 4.4 Parameters +### 4.4 once_on + +**FiniteMachine** allows you to listen on initial state change or when the event is fired first time by using the following 3 types of callbacks: + +* `once_on_enter` +* `once_on_transition` +* `once_on_exit` + +### 4.5 Parameters All callbacks get the `TransitionEvent` object with the following attributes. @@ -490,7 +516,7 @@ end fm.ready(3) # => 'lights switching from red to yellow in 3 seconds' ``` -### 4.5 Same kind of callbacks +### 4.6 Same kind of callbacks You can define any number of the same kind of callback. These callbacks will be executed in the order they are specified. @@ -510,7 +536,7 @@ end fm.slow # => will invoke both callbacks ``` -### 4.6 Fluid callbacks +### 4.7 Fluid callbacks Callbacks can also be specified as full method calls. @@ -532,7 +558,7 @@ fm = FiniteMachine.define do end ``` -### 4.7 Executing methods inside callbacks +### 4.8 Executing methods inside callbacks In order to execute method from another object use `target` helper. @@ -590,7 +616,7 @@ fm.back # => Go Piotr! For more complex example see [Integration](#6-integration) section. -### 4.8 Defining callbacks +### 4.9 Defining callbacks When defining callbacks you are not limited to the `callbacks` helper. After **FiniteMachine** instance is created you can register callbacks the same way as before by calling `on` and supplying the type of notification and state/event you are interested in. From a7efccf8a5e8153b71302d687cef1891dbd22f7b Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 22:24:47 +0000 Subject: [PATCH 37/47] Fix spec to remove errors. --- spec/unit/callbacks_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/unit/callbacks_spec.rb b/spec/unit/callbacks_spec.rb index 438a58c..5032a42 100644 --- a/spec/unit/callbacks_spec.rb +++ b/spec/unit/callbacks_spec.rb @@ -396,9 +396,9 @@ callbacks { # generic callbacks - on_enter &callback - on_transition &callback - on_exit &callback + on_enter(&callback) + on_transition(&callback) + on_exit(&callback) # state callbacks on_enter :green, &callback From 2f304288f8ed55a43d76a77ce64e9595f7708abb Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 22:26:19 +0000 Subject: [PATCH 38/47] Add state helper definition and flesh out docs. --- lib/finite_machine/transition.rb | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/lib/finite_machine/transition.rb b/lib/finite_machine/transition.rb index a97be71..13aad86 100644 --- a/lib/finite_machine/transition.rb +++ b/lib/finite_machine/transition.rb @@ -24,6 +24,9 @@ class Transition # Initialize a Transition # + # @param [StateMachine] machine + # @param [Hash] attrs + # # @api public def initialize(machine, attrs = {}) @machine = machine @@ -35,6 +38,9 @@ def initialize(machine, attrs = {}) @conditions = make_conditions end + # Reduce conditions + # + # @api private def make_conditions @if.map { |c| Callable.new(c) } + @unless.map { |c| Callable.new(c).invert } @@ -42,6 +48,8 @@ def make_conditions # Extract states from attributes # + # @param [Hash] attrs + # # @api private def parse_states(attrs) _attrs = attrs.dup @@ -66,6 +74,25 @@ def define end end + # Define helper state mehods for the transition states + # + # @api private + def define_state_methods + from.concat([to]).each { |state| define_state_method(state) } + end + + # Define state helper method + # + # @param [Symbol] state + # + # @api private + def define_state_method(state) + return if machine.respond_to?("#{state}?") + machine.send(:define_singleton_method, "#{state}?") do + machine.is?(state.to_sym) + end + end + # Define event on the machine # # @api private @@ -83,6 +110,8 @@ def define_event # Execute current transition # + # @return [nil] + # # @api private def call sync_exclusive do @@ -99,6 +128,11 @@ def to_s @name end + # Return string representation + # + # @return [String] + # + # @api public def inspect "<#{self.class} name: #{@name}, transitions: #{@from} => #{@to}, when: #{@conditions}>" end @@ -107,6 +141,13 @@ def inspect # Raise error when not enough transitions are provided # + # @param [Hash] attrs + # + # @raise [NotEnoughTransitionsError] + # if the event has not enough transition arguments + # + # @return [nil] + # # @api private def raise_not_enough_transitions(attrs) raise NotEnoughTransitionsError, "please provide state transitions for '#{attrs.inspect}'" From babd056bcaf681712482ba4279729aaeb36e5c63 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 22:26:49 +0000 Subject: [PATCH 39/47] Add ability to easily query states. --- lib/finite_machine/dsl.rb | 1 + lib/finite_machine/state_machine.rb | 2 +- spec/unit/is_spec.rb | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/finite_machine/dsl.rb b/lib/finite_machine/dsl.rb index 9a166bd..46f5709 100644 --- a/lib/finite_machine/dsl.rb +++ b/lib/finite_machine/dsl.rb @@ -130,6 +130,7 @@ def event(name, attrs = {}, &block) sync_exclusive do _transition = Transition.new(machine, attrs.merge!(name: name)) _transition.define + _transition.define_state_methods _transition.define_event end end diff --git a/lib/finite_machine/state_machine.rb b/lib/finite_machine/state_machine.rb index 37c33ec..2e5685d 100644 --- a/lib/finite_machine/state_machine.rb +++ b/lib/finite_machine/state_machine.rb @@ -95,7 +95,7 @@ def current state end - # Check if current state machtes provided state + # Check if current state matches provided state # # @param [String, Array[String]] state # diff --git a/spec/unit/is_spec.rb b/spec/unit/is_spec.rb index 56128ed..e01c370 100644 --- a/spec/unit/is_spec.rb +++ b/spec/unit/is_spec.rb @@ -30,4 +30,26 @@ expect(fsm.is?([:green, :red])).to be_false expect(fsm.is?([:yellow, :red])).to be_true end + + it "defines helper methods to check current state" do + fsm = FiniteMachine.define do + initial :green + + events { + event :slow, :green => :yellow + event :stop, :yellow => :red + event :ready, :red => :yellow + event :go, :yellow => :green + } + end + expect(fsm.current).to eql(:green) + + expect(fsm.green?).to be_true + expect(fsm.yellow?).to be_false + + fsm.slow + + expect(fsm.green?).to be_false + expect(fsm.yellow?).to be_true + end end From a960185d3a29a7bdc4cb2a232111cd3424d4e3e4 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sat, 29 Mar 2014 22:29:41 +0000 Subject: [PATCH 40/47] Document state helpers. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 87cae6a..b24de65 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,13 @@ fm.is?(:red) # => true fm.is?(:yellow) # => false ``` +Moreover, you can use helper methods to check for current state using the state name itself like so + +```ruby +fm.red? # => true +fm.yellow? # => false +``` + ### 1.5 can? and cannot? To verify whether or not an event can be fired, **FiniteMachine** provides `can?` or `cannot?` methods. `can?` checks if **FiniteMachine** can fire a given event, returning true, otherwise, it will return false. `cannot?` is simply the inverse of `can?`. From 7b68f68efc5eaafc9fac554c583e6aef12bb5acd Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sun, 30 Mar 2014 15:44:52 +0100 Subject: [PATCH 41/47] Add thread context. --- lib/finite_machine/thread_context.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 lib/finite_machine/thread_context.rb diff --git a/lib/finite_machine/thread_context.rb b/lib/finite_machine/thread_context.rb new file mode 100644 index 0000000..f0d9fe4 --- /dev/null +++ b/lib/finite_machine/thread_context.rb @@ -0,0 +1,16 @@ +# encoding: utf-8 + +module FiniteMachine + + # A mixin to allow sharing of thread context + module ThreadContext + + def event_queue + Thread.current[:finite_machine_event_queue] + end + + def event_queue=(value) + Thread.current[:finite_machine_event_queue] = value + end + end # ThreadContext +end # FiniteMachine From d848194302f625402cb6ae3198a3a1eaca249bf9 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sun, 30 Mar 2014 15:45:18 +0100 Subject: [PATCH 42/47] Remove event queue from main module. --- lib/finite_machine.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/finite_machine.rb b/lib/finite_machine.rb index 71e5b97..0523994 100644 --- a/lib/finite_machine.rb +++ b/lib/finite_machine.rb @@ -5,6 +5,7 @@ require "finite_machine/version" require "finite_machine/threadable" +require "finite_machine/thread_context" require "finite_machine/callable" require "finite_machine/catchable" require "finite_machine/async_proxy" @@ -62,9 +63,4 @@ module FiniteMachine def self.define(*args, &block) StateMachine.new(*args, &block) end - - def self.event_queue - Thread.current[:finite_machine_event_queue] ||= FiniteMachine::EventQueue.new - end - end # FiniteMachine From 3dcd597dae93e943450da14656038dd550c29e74 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sun, 30 Mar 2014 15:48:26 +0100 Subject: [PATCH 43/47] Change async proxy to use thread context and threadsafe attribute. --- lib/finite_machine/async_proxy.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/finite_machine/async_proxy.rb b/lib/finite_machine/async_proxy.rb index 3c9fe6f..f288103 100644 --- a/lib/finite_machine/async_proxy.rb +++ b/lib/finite_machine/async_proxy.rb @@ -3,8 +3,10 @@ module FiniteMachine # An asynchronous messages proxy class AsyncProxy + include Threadable + include ThreadContext - attr_reader :context + attr_threadsafe :context # Initialize an AsynxProxy # @@ -13,15 +15,14 @@ class AsyncProxy # # @api private def initialize(context) - @context = context + self.context = context end # Delegate asynchronous event to event queue # # @api private def method_missing(method_name, *args, &block) - @event_queue = FiniteMachine.event_queue - @event_queue << AsyncCall.build(@context, Callable.new(method_name), *args, &block) + event_queue << AsyncCall.build(context, Callable.new(method_name), *args, &block) end end # AsyncProxy end # FiniteMachine From c435cd932b3732679e31d58da5e82eaae87026b5 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sun, 30 Mar 2014 15:54:06 +0100 Subject: [PATCH 44/47] Use thread context. --- lib/finite_machine/state_machine.rb | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/finite_machine/state_machine.rb b/lib/finite_machine/state_machine.rb index 2e5685d..d8427a5 100644 --- a/lib/finite_machine/state_machine.rb +++ b/lib/finite_machine/state_machine.rb @@ -6,6 +6,7 @@ module FiniteMachine class StateMachine include Threadable include Catchable + include ThreadContext # Initial state, defaults to :none attr_threadsafe :initial_state @@ -51,8 +52,13 @@ def initialize(*args, &block) @dsl = DSL.new self @dsl.call(&block) if block_given? send(:"#{@dsl.initial_event}") unless @dsl.defer + self.event_queue = FiniteMachine::EventQueue.new end + # @example + # machine.subscribe(Observer.new(machine)) + # + # @api public def subscribe(*observers) @subscribers.subscribe(*observers) end @@ -70,6 +76,14 @@ def notify(event_type, _transition, *data) end end + # Help to mark the event as synchronous + # + # @example + # fsm.sync.go + # + # @return [self] + # + # @api public alias_method :sync, :method_missing # Explicitly invoke event on proxy or delegate to proxy @@ -97,6 +111,9 @@ def current # Check if current state matches provided state # + # @example + # fsm.is?(:green) # => true + # # @param [String, Array[String]] state # # @return [Boolean] @@ -133,6 +150,9 @@ def event_names # Checks if event can be triggered # + # @example + # fsm.can?(:go) # => true + # # @param [String] event # # @return [Boolean] @@ -144,6 +164,9 @@ def can?(event) # Checks if event cannot be triggered # + # @example + # fsm.cannot?(:go) # => false + # # @param [String] event # # @return [Boolean] @@ -179,6 +202,12 @@ def valid_state?(_transition) # Performs transition # + # @param [Transition] _transition + # @param [Array] args + # + # @return [Integer] + # the status code for the transition + # # @api private def transition(_transition, *args, &block) return CANCELLED if valid_state?(_transition) From bd8e2c72b69eaaf655e00c6a232283fbffe8d86e Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sun, 30 Mar 2014 15:54:38 +0100 Subject: [PATCH 45/47] Test async behaviour for multiple state machines. --- spec/unit/async_events_spec.rb | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/spec/unit/async_events_spec.rb b/spec/unit/async_events_spec.rb index 87230c7..e2c8c01 100644 --- a/spec/unit/async_events_spec.rb +++ b/spec/unit/async_events_spec.rb @@ -24,17 +24,45 @@ expect(fsm.current).to eql(:green) fsm.async.slow(:foo) - FiniteMachine.event_queue.join 0.01 + fsm.event_queue.join 0.01 expect(fsm.current).to eql(:yellow) expect(called).to eql([ 'on_enter_yellow_foo' ]) fsm.async.stop(:bar) - FiniteMachine.event_queue.join 0.01 + fsm.event_queue.join 0.01 expect(fsm.current).to eql(:red) expect(called).to eql([ 'on_enter_yellow_foo', 'on_enter_red_bar' ]) end + + it "ensure queue per thread" do + called = [] + fsmFoo = FiniteMachine.define do + initial :green + events { event :slow, :green => :yellow } + + callbacks { + on_enter :yellow do |event, a| called << "(foo)on_enter_yellow_#{a}" end + } + end + fsmBar = FiniteMachine.define do + initial :green + events { event :slow, :green => :yellow } + + callbacks { + on_enter :yellow do |event, a| called << "(bar)on_enter_yellow_#{a}" end + } + end + fsmFoo.slow(:foo) + fsmBar.slow(:bar) + fsmFoo.event_queue.join 0.01 + fsmBar.event_queue.join 0.01 + expect(called).to include('(foo)on_enter_yellow_foo') + expect(called).to include('(bar)on_enter_yellow_bar') + expect(fsmFoo.current).to eql(:yellow) + expect(fsmBar.current).to eql(:yellow) + end end From 0f1aa9f2f2b4d2e0b13bcc0bf516d78c049831bb Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sun, 30 Mar 2014 15:59:58 +0100 Subject: [PATCH 46/47] Note changes for release. --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15ac976..0532f82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +0.3.0 (March 30, 2014) + +* Move development dependencies to Gemfile +* Increase test coverage to 95% +* Fix bug with event methods dynamic redefinition +* Change attr_threadsafe to accept default values +* Fix observer respond_to +* Add ability to specify callbacks on machine instance +* Add once_on type of callback +* Add off method for removing callbacks +* Add async method to state_machine for asynchronous events firing +* Fix Callable to correctly forward arguments +* Add state helpers fsm.green? to allow easily check current state + 0.2.0 (March 01, 2014) * Ensure correct transition object state From f3ed59fa65c7870f5108bf776b07b6a9d76b0fc2 Mon Sep 17 00:00:00 2001 From: Piotr Murach Date: Sun, 30 Mar 2014 16:00:18 +0100 Subject: [PATCH 47/47] Bump gem version. --- lib/finite_machine/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/finite_machine/version.rb b/lib/finite_machine/version.rb index bf43364..56a87aa 100644 --- a/lib/finite_machine/version.rb +++ b/lib/finite_machine/version.rb @@ -1,5 +1,5 @@ # encoding: utf-8 module FiniteMachine - VERSION = "0.2.0" + VERSION = "0.3.0" end