ardm-is-state_machine 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +35 -0
  3. data/.travis.yml +11 -0
  4. data/Gemfile +65 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +102 -0
  7. data/Rakefile +4 -0
  8. data/ardm-is-state_machine.gemspec +24 -0
  9. data/lib/ardm-is-state_machine.rb +1 -0
  10. data/lib/dm-is-state_machine.rb +10 -0
  11. data/lib/dm-is-state_machine/is/data/event.rb +25 -0
  12. data/lib/dm-is-state_machine/is/data/machine.rb +90 -0
  13. data/lib/dm-is-state_machine/is/data/state.rb +21 -0
  14. data/lib/dm-is-state_machine/is/dsl/event_dsl.rb +81 -0
  15. data/lib/dm-is-state_machine/is/dsl/state_dsl.rb +40 -0
  16. data/lib/dm-is-state_machine/is/state_machine.rb +139 -0
  17. data/lib/dm-is-state_machine/is/version.rb +7 -0
  18. data/spec/examples/invalid_events.rb +20 -0
  19. data/spec/examples/invalid_states.rb +20 -0
  20. data/spec/examples/invalid_transitions_1.rb +22 -0
  21. data/spec/examples/invalid_transitions_2.rb +22 -0
  22. data/spec/examples/light_switch.rb +25 -0
  23. data/spec/examples/slot_machine.rb +48 -0
  24. data/spec/examples/traffic_light.rb +48 -0
  25. data/spec/integration/inheritance_spec.rb +11 -0
  26. data/spec/integration/invalid_events_spec.rb +11 -0
  27. data/spec/integration/invalid_states_spec.rb +11 -0
  28. data/spec/integration/invalid_transitions_spec.rb +21 -0
  29. data/spec/integration/slot_machine_spec.rb +86 -0
  30. data/spec/integration/traffic_light_spec.rb +181 -0
  31. data/spec/rcov.opts +6 -0
  32. data/spec/spec.opts +4 -0
  33. data/spec/spec_helper.rb +11 -0
  34. data/spec/unit/data/event_spec.rb +27 -0
  35. data/spec/unit/data/machine_spec.rb +102 -0
  36. data/spec/unit/data/state_spec.rb +21 -0
  37. data/spec/unit/dsl/event_dsl_spec.rb +55 -0
  38. data/spec/unit/dsl/state_dsl_spec.rb +24 -0
  39. data/spec/unit/state_machine_spec.rb +28 -0
  40. data/tasks/spec.rake +38 -0
  41. data/tasks/yard.rake +9 -0
  42. data/tasks/yardstick.rake +19 -0
  43. metadata +132 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 33f98553240c3dda7d71d6c6778ced2eef9aea70
4
+ data.tar.gz: 47c1378362e0d2c2e4cc6250b9fb20903627a493
5
+ SHA512:
6
+ metadata.gz: 348a08cd707301ae0978cad8f57eb347de5d44b14943ebf09ccb10feffbee0388d831487499c8e73128f0b959afcce18b9b8535d25c04f6eda752e7c5eb84621
7
+ data.tar.gz: a7fd6133e0d63ae775cae9e574ef465578769a05ba0be7453933614133f3a14d80f15677daf61cadbf34a65e20808eee2b5e9abc4be6c14d2c4340d1b635a0a6
@@ -0,0 +1,35 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## Rubinius
17
+ *.rbc
18
+
19
+ ## PROJECT::GENERAL
20
+ *.gem
21
+ coverage
22
+ rdoc
23
+ pkg
24
+ tmp
25
+ doc
26
+ log
27
+ .yardoc
28
+ measurements
29
+
30
+ ## BUNDLER
31
+ .bundle
32
+ Gemfile.*
33
+
34
+ ## PROJECT::SPECIFIC
35
+ spec/db/
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ sudo: false
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.5
7
+ - 2.2.0
8
+ matrix:
9
+ allow_failures:
10
+ - rvm: 2.1.5
11
+ - rvm: 2.2.0
data/Gemfile ADDED
@@ -0,0 +1,65 @@
1
+ require 'pathname'
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ SOURCE = ENV.fetch('SOURCE', :git).to_sym
8
+ REPO_POSTFIX = SOURCE == :path ? '' : '.git'
9
+ DATAMAPPER = SOURCE == :path ? Pathname(__FILE__).dirname.parent : 'http://github.com/ar-dm'
10
+ DM_VERSION = '~> 1.2.0'
11
+ DO_VERSION = '~> 0.10.6'
12
+ DM_DO_ADAPTERS = %w[ sqlite postgres mysql oracle sqlserver ]
13
+ CURRENT_BRANCH = ENV.fetch('GIT_BRANCH', 'master')
14
+
15
+ gem 'ardm-core', DM_VERSION,
16
+ SOURCE => "#{DATAMAPPER}/ardm-core#{REPO_POSTFIX}",
17
+ :branch => CURRENT_BRANCH
18
+
19
+ platforms :mri_18 do
20
+ group :quality do
21
+
22
+ gem 'rcov', '~> 0.9.10'
23
+ gem 'yard', '~> 0.7.2'
24
+ gem 'yardstick', '~> 0.4'
25
+
26
+ end
27
+ end
28
+
29
+ group :datamapper do
30
+
31
+ adapters = ENV['ADAPTER'] || ENV['ADAPTERS']
32
+ adapters = adapters.to_s.tr(',', ' ').split.uniq - %w[ in_memory ]
33
+
34
+ if (do_adapters = DM_DO_ADAPTERS & adapters).any?
35
+ do_options = {}
36
+ do_options[:git] = "#{DATAMAPPER}/do#{REPO_POSTFIX}" if ENV['DO_GIT'] == 'true'
37
+
38
+ gem 'data_objects', DO_VERSION, do_options.dup
39
+
40
+ do_adapters.each do |adapter|
41
+ adapter = 'sqlite3' if adapter == 'sqlite'
42
+ gem "do_#{adapter}", DO_VERSION, do_options.dup
43
+ end
44
+
45
+ gem 'ardm-do-adapter', DM_VERSION,
46
+ SOURCE => "#{DATAMAPPER}/ardm-do-adapter#{REPO_POSTFIX}",
47
+ :branch => CURRENT_BRANCH
48
+ end
49
+
50
+ adapters.each do |adapter|
51
+ gem "ardm-#{adapter}-adapter", DM_VERSION,
52
+ SOURCE => "#{DATAMAPPER}/ardm-#{adapter}-adapter#{REPO_POSTFIX}",
53
+ :branch => CURRENT_BRANCH
54
+ end
55
+
56
+ plugins = ENV['PLUGINS'] || ENV['PLUGIN']
57
+ plugins = plugins.to_s.tr(',', ' ').split.push('ardm-migrations').uniq
58
+
59
+ plugins.each do |plugin|
60
+ gem plugin, DM_VERSION,
61
+ SOURCE => "#{DATAMAPPER}/#{plugin}#{REPO_POSTFIX}",
62
+ :branch => CURRENT_BRANCH
63
+ end
64
+
65
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 David James
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,102 @@
1
+ = dm-is-state_machine
2
+
3
+ DataMapper plugin that adds state machine functionality to your models.
4
+
5
+ == Why is this plugin useful?
6
+
7
+ Your DataMapper resource might benefit from a state machine if it:
8
+
9
+ * has different "modes" of operation
10
+ * has discrete behaviors
11
+ * especially if the behaviors are mutually exclusive
12
+
13
+ And you want a clean, high-level way of describing these modes / behaviors
14
+ and how the resource moves between them. This plugin allows you to
15
+ declaratively describe the states and transitions involved.
16
+
17
+ == Installation
18
+
19
+ 1. Download dm-more.
20
+ 2. Install dm-is-state_machine using the supplied rake files.
21
+
22
+ == Setting up with Merb ##
23
+
24
+ Add this line to your init.rb:
25
+
26
+ dependency "dm-is-state_machine"
27
+
28
+ ## Example DataMapper resource (i.e. model) ##
29
+
30
+ # /app/models/traffic_light.rb
31
+ class TrafficLight
32
+ include DataMapper::Resource
33
+
34
+ property :id, Serial
35
+
36
+ is :state_machine, :initial => :green, :column => :color do
37
+ state :green
38
+ state :yellow
39
+ state :red, :enter => :red_hook
40
+ state :broken
41
+
42
+ event :forward do
43
+ transition :from => :green, :to => :yellow
44
+ transition :from => :yellow, :to => :red
45
+ transition :from => :red, :to => :green
46
+ end
47
+ end
48
+
49
+ def red_hook
50
+ # Do something
51
+ end
52
+ end
53
+
54
+ == What this gives you
55
+
56
+ === Explained in words
57
+
58
+ The above DSL (domain specific language) does these things "behind the scenes":
59
+
60
+ 1. Defines a DataMapper property called 'color'.
61
+
62
+ 2. Makes the current state available by using 'traffic_light.color'.
63
+
64
+ 3. Defines the 'forward!' transition method. This method triggers the
65
+ appropriate transition based on the current state and comparing it against
66
+ the various :from states. It will raise an error if you attempt to call
67
+ it with an invalid state (such as :broken, see above). After the method
68
+ runs successfully, the state machine will be left in the :to state.
69
+
70
+ === Explained with some code examples
71
+
72
+ # Somewhere in your controller, perhaps
73
+ light = TrafficLight.new
74
+
75
+ # Move to the next state
76
+ light.forward!
77
+
78
+ # Do something based on the current state
79
+ case light.color
80
+ when "green"
81
+ # do something green-related
82
+ when "yellow"
83
+ # do something yellow-related
84
+ when "red"
85
+ # do something red-related
86
+ end
87
+
88
+ == Specific examples
89
+
90
+ We would also like to hear how *you* are using state machines in your code.
91
+
92
+ == See also
93
+
94
+ Here are some other projects you might want to look at. Most of them
95
+ are probably intended for ActiveRecord. They take different approaches,
96
+ which is pretty interesting. If you find something you like in these other
97
+ projects, let us know. Maybe we can incorporate some of your favorite parts.
98
+ That said, I do not want to create a Frankenstein. :)
99
+
100
+ * http://github.com/pluginaweek/state_machine/tree/master
101
+ * http://github.com/davidlee/stateful/tree/master
102
+ * http://github.com/sbfaulkner/has_states/tree/master
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ FileList['tasks/**/*.rake'].each { |task| import task }
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/dm-is-state_machine/is/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'ardm-is-state_machine'
6
+ gem.version = DataMapper::Is::StateMachine::VERSION
7
+
8
+ gem.authors = [ 'Martin Emde', 'David James' ]
9
+ gem.email = [ 'me@martinemde.com', 'djwonk [a] collectiveinsight [d] net' ]
10
+ gem.summary = 'Ardm fork of dm-is-state_machine'
11
+ gem.description = 'DataMapper plugin for creating state machines'
12
+ gem.homepage = "https://github.com/ar-dm/ardm-is-state_machine"
13
+ gem.license = 'MIT'
14
+
15
+ gem.files = `git ls-files`.split("\n")
16
+ gem.test_files = `git ls-files -- {spec}/*`.split("\n")
17
+ gem.extra_rdoc_files = %w[LICENSE README.rdoc]
18
+ gem.require_paths = [ "lib" ]
19
+
20
+ gem.add_runtime_dependency 'ardm-core', '~> 1.2'
21
+
22
+ gem.add_development_dependency 'rake', '~> 0.9'
23
+ gem.add_development_dependency 'rspec', '~> 1.3'
24
+ end
@@ -0,0 +1 @@
1
+ require 'dm-is-state_machine'
@@ -0,0 +1,10 @@
1
+ require 'dm-core'
2
+
3
+ require 'dm-is-state_machine/is/state_machine'
4
+ require 'dm-is-state_machine/is/data/event'
5
+ require 'dm-is-state_machine/is/data/machine'
6
+ require 'dm-is-state_machine/is/data/state'
7
+ require 'dm-is-state_machine/is/dsl/event_dsl'
8
+ require 'dm-is-state_machine/is/dsl/state_dsl'
9
+
10
+ DataMapper::Model.append_extensions DataMapper::Is::StateMachine
@@ -0,0 +1,25 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+ module Data
5
+
6
+ class Event
7
+
8
+ attr_reader :name, :machine, :transitions
9
+
10
+ def initialize(name, machine)
11
+ @name = name
12
+ @machine = machine
13
+ @transitions = []
14
+ end
15
+
16
+ def add_transition(from, to)
17
+ @transitions << { :from => from, :to => to }
18
+ end
19
+
20
+ end
21
+
22
+ end # Data
23
+ end # StateMachine
24
+ end # Is
25
+ end # DataMapper
@@ -0,0 +1,90 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+ module Data
5
+
6
+ # This Machine class represents one state machine.
7
+ #
8
+ # A model (i.e. a DataMapper resource) can have more than one Machine.
9
+ class Machine
10
+
11
+ # The property of the DM resource that will hold this Machine's
12
+ # state.
13
+ #
14
+ # TODO: change :column to :property
15
+ attr_accessor :column
16
+
17
+ # The initial value of this Machine's state
18
+ attr_accessor :initial
19
+
20
+ # The current value of this Machine's state
21
+ #
22
+ # This is the "primary control" of this Machine's state. All
23
+ # other methods key off the value of @current_state_name.
24
+ attr_accessor :current_state_name
25
+
26
+ attr_accessor :events
27
+
28
+ attr_accessor :states
29
+
30
+ def initialize(column, initial)
31
+ @column, @initial = column, initial
32
+ @events, @states = [], []
33
+ @current_state_name = initial
34
+ end
35
+
36
+ # Fire (activate) the event with name +event_name+
37
+ #
38
+ # @api public
39
+ def fire_event(event_name, resource)
40
+ unless event = find_event(event_name)
41
+ raise InvalidEvent, "Could not find event (#{event_name.inspect})"
42
+ end
43
+ transition = event.transitions.find do |t|
44
+ t[:from].to_s == @current_state_name.to_s
45
+ end
46
+ unless transition
47
+ raise InvalidEvent, "Event (#{event_name.inspect}) does not" +
48
+ "exist for current state (#{@current_state_name.inspect})"
49
+ end
50
+
51
+ # == Run :exit hook (if present) ==
52
+ resource.run_hook_if_present current_state.options[:exit]
53
+
54
+ # == Change the current_state ==
55
+ @current_state_name = transition[:to]
56
+
57
+ # == Run :enter hook (if present) ==
58
+ resource.run_hook_if_present current_state.options[:enter]
59
+ end
60
+
61
+ # Return the current state
62
+ #
63
+ # @api public
64
+ def current_state
65
+ find_state(@current_state_name)
66
+ # TODO: add caching, i.e. with `@current_state ||= ...`
67
+ end
68
+
69
+ # Find event whose name is +event_name+
70
+ #
71
+ # @api semipublic
72
+ def find_event(event_name)
73
+ @events.find { |event| event.name.to_s == event_name.to_s }
74
+ # TODO: use a data structure that prevents duplicates
75
+ end
76
+
77
+ # Find state whose name is +event_name+
78
+ #
79
+ # @api semipublic
80
+ def find_state(state_name)
81
+ @states.find { |state| state.name.to_s == state_name.to_s }
82
+ # TODO: use a data structure that prevents duplicates
83
+ end
84
+
85
+ end
86
+
87
+ end # Data
88
+ end # StateMachine
89
+ end # Is
90
+ end # DataMapper
@@ -0,0 +1,21 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+ module Data
5
+
6
+ class State
7
+
8
+ attr_reader :name, :machine, :options
9
+
10
+ def initialize(name, machine, options = {})
11
+ @name = name
12
+ @options = options
13
+ @machine = machine
14
+ end
15
+
16
+ end
17
+
18
+ end # Data
19
+ end # StateMachine
20
+ end # Is
21
+ end # DataMapper
@@ -0,0 +1,81 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+ # Event DSL (Domain Specific Language)
5
+ module EventDsl
6
+
7
+ # Define an event. This takes a block which describes all valid
8
+ # transitions for this event.
9
+ #
10
+ # Example:
11
+ #
12
+ # class TrafficLight
13
+ # include DataMapper::Resource
14
+ # property :id, Serial
15
+ # is :state_machine, :initial => :green, :column => :color do
16
+ # # state definitions go here...
17
+ #
18
+ # event :forward do
19
+ # transition :from => :green, :to => :yellow
20
+ # transition :from => :yellow, :to => :red
21
+ # transition :from => :red, :to => :green
22
+ # end
23
+ # end
24
+ # end
25
+ #
26
+ # +transition+ takes a hash where <tt>:to</tt> is the state to transition
27
+ # to and <tt>:from</tt> is a state (or Array of states) from which this
28
+ # event can be fired.
29
+ def event(name, &block)
30
+ unless state_machine_context?(:is)
31
+ raise InvalidContext, "Valid only in 'is :state_machine' block"
32
+ end
33
+
34
+ # ===== Setup context =====
35
+ machine = @is_state_machine[:machine]
36
+ event = Data::Event.new(name, machine)
37
+ machine.events << event
38
+ @is_state_machine[:event] = {
39
+ :name => name,
40
+ :object => event
41
+ }
42
+ push_state_machine_context(:event)
43
+
44
+ # ===== Define methods =====
45
+ define_method("#{name}!") do
46
+ transition!(name)
47
+ end
48
+
49
+ # Possible alternative to the above:
50
+ # (class_eval is typically faster than define_method)
51
+ #
52
+ # self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
53
+ # def #{name}!
54
+ # machine.current_state_name = __send__(:"#{column}")
55
+ # machine.fire_event(name, self)
56
+ # __send__(:"#{column}="), machine.current_state_name
57
+ # end
58
+ # RUBY
59
+
60
+ yield if block_given?
61
+
62
+ # ===== Teardown context =====
63
+ pop_state_machine_context
64
+ end
65
+
66
+ def transition(options)
67
+ unless state_machine_context?(:event)
68
+ raise InvalidContext, "Valid only in 'event' block"
69
+ end
70
+ event_name = @is_state_machine[:event][:name]
71
+ event_object = @is_state_machine[:event][:object]
72
+
73
+ from = options[:from]
74
+ to = options[:to]
75
+ event_object.add_transition(from, to)
76
+ end
77
+
78
+ end # EventDsl
79
+ end # StateMachine
80
+ end # Is
81
+ end # DataMapper