state_machines 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +0 -2
- data/README.md +25 -0
- data/Rakefile +10 -1
- data/lib/state_machines/branch.rb +0 -4
- data/lib/state_machines/core.rb +23 -5
- data/lib/state_machines/error.rb +81 -2
- data/lib/state_machines/event.rb +2 -20
- data/lib/state_machines/event_collection.rb +25 -27
- data/lib/state_machines/extensions.rb +34 -34
- data/lib/state_machines/integrations.rb +98 -90
- data/lib/state_machines/integrations/base.rb +11 -60
- data/lib/state_machines/matcher.rb +0 -2
- data/lib/state_machines/node_collection.rb +0 -2
- data/lib/state_machines/path_collection.rb +0 -2
- data/lib/state_machines/state.rb +0 -3
- data/lib/state_machines/state_collection.rb +17 -19
- data/lib/state_machines/state_context.rb +1 -6
- data/lib/state_machines/transition.rb +0 -56
- data/lib/state_machines/version.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/state_machines/assertions_spec.rb +31 -0
- data/spec/state_machines/branch_spec.rb +827 -0
- data/spec/state_machines/callbacks_spec.rb +706 -0
- data/spec/state_machines/errors_spec.rb +1 -0
- data/spec/state_machines/event_collection_spec.rb +401 -0
- data/spec/state_machines/event_spec.rb +1140 -0
- data/spec/{helpers → state_machines}/helper_spec.rb +0 -0
- data/spec/state_machines/integration_base_spec.rb +12 -0
- data/spec/state_machines/integration_spec.rb +132 -0
- data/spec/state_machines/invalid_event_spec.rb +19 -0
- data/spec/state_machines/invalid_parallel_transition_spec.rb +18 -0
- data/spec/state_machines/invalid_transition_spec.rb +114 -0
- data/spec/state_machines/machine_collection_spec.rb +606 -0
- data/spec/{machine_spec.rb → state_machines/machine_spec.rb} +11 -2
- data/spec/{matcher_helpers_spec.rb → state_machines/matcher_helpers_spec.rb} +0 -0
- data/spec/{matcher_spec.rb → state_machines/matcher_spec.rb} +0 -0
- data/spec/{node_collection_spec.rb → state_machines/node_collection_spec.rb} +0 -0
- data/spec/{path_collection_spec.rb → state_machines/path_collection_spec.rb} +0 -0
- data/spec/{path_spec.rb → state_machines/path_spec.rb} +0 -0
- data/spec/{state_collection_spec.rb → state_machines/state_collection_spec.rb} +0 -0
- data/spec/{state_context_spec.rb → state_machines/state_context_spec.rb} +0 -0
- data/spec/{state_machine_spec.rb → state_machines/state_machine_spec.rb} +0 -0
- data/spec/{state_spec.rb → state_machines/state_spec.rb} +0 -0
- data/spec/{transition_collection_spec.rb → state_machines/transition_collection_spec.rb} +0 -0
- data/spec/{transition_spec.rb → state_machines/transition_spec.rb} +0 -0
- data/spec/support/migration_helpers.rb +9 -0
- data/state_machines.gemspec +3 -1
- metadata +68 -45
- data/lib/state_machines/yard.rb +0 -8
- data/spec/errors/default_spec.rb +0 -14
- data/spec/errors/with_message_spec.rb +0 -39
@@ -1,11 +1,4 @@
|
|
1
1
|
module StateMachines
|
2
|
-
# An invalid integration was specified
|
3
|
-
class IntegrationNotFound < Error
|
4
|
-
def initialize(name)
|
5
|
-
super(nil, "#{name.inspect} is an invalid integration")
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
2
|
# Integrations allow state machines to take advantage of features within the
|
10
3
|
# context of a particular library. This is currently most useful with
|
11
4
|
# database libraries. For example, the various database integrations allow
|
@@ -25,89 +18,104 @@ module StateMachines
|
|
25
18
|
# built-in integrations for more information about how to define additional
|
26
19
|
# integrations.
|
27
20
|
module Integrations
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
21
|
+
@integrations = Set.new
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# Register integration
|
25
|
+
def register(arg)
|
26
|
+
#TODO check name conflict
|
27
|
+
case arg.class.to_s
|
28
|
+
when 'Module'
|
29
|
+
add(arg)
|
30
|
+
else
|
31
|
+
fail IntegrationError
|
32
|
+
end
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
# Gets a list of all of the available integrations for use.
|
38
|
+
#
|
39
|
+
# == Example
|
40
|
+
#
|
41
|
+
# StateMachines::Integrations.integrations
|
42
|
+
# # => []
|
43
|
+
# StateMachines::Integrations.register(StateMachines::Integrations::ActiveModel)
|
44
|
+
# StateMachines::Integrations.integrations
|
45
|
+
# # => [StateMachines::Integrations::ActiveModel]
|
46
|
+
def integrations
|
47
|
+
# Register all namespaced integrations
|
48
|
+
name_spaced_integrations
|
49
|
+
@integrations
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# Attempts to find an integration that matches the given class. This will
|
54
|
+
# look through all of the built-in integrations under the StateMachines::Integrations
|
55
|
+
# namespace and find one that successfully matches the class.
|
56
|
+
#
|
57
|
+
# == Examples
|
58
|
+
#
|
59
|
+
# class Vehicle
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# class ActiveModelVehicle
|
63
|
+
# include ActiveModel::Observing
|
64
|
+
# include ActiveModel::Validations
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# class ActiveRecordVehicle < ActiveRecord::Base
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# StateMachines::Integrations.match(Vehicle) # => nil
|
71
|
+
# StateMachines::Integrations.match(ActiveModelVehicle) # => StateMachines::Integrations::ActiveModel
|
72
|
+
# StateMachines::Integrations.match(ActiveRecordVehicle) # => StateMachines::Integrations::ActiveRecord
|
73
|
+
def match(klass)
|
74
|
+
integrations.detect { |integration| integration.matches?(klass) }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Attempts to find an integration that matches the given list of ancestors.
|
78
|
+
# This will look through all of the built-in integrations under the StateMachines::Integrations
|
79
|
+
# namespace and find one that successfully matches one of the ancestors.
|
80
|
+
#
|
81
|
+
# == Examples
|
82
|
+
#
|
83
|
+
# StateMachines::Integrations.match_ancestors([]) # => nil
|
84
|
+
# StateMachines::Integrations.match_ancestors(['ActiveRecord::Base']) # => StateMachines::Integrations::ActiveModel
|
85
|
+
def match_ancestors(ancestors)
|
86
|
+
integrations.detect { |integration| integration.matches_ancestors?(ancestors) }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Finds an integration with the given name. If the integration cannot be
|
90
|
+
# found, then a NameError exception will be raised.
|
91
|
+
#
|
92
|
+
# == Examples
|
93
|
+
#
|
94
|
+
# StateMachines::Integrations.find_by_name(:active_model) # => StateMachines::Integrations::ActiveModel
|
95
|
+
# StateMachines::Integrations.find_by_name(:active_record) # => StateMachines::Integrations::ActiveRecord
|
96
|
+
# StateMachines::Integrations.find_by_name(:invalid) # => StateMachines::IntegrationNotFound: :invalid is an invalid integration
|
97
|
+
def find_by_name(name)
|
98
|
+
integrations.detect { |integration| integration.integration_name == name } || raise(IntegrationNotFound.new(name))
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def name_spaced_integrations
|
105
|
+
self.constants.each do |const|
|
106
|
+
integration = self.const_get(const)
|
107
|
+
add(integration) if integration.respond_to?(:integration_name)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def add(integration)
|
112
|
+
@integrations << integration
|
113
|
+
end
|
114
|
+
|
115
|
+
def reset
|
116
|
+
@integrations = Set.new
|
117
|
+
name_spaced_integrations
|
118
|
+
end
|
111
119
|
end
|
112
120
|
end
|
113
121
|
end
|
@@ -5,93 +5,44 @@ module StateMachines
|
|
5
5
|
module ClassMethods
|
6
6
|
# The default options to use for state machines using this integration
|
7
7
|
attr_reader :defaults
|
8
|
-
|
8
|
+
|
9
9
|
# The name of the integration
|
10
10
|
def integration_name
|
11
11
|
@integration_name ||= begin
|
12
12
|
name = self.name.split('::').last
|
13
|
-
name.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
14
|
-
name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
13
|
+
name.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
14
|
+
name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
15
15
|
name.downcase!
|
16
16
|
name.to_sym
|
17
17
|
end
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
# Whether this integration is available for the current library. This
|
21
21
|
# is only true if the ORM that the integration is for is currently
|
22
22
|
# defined.
|
23
23
|
def available?
|
24
24
|
matching_ancestors.any? && Object.const_defined?(matching_ancestors[0].split('::')[0])
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
# The list of ancestor names that cause this integration to matched.
|
28
28
|
def matching_ancestors
|
29
29
|
[]
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
# Whether the integration should be used for the given class.
|
33
33
|
def matches?(klass)
|
34
|
-
matches_ancestors?(klass.ancestors.map {|ancestor| ancestor.name})
|
34
|
+
matches_ancestors?(klass.ancestors.map { |ancestor| ancestor.name })
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
37
|
# Whether the integration should be used for the given list of ancestors.
|
38
38
|
def matches_ancestors?(ancestors)
|
39
39
|
(ancestors & matching_ancestors).any?
|
40
40
|
end
|
41
|
-
|
42
|
-
# Tracks the various version overrides for an integration
|
43
|
-
def versions
|
44
|
-
@versions ||= []
|
45
|
-
end
|
46
|
-
|
47
|
-
# Creates a new version override for an integration. When this
|
48
|
-
# integration is activated, each version that is marked as active will
|
49
|
-
# also extend the integration.
|
50
|
-
#
|
51
|
-
# == Example
|
52
|
-
#
|
53
|
-
# module StateMachines
|
54
|
-
# module Integrations
|
55
|
-
# module ORMLibrary
|
56
|
-
# version '0.2.x - 0.3.x' do
|
57
|
-
# def self.active?
|
58
|
-
# ::ORMLibrary::VERSION >= '0.2.0' && ::ORMLibrary::VERSION < '0.4.0'
|
59
|
-
# end
|
60
|
-
#
|
61
|
-
# def invalidate(object, attribute, message, values = [])
|
62
|
-
# # Override here...
|
63
|
-
# end
|
64
|
-
# end
|
65
|
-
# end
|
66
|
-
# end
|
67
|
-
# end
|
68
|
-
#
|
69
|
-
# In the above example, a version override is defined for the ORMLibrary
|
70
|
-
# integration when the version is between 0.2.x and 0.3.x.
|
71
|
-
def version(name, &block)
|
72
|
-
versions << mod = Module.new(&block)
|
73
|
-
mod
|
74
|
-
end
|
75
|
-
|
76
|
-
# The path to the locale file containing translations for this
|
77
|
-
# integration. This file will only exist for integrations that actually
|
78
|
-
# support i18n.
|
79
|
-
def locale_path
|
80
|
-
path = "#{File.dirname(__FILE__)}/#{integration_name}/locale.rb"
|
81
|
-
path if File.exist?(path)
|
82
|
-
end
|
83
|
-
|
84
|
-
# Extends the given object with any version overrides that are currently
|
85
|
-
# active
|
86
|
-
def extended(base)
|
87
|
-
versions.each do |version|
|
88
|
-
base.extend(version) if version.active?
|
89
|
-
end
|
90
|
-
end
|
41
|
+
|
91
42
|
end
|
92
|
-
|
43
|
+
|
93
44
|
extend ClassMethods
|
94
|
-
|
45
|
+
|
95
46
|
def self.included(base) #:nodoc:
|
96
47
|
base.class_eval { extend ClassMethods }
|
97
48
|
end
|
data/lib/state_machines/state.rb
CHANGED
@@ -1,12 +1,10 @@
|
|
1
|
-
require 'state_machines/node_collection'
|
2
|
-
|
3
1
|
module StateMachines
|
4
2
|
# Represents a collection of states in a state machine
|
5
3
|
class StateCollection < NodeCollection
|
6
4
|
def initialize(machine) #:nodoc:
|
7
5
|
super(machine, :index => [:name, :qualified_name, :value])
|
8
6
|
end
|
9
|
-
|
7
|
+
|
10
8
|
# Determines whether the given object is in a specific state. If the
|
11
9
|
# object's current value doesn't match the state, then this will return
|
12
10
|
# false, otherwise true. If the given state is unknown, then an IndexError
|
@@ -29,7 +27,7 @@ module StateMachines
|
|
29
27
|
def matches?(object, name)
|
30
28
|
fetch(name).matches?(machine.read(object, :state))
|
31
29
|
end
|
32
|
-
|
30
|
+
|
33
31
|
# Determines the current state of the given object as configured by this
|
34
32
|
# state machine. This will attempt to find a known state that matches
|
35
33
|
# the value of the attribute on the object.
|
@@ -54,9 +52,9 @@ module StateMachines
|
|
54
52
|
# states.match(vehicle) # => nil
|
55
53
|
def match(object)
|
56
54
|
value = machine.read(object, :state)
|
57
|
-
self[value, :value] || detect {|state| state.matches?(value)}
|
55
|
+
self[value, :value] || detect { |state| state.matches?(value) }
|
58
56
|
end
|
59
|
-
|
57
|
+
|
60
58
|
# Determines the current state of the given object as configured by this
|
61
59
|
# state machine. If no state is found, then an ArgumentError will be
|
62
60
|
# raised.
|
@@ -79,7 +77,7 @@ module StateMachines
|
|
79
77
|
def match!(object)
|
80
78
|
match(object) || raise(ArgumentError, "#{machine.read(object, :state).inspect} is not a known #{machine.name} value")
|
81
79
|
end
|
82
|
-
|
80
|
+
|
83
81
|
# Gets the order in which states should be displayed based on where they
|
84
82
|
# were first referenced. This will order states in the following priority:
|
85
83
|
#
|
@@ -91,22 +89,22 @@ module StateMachines
|
|
91
89
|
#
|
92
90
|
# This order will determine how the GraphViz visualizations are rendered.
|
93
91
|
def by_priority
|
94
|
-
order = select {|state| state.initial}.map {|state| state.name}
|
95
|
-
|
96
|
-
machine.events.each {|event| order += event.known_states}
|
97
|
-
order += select {|state| state.context_methods.any?}.map {|state| state.name}
|
98
|
-
order += keys(:name) - machine.callbacks.values.flatten.map {|callback| callback.known_states}.flatten
|
92
|
+
order = select { |state| state.initial }.map { |state| state.name }
|
93
|
+
|
94
|
+
machine.events.each { |event| order += event.known_states }
|
95
|
+
order += select { |state| state.context_methods.any? }.map { |state| state.name }
|
96
|
+
order += keys(:name) - machine.callbacks.values.flatten.map { |callback| callback.known_states }.flatten
|
99
97
|
order += keys(:name)
|
100
|
-
|
98
|
+
|
101
99
|
order.uniq!
|
102
|
-
order.map! {|name| self[name]}
|
100
|
+
order.map! { |name| self[name] }
|
103
101
|
order
|
104
102
|
end
|
105
|
-
|
103
|
+
|
106
104
|
private
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
105
|
+
# Gets the value for the given attribute on the node
|
106
|
+
def value(node, attribute)
|
107
|
+
attribute == :value ? node.value(false) : super
|
108
|
+
end
|
111
109
|
end
|
112
110
|
end
|
@@ -1,10 +1,5 @@
|
|
1
|
-
require 'state_machines/assertions'
|
2
|
-
require 'state_machines/eval_helpers'
|
3
|
-
|
4
1
|
module StateMachines
|
5
|
-
|
6
|
-
class InvalidContext < Error
|
7
|
-
end
|
2
|
+
|
8
3
|
|
9
4
|
# Represents a module which will get evaluated within the context of a state.
|
10
5
|
#
|
@@ -1,60 +1,4 @@
|
|
1
|
-
require 'state_machines/transition_collection'
|
2
|
-
require 'state_machines/error'
|
3
|
-
|
4
1
|
module StateMachines
|
5
|
-
# An invalid transition was attempted
|
6
|
-
class InvalidTransition < Error
|
7
|
-
# The machine attempting to be transitioned
|
8
|
-
attr_reader :machine
|
9
|
-
|
10
|
-
# The current state value for the machine
|
11
|
-
attr_reader :from
|
12
|
-
|
13
|
-
def initialize(object, machine, event) #:nodoc:
|
14
|
-
@machine = machine
|
15
|
-
@from_state = machine.states.match!(object)
|
16
|
-
@from = machine.read(object, :state)
|
17
|
-
@event = machine.events.fetch(event)
|
18
|
-
errors = machine.errors_for(object)
|
19
|
-
|
20
|
-
message = "Cannot transition #{machine.name} via :#{self.event} from #{from_name.inspect}"
|
21
|
-
message << " (Reason(s): #{errors})" unless errors.empty?
|
22
|
-
super(object, message)
|
23
|
-
end
|
24
|
-
|
25
|
-
# The event that triggered the failed transition
|
26
|
-
def event
|
27
|
-
@event.name
|
28
|
-
end
|
29
|
-
|
30
|
-
# The fully-qualified name of the event that triggered the failed transition
|
31
|
-
def qualified_event
|
32
|
-
@event.qualified_name
|
33
|
-
end
|
34
|
-
|
35
|
-
# The name for the current state
|
36
|
-
def from_name
|
37
|
-
@from_state.name
|
38
|
-
end
|
39
|
-
|
40
|
-
# The fully-qualified name for the current state
|
41
|
-
def qualified_from_name
|
42
|
-
@from_state.qualified_name
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
# A set of transition failed to run in parallel
|
47
|
-
class InvalidParallelTransition < Error
|
48
|
-
# The set of events that failed the transition(s)
|
49
|
-
attr_reader :events
|
50
|
-
|
51
|
-
def initialize(object, events) #:nodoc:
|
52
|
-
@events = events
|
53
|
-
|
54
|
-
super(object, "Cannot run events in parallel: #{events * ', '}")
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
2
|
# A transition represents a state change for a specific attribute.
|
59
3
|
#
|
60
4
|
# Transitions consist of:
|