Skip to content

Commit

Permalink
Merge 051a772 into 73e86e6
Browse files Browse the repository at this point in the history
  • Loading branch information
hopsoft committed May 3, 2017
2 parents 73e86e6 + 051a772 commit e00a097
Show file tree
Hide file tree
Showing 15 changed files with 620 additions and 324 deletions.
97 changes: 97 additions & 0 deletions .rubocop.yml
@@ -0,0 +1,97 @@
AllCops:
TargetRubyVersion: 2.3
# RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
# to ignore them, so only the ones explicitly set in this file are enabled.
DisabledByDefault: true
Exclude:
- '**/bin/*'
- '**/db/**/*'
- '**/templates/**/*'
- '**/vendor/**/*'

# Prefer &&/|| over and/or.
Style/AndOr:
Enabled: true

# Do not use braces for hash literals when they are the last argument of a
# method call.
Style/BracesAroundHashParameters:
Enabled: true

# Align `when` with `case`.
Style/CaseIndentation:
Enabled: true

# No extra empty lines.
Style/EmptyLines:
Enabled: true

# In a regular class definition, no empty lines around the body.
Style/EmptyLinesAroundClassBody:
Enabled: true

# In a regular module definition, no empty lines around the body.
Style/EmptyLinesAroundModuleBody:
Enabled: true

# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
Style/HashSyntax:
Enabled: true

# Method definitions after `private` or `protected` isolated calls need one
# extra level of indentation.
Style/IndentationConsistency:
Enabled: true
EnforcedStyle: rails

# Two spaces, no tabs (for indentation).
Style/IndentationWidth:
Enabled: true

# Defining a method with parameters needs parentheses.
Style/MethodDefParentheses:
Enabled: true

# Use `foo {}` not `foo{}`.
Style/SpaceBeforeBlockBraces:
Enabled: true

# Use `foo { bar }` not `foo {bar}`.
Style/SpaceInsideBlockBraces:
Enabled: true

# Use `{ a: 1 }` not `{a:1}`.
Style/SpaceInsideHashLiteralBraces:
Enabled: true

# Check quotes usage according to lint rule below.
Style/StringLiterals:
Enabled: true
EnforcedStyle: double_quotes

# Detect hard tabs, no hard tabs.
Style/Tab:
Enabled: true

# Blank lines should not have any spaces.
Style/TrailingBlankLines:
Enabled: true

# No trailing whitespace.
Style/TrailingWhitespace:
Enabled: true

# Use quotes for string literals when they are enough.
Style/UnneededPercentQ:
Enabled: true

# Align `end` with the matching keyword or starting expression except for
# assignments, where it should be aligned with the LHS.
Lint/EndAlignment:
Enabled: true
EnforcedStyleAlignWith: variable

# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
Lint/RequireParentheses:
Enabled: true

5 changes: 1 addition & 4 deletions .travis.yml
Expand Up @@ -2,7 +2,4 @@ language: ruby
env: LC_ALL="en_US.UTF-8" LANG="en_US.UTF-8"
install: bundle install --jobs=1 --retry=3
rvm:
- ruby
- rbx
- jruby

- 2.4.1
3 changes: 1 addition & 2 deletions Gemfile
@@ -1,3 +1,2 @@
source 'https://rubygems.org'
source "https://rubygems.org"
gemspec

166 changes: 60 additions & 106 deletions README.md
@@ -1,4 +1,4 @@
[![Lines of Code](http://img.shields.io/badge/lines_of_code-60-brightgreen.svg?style=flat)](http://blog.codinghorror.com/the-best-code-is-no-code-at-all/)
[![Lines of Code](http://img.shields.io/badge/lines_of_code-130-brightgreen.svg?style=flat)](http://blog.codinghorror.com/the-best-code-is-no-code-at-all/)
[![Code Status](http://img.shields.io/codeclimate/github/hopsoft/state_jacket.svg?style=flat)](https://codeclimate.com/github/hopsoft/state_jacket)
[![Dependency Status](http://img.shields.io/gemnasium/hopsoft/state_jacket.svg?style=flat)](https://gemnasium.com/hopsoft/state_jacket)
[![Build Status](http://img.shields.io/travis/hopsoft/state_jacket.svg?style=flat)](https://travis-ci.org/hopsoft/state_jacket)
Expand All @@ -7,132 +7,86 @@

# StateJacket

## An Intuitive [State Transition System](http://en.wikipedia.org/wiki/State_transition_system)
## An Intuitive [State Transition System](http://en.wikipedia.org/wiki/State_transition_system) & [State Machine](https://en.wikipedia.org/wiki/Finite-state_machine)

[State machines](http://en.wikipedia.org/wiki/Finite-state_machine) are awesome
but can be pretty daunting as a system grows.
Keeping states, transitions, & events straight can be tricky.
StateJacket simplifies things by isolating the management of states & transitions.
Events are left out, making it much easier to reason about what states exist
and how they transition to other states.
StateJacket provides an intuitive approach to building complex state machines
by isolating the concerns of the state transition system & state machine.

*The examples below are somewhat contrived, but should clearly illustrate usage.*

## The Basics

#### Install
## Install

```sh
gem install state_jacket
```

#### Define states & transitions for a simple [turnstyle](http://en.wikipedia.org/wiki/Finite-state_machine#Example:_a_turnstile).

![Turnstyle](https://raw.github.com/hopsoft/state_jacket/master/doc/turnstyle.png)

```ruby
require "state_jacket"

states = StateJacket::Catalog.new
states.add :open => [:closed, :error]
states.add :closed => [:open, :error]
states.add :error
states.lock

states.inspect # => {:open=>[:closed, :error], :closed=>[:open, :error], :error=>nil}
states.transitioners # => [:open, :closed]
states.terminators # => [:error]

states.can_transition? :open => :closed # => true
states.can_transition? :closed => :open # => true
states.can_transition? :error => :open # => false
states.can_transition? :error => :closed # => false
```

## Next Steps
## Example

Lets model something a bit more complex.
Let's define states & transitions (i.e. the state transition system) & a state machine for a [turnstyle](http://en.wikipedia.org/wiki/Finite-state_machine#Example:_a_turnstile).

#### Define states & transitions for a phone call.
![Turnstyle](https://raw.github.com/hopsoft/state_jacket/master/doc/turnstyle.png)

![Phone Call](https://raw.github.com/hopsoft/state_jacket/master/doc/phone-call.png)
### State Transition System

```ruby
require "state_jacket"

states = StateJacket::Catalog.new
states.add :idle => [:dialing]
states.add :dialing => [:idle, :connecting]
states.add :connecting => [:idle, :busy, :connected]
states.add :busy => [:idle]
states.add :connected => [:idle]
states.lock

states.transitioners # => [:idle, :dialing, :connecting, :busy, :connected]
states.terminators # => []

states.can_transition? :idle => :dialing # => true
states.can_transition? :dialing => [:idle, :connecting] # => true
states.can_transition? :connecting => [:idle, :busy, :connected] # => true
states.can_transition? :busy => :idle # => true
states.can_transition? :connected => :idle # => true
states.can_transition? :idle => [:dialing, :connected] # => false
system = StateJacket::StateTransitionSystem.new
system.add :opened => [:closed, :errored]
system.add :closed => [:opened, :errored]
system.lock # prevent further changes

system.to_h.inspect # => {"opened"=>["closed", "errored"], "closed"=>["opened", "errored"], "errored"=>nil}
system.transitioners # => ["opened", "closed"]
system.terminators # => ["errored"]

system.can_transition? :opened => :closed # => true
system.can_transition? :closed => :opened # => true
system.can_transition? :errored => :opened # => false
system.can_transition? :errored => :closed # => false
```

## Deep Cuts
### State Machine

Lets add state awareness and behavior to another class.
We'll reuse the turnstyle states from the example from above.
Define the events that trigger transitions defined by the state transition system (i.e. the state machine).

```ruby
require "state_jacket"

class Turnstyle
attr_reader :states, :current_state

def initialize
@states = StateJacket::Catalog.new
@states.add :open => [:closed, :error]
@states.add :closed => [:open, :error]
@states.add :error
@states.lock
@current_state = :closed
end
machine = StateJacket::StateMachine.new(system, state: "closed")
machine.on :open, :closed => :opened
machine.on :close, :opened => :closed
machine.lock # prevent further changes

machine.to_h.inspect # => {"open"=>[{"closed"=>"opened"}], "close"=>[{"opened"=>"closed"}]}
machine.events # => ["open", "close"]

machine.state # => "closed"
machine.is_event? :open # => true
machine.is_event? :close # => true
machine.is_event? :other # => false

machine.can_trigger? :open # => true
machine.can_trigger? :close # => false

machine.state # => "closed"
machine.trigger :open # => "opened"
machine.state # => "opened"

# you can also pass a block when triggering events
machine.trigger :close do |from_state, to_state|
# custom logic can be placed here
from_state # => "opened"
to_state # => "closed"
end

def open
if states.can_transition? current_state => :open
@current_state = :open
else
raise "Can't transition from #{@current_state} to :open"
end
end
machine.state # => "closed"

def close
if states.can_transition? current_state => :closed
@current_state = :closed
else
raise "Can't transition from #{@current_state} to :closed"
end
end
# this is a noop because can_trigger?(:close) is false
machine.trigger :close # => nil

machine.state # => "closed"

def break
@current_state = :error
begin
machine.trigger :open do |from_state, to_state|
raise # the transition isn't performed if an error occurs in the block
end
rescue
end

# example usage
turnstyle = Turnstyle.new
turnstyle.current_state # => :closed
turnstyle.open
turnstyle.current_state # => :open
turnstyle.close
turnstyle.current_state # => :closed
turnstyle.close # => RuntimeError: Can't transition from closed to :closed
turnstyle.open
turnstyle.current_state # => :open
turnstyle.open # => RuntimeError: Can't transition from open to :open
turnstyle.break
turnstyle.open # => RuntimeError: Can't transition from error to :open
turnstyle.close # => RuntimeError: Can't transition from error to :closed
machine.state # => "closed"
```

8 changes: 6 additions & 2 deletions Rakefile
@@ -1,9 +1,13 @@
require "bundler/gem_tasks"

task :default => [:test]
task default: [:test]

desc "Runs rubocop."
task :rubocop do
exec "bundle exec rubocop -c .rubocop.yml"
end

desc "Runs the test suite."
task :test do
exec "bundle exec pry-test --disable-pry"
end

7 changes: 5 additions & 2 deletions lib/state_jacket.rb
@@ -1,3 +1,6 @@
require "state_jacket/version"
require "state_jacket/catalog"
require_relative "./state_jacket/version"
require_relative "./state_jacket/state_transition_system"
require_relative "./state_jacket/state_machine"

module StateJacket
end

0 comments on commit e00a097

Please sign in to comment.