├── .gitignore ├── .hound.yml ├── .rubocop.yml ├── .travis.yml ├── LICENSE ├── README.md ├── aquam.gemspec ├── lib ├── aquam.rb └── aquam │ ├── errors.rb │ ├── event_transitions.rb │ ├── machine.rb │ ├── machine_class_methods.rb │ └── state.rb ├── rakefile └── test ├── door_state_machine.rb ├── helper.rb ├── machine_class_methods_test.rb ├── machine_test.rb └── state_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | aquam-*.gem 2 | .ruby-* 3 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | LineLength: 2 | Description: 'Limit lines to 80 characters.' 3 | Max: 90 4 | 5 | StringLiterals: 6 | Description: 'Checks if uses of quotes match the configured preference.' 7 | Enabled: false 8 | 9 | Documentation: 10 | Description: 'Document classes and non-namespace modules.' 11 | Enabled: false 12 | 13 | MethodLength: 14 | Description: Avoid methods longer than 25 lines of code. 15 | Enabled: true 16 | CountComments: false 17 | Max: 25 18 | 19 | ClassAndModuleChildren: 20 | Description: Checks style of children classes and modules. 21 | Enabled: false 22 | EnforcedStyle: nested 23 | 24 | AlignParameters: 25 | Enabled: false 26 | 27 | GuardClause: 28 | Enabled: false 29 | 30 | DoubleNegation: 31 | Enabled: false 32 | 33 | CyclomaticComplexity: 34 | Max: 10 35 | 36 | DotPosition: 37 | Description: 'Checks the position of the dot in multi-line method calls.' 38 | Enabled: false #Disabled because hound != rubocop 39 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Metrics/LineLength: 2 | Description: 'Limit lines to 80 characters.' 3 | Max: 80 4 | 5 | Style/StringLiterals: 6 | Description: 'Checks if uses of quotes match the configured preference.' 7 | Enabled: false 8 | 9 | Style/IfUnlessModifier: 10 | Enabled: false 11 | 12 | Style/RaiseArgs: 13 | Description: 'Check if exceptions are raised in a particular way' 14 | Enabled: false 15 | 16 | Style/Documentation: 17 | Description: 'Document classes and non-namespace modules.' 18 | Enabled: false 19 | 20 | Metrics/MethodLength: 21 | Description: Avoid methods longer than 25 lines of code. 22 | Enabled: true 23 | CountComments: false 24 | Max: 25 25 | 26 | Style/ClassAndModuleChildren: 27 | Description: Checks style of children classes and modules. 28 | Enabled: false 29 | EnforcedStyle: nested 30 | 31 | Style/AlignParameters: 32 | Enabled: false 33 | 34 | Style/GuardClause: 35 | Enabled: false 36 | 37 | Style/DoubleNegation: 38 | Enabled: false 39 | 40 | Metrics/CyclomaticComplexity: 41 | Max: 10 42 | 43 | Style/NumericLiterals: 44 | Enabled: false 45 | 46 | Style/ClassVars: 47 | Enabled: false 48 | 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.0 4 | - 2.2.0 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Emiliano Mancuso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aquam 2 | [![Gem Version](https://badge.fury.io/rb/aquam.svg)](http://badge.fury.io/rb/aquam) 3 | [![Build Status](https://travis-ci.org/emancu/aquam.svg)](https://travis-ci.org/emancu/aquam) 4 | [![Code Climate](https://codeclimate.com/github/emancu/aquam/badges/gpa.svg)](https://codeclimate.com/github/emancu/aquam) 5 | [![Dependency Status](https://gemnasium.com/emancu/aquam.svg)](https://gemnasium.com/emancu/aquam) 6 | 7 | A Ruby DSL for writing Finite State Machines and validate its transitions' 8 | 9 | ## Dependencies 10 | 11 | `aquam` requires Ruby 2.1.x or later. No more dependencies. 12 | 13 | ## Installation 14 | 15 | $ gem install aquam 16 | 17 | # Getting started 18 | 19 | `aquam` helps you to define _Finite State Machines_ with a very simple DSL which 20 | also will validate events, states and the transition between them. 21 | 22 | First of all, you must know that a State Machine should be a different object, 23 | where you specify the valid states and the transitions fired by the events. 24 | 25 | That being said, lets take a look how it works. 26 | 27 | ## Machine 28 | 29 | Basically a Machine consists on 30 | 31 | - states 32 | - events 33 | - transitions 34 | 35 | There are three key words in our DSL that will help you to write your 36 | own Finite State Machine, plus the `attribute` method. 37 | 38 | ### Example 39 | 40 | ```ruby 41 | class DoorStateMachine < Aquam::Machine 42 | state :opened, OpenedDoorState 43 | state :closed, ClosedDoorState 44 | 45 | event :open do 46 | transition from: :closed, to: :opened 47 | end 48 | 49 | event :close do 50 | transition from: :opened, to: :closed 51 | end 52 | 53 | event :knock do 54 | transition from: :opened, to: :opened 55 | transition from: :closed, to: :closed 56 | end 57 | end 58 | ``` 59 | 60 | > NOTE: `OpenedDoorState` and `ClosedDoorState` definitions are missing but 61 | > we will cover States definition later. 62 | 63 | ### state 64 | 65 | A `state` maps a symbol to a State class. 66 | It tells to the _machine_ it is a valid state and which class represents it. 67 | 68 | ```ruby 69 | state :opened, OpenedDoorState 70 | ``` 71 | 72 | ### event 73 | 74 | An `event` is a method which triggers the transition from one _state_ to another. 75 | Each _state object_ must define **only** the events that are specified here. 76 | 77 | ```ruby 78 | event :open do 79 | ... 80 | end 81 | ``` 82 | 83 | ### transition 84 | 85 | A `transition` moves the _state machine_ from **state A** to **state B**. 86 | It can only be defined inside an `event` and you can define multiple transitions. 87 | 88 | ```ruby 89 | transition from: :a_valid_state, to: :other_valid_state 90 | ``` 91 | 92 | ### attribute 93 | 94 | The `attribute` holds the name of the accessor in your own class where 95 | the state name (string or symbol) will be stored. 96 | 97 | By default uses `:state` as method accessor. 98 | 99 | ```ruby 100 | attribute :state 101 | ``` 102 | 103 | ### Extra 104 | 105 | Being a subclass of `Aquam::Machine` also gives you some helpful **class methods**: 106 | 107 | | Class Method | Description | Example (ruby) | 108 | |:-------------|:-------------------------------------------------|:--------------------------------| 109 | | states | `Hash` Valid states mapped to a class | `{ opened: OpenedDoorState }` | 110 | | events | `Hash` Valid events with all its transitions | `{ open: { closed: :opened } }` | 111 | | valid_state? | `Boolean` Check if it is a valid state | `true` | 112 | | valid_event? | `Boolean` Check if it is a valid event | `true` | 113 | 114 | And for **instance methods** it defines: 115 | 116 | | Class Method | Description | Example (ruby) | 117 | |:------------------|:-----------------------------------------|:------------------------------| 118 | | current_state | `Aquam::State` Instance of current state | `#` | 119 | | trigger | `Aquam::State` Instance of the new state | `#` | 120 | | valid_state? | `Boolean` Check if it is a valid state | `true` | 121 | | valid_event? | `Boolean` Check if it is a valid event | `true` | 122 | | valid_transition? | `Boolean` Check if it is a valid event | `true` | 123 | 124 | 125 | ## State 126 | 127 | For each state, we define a class that implements the corresponding _events_. 128 | Every bit of behavior that is *state-dependent* should become a method in the class. 129 | `aquam` uses metaprogramming to define methods for every single event listed 130 | in the state machine used. 131 | 132 | ### Example 133 | 134 | ```ruby 135 | class OpenedDoorState < Aquam::State 136 | use_machine DoorStateMachine 137 | 138 | def close 139 | # Do something 140 | 141 | @object.state = :closed 142 | end 143 | end 144 | 145 | class ClosedDoorState < Aquam::State 146 | use_machine DoorStateMachine 147 | 148 | def open 149 | # Do something 150 | 151 | @object.state = :opened 152 | end 153 | end 154 | ``` 155 | 156 | ### use_machine 157 | 158 | This is the only method that you **must** call from every State class, 159 | in order to define the interface according to the _state machine_. 160 | Basically, it defines a method for every event defined in the _state machine_. 161 | 162 | ```ruby 163 | use_machine DoorStateMachine 164 | ``` 165 | > NOTE: You can not change its value and it is accessible from all subclasses. 166 | -------------------------------------------------------------------------------- /aquam.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'aquam' 3 | s.version = '0.0.2' 4 | s.date = Time.now.strftime('%Y-%m-%d') 5 | s.summary = 'DSL to define State Machines' 6 | s.description = 'A Ruby DSL for writing Finite State Machines and validate its transitions' 7 | s.authors = ['Emiliano Mancuso'] 8 | s.email = ['emiliano.mancuso@gmail.com'] 9 | s.homepage = 'http://github.com/emancu/aquam' 10 | s.license = 'MIT' 11 | 12 | s.files = Dir[ 13 | 'README.md', 14 | 'rakefile', 15 | 'lib/**/*.rb', 16 | '*.gemspec' 17 | ] 18 | s.test_files = Dir['test/*.*'] 19 | end 20 | -------------------------------------------------------------------------------- /lib/aquam.rb: -------------------------------------------------------------------------------- 1 | require_relative 'aquam/errors' 2 | require_relative 'aquam/machine' 3 | require_relative 'aquam/state' 4 | 5 | module Aquam 6 | VERSION = '0.0.2' 7 | end 8 | -------------------------------------------------------------------------------- /lib/aquam/errors.rb: -------------------------------------------------------------------------------- 1 | module Aquam 2 | class InvalidStateError < StandardError; end 3 | class InvalidEventError < StandardError; end 4 | class InvalidTransitionError < StandardError; end 5 | class InvalidStateMachineError < StandardError; end 6 | 7 | class FailedTransitionError < StandardError 8 | attr_reader :errors 9 | 10 | def initialize(errors = {}) 11 | @errors = errors 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/aquam/event_transitions.rb: -------------------------------------------------------------------------------- 1 | module Aquam 2 | class EventTransitions 3 | def initialize(machine, event_name, &block) 4 | @machine = machine 5 | @event_name = event_name 6 | instance_eval(&block) 7 | end 8 | 9 | def transition(from:, to:) 10 | @machine.transition(from, to, @event_name) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/aquam/machine.rb: -------------------------------------------------------------------------------- 1 | require_relative 'machine_class_methods' 2 | 3 | module Aquam 4 | class Machine 5 | extend Aquam::MachineClassMethods 6 | 7 | attr_accessor :object 8 | 9 | def initialize(object) 10 | @object = object 11 | end 12 | 13 | def valid_state? 14 | self.class.valid_state? attribute 15 | end 16 | 17 | def valid_event?(event) 18 | self.class.valid_event? event 19 | end 20 | 21 | def valid_transition?(event) 22 | self.class.events[event].key? attribute 23 | end 24 | 25 | def current_state 26 | fail Aquam::InvalidStateError unless valid_state? 27 | 28 | self.class.states[attribute].new object 29 | end 30 | 31 | def trigger(event, *args) 32 | state = current_state 33 | 34 | fail Aquam::InvalidEventError unless valid_event? event 35 | fail Aquam::InvalidTransitionError unless valid_transition? event 36 | 37 | state.send event, *args 38 | 39 | current_state 40 | end 41 | 42 | private 43 | 44 | def attribute 45 | object.send(self.class.attribute.to_sym).to_sym 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/aquam/machine_class_methods.rb: -------------------------------------------------------------------------------- 1 | require_relative 'event_transitions' 2 | 3 | module Aquam 4 | module MachineClassMethods 5 | def attribute(name = nil) 6 | name ? @attribute = name : @attribute ||= :state 7 | end 8 | 9 | def states 10 | @states ||= {} 11 | end 12 | 13 | def events 14 | @event ||= Hash.new { |hash, key| hash[key] = {} } 15 | end 16 | 17 | def state(name, klass) 18 | states[name] = klass 19 | end 20 | 21 | def event(name, &block) 22 | Aquam::EventTransitions.new self, name, &block 23 | end 24 | 25 | def transition(from, to, event_name) 26 | fail Aquam::InvalidStateError if !valid_state?(from) || !valid_state?(to) 27 | 28 | events[event_name][from] = to 29 | end 30 | 31 | def valid_state?(state) 32 | states.keys.include? state 33 | end 34 | 35 | def valid_event?(event) 36 | events.keys.include? event 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/aquam/state.rb: -------------------------------------------------------------------------------- 1 | module Aquam 2 | class State 3 | def initialize(object) 4 | @object = object 5 | end 6 | 7 | def state_machine 8 | self.class.state_machine || fail(Aquam::InvalidStateMachineError) 9 | end 10 | 11 | class << self 12 | def state_machine 13 | @state_machine 14 | end 15 | 16 | def use_machine(state_machine) 17 | @state_machine ||= begin 18 | validate_state_machine state_machine 19 | define_event_methods state_machine 20 | 21 | state_machine 22 | end 23 | end 24 | 25 | private 26 | 27 | def define_event_methods(machine) 28 | machine.events.keys.each do |event| 29 | define_method event do 30 | fail Aquam::InvalidTransitionError 31 | end 32 | end 33 | end 34 | 35 | def validate_state_machine(machine) 36 | unless machine && machine.ancestors.include?(Aquam::Machine) 37 | fail Aquam::InvalidStateMachineError 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /rakefile: -------------------------------------------------------------------------------- 1 | task :default => :test 2 | 3 | desc 'Run tests' 4 | task :test do 5 | require File.expand_path("./test/helper", File.dirname(__FILE__)) 6 | 7 | Dir["test/**/*_test.rb"].each { |file| load file } 8 | end 9 | -------------------------------------------------------------------------------- /test/door_state_machine.rb: -------------------------------------------------------------------------------- 1 | require 'aquam' 2 | 3 | class OpenedDoorState < Aquam::State 4 | def close 5 | @object.state = :closed 6 | end 7 | end 8 | 9 | class ClosedDoorState < Aquam::State 10 | def open 11 | @object.state = :opened 12 | end 13 | end 14 | 15 | class DoorStateMachine < Aquam::Machine 16 | state :opened, OpenedDoorState 17 | state :closed, ClosedDoorState 18 | 19 | event :open do 20 | transition from: :closed, to: :opened 21 | end 22 | 23 | event :close do 24 | transition from: :opened, to: :closed 25 | end 26 | 27 | event :knock do 28 | transition from: :opened, to: :opened 29 | transition from: :closed, to: :closed 30 | end 31 | end 32 | 33 | class Door 34 | attr_accessor :state 35 | attr_reader :machine 36 | 37 | def initialize 38 | @state = :closed 39 | @machine = DoorStateMachine.new self 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib')) 2 | 3 | require 'minitest/autorun' 4 | require 'aquam' 5 | 6 | def deny(condition, message = 'Expected condition to be unsatisfied') 7 | assert !condition, message 8 | end 9 | -------------------------------------------------------------------------------- /test/machine_class_methods_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | require_relative 'door_state_machine' 3 | 4 | describe Aquam::MachineClassMethods do 5 | describe 'attribute' do 6 | it 'uses :state as default' do 7 | assert_equal :state, DoorStateMachine.attribute 8 | end 9 | 10 | it 'defines the proper attribute to access the state name' do 11 | class NewStateMachine < Aquam::Machine 12 | attribute :state_name 13 | end 14 | 15 | assert_equal :state_name, NewStateMachine.attribute 16 | 17 | Object.send(:remove_const, :NewStateMachine) 18 | end 19 | end 20 | 21 | describe 'states' do 22 | it 'returns a hash with all the states as keys' do 23 | assert_equal [:opened, :closed], DoorStateMachine.states.keys 24 | end 25 | 26 | it 'returns a hash with all the states defined' do 27 | assert_equal OpenedDoorState, DoorStateMachine.states[:opened] 28 | assert_equal ClosedDoorState, DoorStateMachine.states[:closed] 29 | end 30 | 31 | it 'checks if it is a valid state' do 32 | assert DoorStateMachine.valid_state? :opened 33 | deny DoorStateMachine.valid_state? :not_a_valid_state 34 | end 35 | end 36 | 37 | describe 'events' do 38 | it 'returns a hash with the events as keys' do 39 | assert_equal [:open, :close, :knock], DoorStateMachine.events.keys 40 | end 41 | 42 | it 'returns a hash with the transitions of each event' do 43 | close = { opened: :closed } 44 | open = { closed: :opened } 45 | knock = { opened: :opened, closed: :closed } 46 | 47 | assert_equal close, DoorStateMachine.events[:close] 48 | assert_equal open, DoorStateMachine.events[:open] 49 | assert_equal knock, DoorStateMachine.events[:knock] 50 | end 51 | 52 | it 'checks if it is a valid event' do 53 | assert DoorStateMachine.valid_event? :open 54 | deny DoorStateMachine.valid_event? :not_a_valid_event 55 | end 56 | end 57 | 58 | describe 'state' do 59 | before do 60 | class AState < Aquam::State; end 61 | class StateMachine < Aquam::Machine; end 62 | end 63 | 64 | after do 65 | Object.send(:remove_const, :AState) 66 | Object.send(:remove_const, :StateMachine) 67 | end 68 | 69 | it 'defines a new state into the Machine' do 70 | assert_equal [], StateMachine.states.keys 71 | 72 | StateMachine.state :a, AState 73 | 74 | assert_equal [:a], StateMachine.states.keys 75 | assert_equal AState, StateMachine.states[:a] 76 | end 77 | end 78 | 79 | describe 'event' do 80 | before do 81 | class AState < Aquam::State; end 82 | class BState < Aquam::State; end 83 | 84 | class StateMachine < Aquam::Machine 85 | state :a, AState 86 | state :b, BState 87 | end 88 | end 89 | 90 | after do 91 | Object.send(:remove_const, :AState) 92 | Object.send(:remove_const, :BState) 93 | Object.send(:remove_const, :StateMachine) 94 | end 95 | 96 | it 'defines an event with transitions' do 97 | assert_equal [], StateMachine.events.keys 98 | 99 | StateMachine.event :toggle do 100 | transition from: :a, to: :b 101 | transition from: :b, to: :a 102 | end 103 | 104 | assert_equal [:toggle], StateMachine.events.keys 105 | end 106 | end 107 | 108 | describe 'transition' do 109 | before do 110 | class AState < Aquam::State; end 111 | class BState < Aquam::State; end 112 | 113 | class StateMachine < Aquam::Machine 114 | state :a, AState 115 | state :b, BState 116 | end 117 | end 118 | 119 | after do 120 | Object.send(:remove_const, :AState) 121 | Object.send(:remove_const, :BState) 122 | Object.send(:remove_const, :StateMachine) 123 | end 124 | 125 | it 'fails defining a transition between invalid states' do 126 | assert_raises Aquam::InvalidStateError do 127 | StateMachine.transition :undefined, :b, :event 128 | end 129 | 130 | assert_raises Aquam::InvalidStateError do 131 | StateMachine.transition :a, :undefined, :event 132 | end 133 | end 134 | 135 | it 'defines a new transition' do 136 | assert_equal Hash.new, StateMachine.events[:event] 137 | 138 | StateMachine.transition :a, :b, :event 139 | expected_transition = { a: :b } 140 | 141 | assert_equal expected_transition, StateMachine.events[:event] 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /test/machine_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | require_relative 'door_state_machine' 3 | 4 | describe Aquam::Machine do 5 | before do 6 | @door = Door.new 7 | @machine = @door.machine 8 | end 9 | 10 | describe 'valid_*? methods' do 11 | it 'returns a boolean if is a valid state' do 12 | assert @machine.valid_state? 13 | 14 | @door.state = :wrong_state 15 | 16 | deny @machine.valid_state? 17 | end 18 | 19 | it 'returns a boolean if is a valid event' do 20 | assert @machine.valid_event? :open 21 | deny @machine.valid_event? :lock 22 | end 23 | 24 | it 'returns a boolean if is a valid transition from the current state' do 25 | deny @machine.valid_transition? :close 26 | assert @machine.valid_transition? :open 27 | end 28 | end 29 | 30 | describe 'current_state' do 31 | it 'returns an instance of the current state object' do 32 | assert @machine.current_state.instance_of? ClosedDoorState 33 | 34 | @door.state = :opened 35 | 36 | assert @machine.current_state.instance_of? OpenedDoorState 37 | end 38 | 39 | it 'fails if the current state was not defined into the machine' do 40 | assert_raises Aquam::InvalidStateError do 41 | @door.state = :must_fail 42 | @machine.current_state 43 | end 44 | end 45 | end 46 | 47 | describe 'trigger' do 48 | it 'fires the event and returns the new state' do 49 | assert @machine.current_state.instance_of? ClosedDoorState 50 | 51 | new_state = @machine.trigger(:open) 52 | 53 | assert new_state.instance_of? OpenedDoorState 54 | end 55 | 56 | it 'fails if is not a valid event' do 57 | assert_raises Aquam::InvalidEventError do 58 | @machine.trigger :lock 59 | end 60 | end 61 | 62 | it 'fails if is not a valid transition' do 63 | assert_raises Aquam::InvalidTransitionError do 64 | @machine.trigger :close 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/state_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | require_relative 'door_state_machine' 3 | 4 | describe Aquam::State do 5 | after do 6 | OpenedDoorState.instance_variable_set :@state_machine, nil 7 | end 8 | 9 | describe 'use_machine class method' do 10 | it 'fails if it is not a valid Aquam::Machine class' do 11 | assert_raises Aquam::InvalidStateMachineError do 12 | OpenedDoorState.use_machine String 13 | end 14 | end 15 | 16 | it 'defines the state machine that will be used in the state' do 17 | assert_equal nil, OpenedDoorState.state_machine 18 | 19 | OpenedDoorState.use_machine DoorStateMachine 20 | 21 | assert_equal DoorStateMachine, OpenedDoorState.state_machine 22 | end 23 | 24 | it 'defines two different state machines for two different states' do 25 | class OpenedWindowState < Aquam::State; end 26 | class WindowStateMachine < Aquam::Machine 27 | state :opened, OpenedWindowState 28 | end 29 | 30 | OpenedDoorState.use_machine DoorStateMachine 31 | OpenedWindowState.use_machine WindowStateMachine 32 | 33 | assert_equal DoorStateMachine, OpenedDoorState.state_machine 34 | assert_equal WindowStateMachine, OpenedWindowState.state_machine 35 | 36 | Object.send(:remove_const, :WindowStateMachine) 37 | Object.send(:remove_const, :OpenedWindowState) 38 | end 39 | 40 | it 'defines the state machine only once' do 41 | class WindowStateMachine < Aquam::Machine; end 42 | 43 | OpenedDoorState.use_machine DoorStateMachine 44 | OpenedDoorState.use_machine WindowStateMachine 45 | 46 | assert_equal DoorStateMachine, OpenedDoorState.state_machine 47 | 48 | Object.send(:remove_const, :WindowStateMachine) 49 | end 50 | 51 | it 'defines all the events as methods' do 52 | OpenedDoorState.use_machine DoorStateMachine 53 | 54 | assert OpenedDoorState.instance_methods.include? :open 55 | assert OpenedDoorState.instance_methods.include? :close 56 | assert OpenedDoorState.instance_methods.include? :knock 57 | end 58 | 59 | it 'fails by default on every event method' do 60 | OpenedDoorState.use_machine DoorStateMachine 61 | 62 | assert_raises Aquam::InvalidTransitionError do 63 | OpenedDoorState.new(nil).open 64 | end 65 | 66 | assert_raises Aquam::InvalidTransitionError do 67 | OpenedDoorState.new(nil).close 68 | end 69 | 70 | assert_raises Aquam::InvalidTransitionError do 71 | OpenedDoorState.new(nil).knock 72 | end 73 | end 74 | end 75 | 76 | describe 'state_machine instance method' do 77 | it 'returns the state machine class defined' do 78 | OpenedDoorState.use_machine DoorStateMachine 79 | 80 | assert_equal DoorStateMachine, OpenedDoorState.new(nil).state_machine 81 | end 82 | 83 | it 'fails if the state machine was not defined' do 84 | assert_raises Aquam::InvalidStateMachineError do 85 | OpenedDoorState.new(nil).state_machine 86 | end 87 | end 88 | end 89 | end 90 | --------------------------------------------------------------------------------