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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -2
  3. data/README.md +25 -0
  4. data/Rakefile +10 -1
  5. data/lib/state_machines/branch.rb +0 -4
  6. data/lib/state_machines/core.rb +23 -5
  7. data/lib/state_machines/error.rb +81 -2
  8. data/lib/state_machines/event.rb +2 -20
  9. data/lib/state_machines/event_collection.rb +25 -27
  10. data/lib/state_machines/extensions.rb +34 -34
  11. data/lib/state_machines/integrations.rb +98 -90
  12. data/lib/state_machines/integrations/base.rb +11 -60
  13. data/lib/state_machines/matcher.rb +0 -2
  14. data/lib/state_machines/node_collection.rb +0 -2
  15. data/lib/state_machines/path_collection.rb +0 -2
  16. data/lib/state_machines/state.rb +0 -3
  17. data/lib/state_machines/state_collection.rb +17 -19
  18. data/lib/state_machines/state_context.rb +1 -6
  19. data/lib/state_machines/transition.rb +0 -56
  20. data/lib/state_machines/version.rb +1 -1
  21. data/spec/spec_helper.rb +1 -0
  22. data/spec/state_machines/assertions_spec.rb +31 -0
  23. data/spec/state_machines/branch_spec.rb +827 -0
  24. data/spec/state_machines/callbacks_spec.rb +706 -0
  25. data/spec/state_machines/errors_spec.rb +1 -0
  26. data/spec/state_machines/event_collection_spec.rb +401 -0
  27. data/spec/state_machines/event_spec.rb +1140 -0
  28. data/spec/{helpers → state_machines}/helper_spec.rb +0 -0
  29. data/spec/state_machines/integration_base_spec.rb +12 -0
  30. data/spec/state_machines/integration_spec.rb +132 -0
  31. data/spec/state_machines/invalid_event_spec.rb +19 -0
  32. data/spec/state_machines/invalid_parallel_transition_spec.rb +18 -0
  33. data/spec/state_machines/invalid_transition_spec.rb +114 -0
  34. data/spec/state_machines/machine_collection_spec.rb +606 -0
  35. data/spec/{machine_spec.rb → state_machines/machine_spec.rb} +11 -2
  36. data/spec/{matcher_helpers_spec.rb → state_machines/matcher_helpers_spec.rb} +0 -0
  37. data/spec/{matcher_spec.rb → state_machines/matcher_spec.rb} +0 -0
  38. data/spec/{node_collection_spec.rb → state_machines/node_collection_spec.rb} +0 -0
  39. data/spec/{path_collection_spec.rb → state_machines/path_collection_spec.rb} +0 -0
  40. data/spec/{path_spec.rb → state_machines/path_spec.rb} +0 -0
  41. data/spec/{state_collection_spec.rb → state_machines/state_collection_spec.rb} +0 -0
  42. data/spec/{state_context_spec.rb → state_machines/state_context_spec.rb} +0 -0
  43. data/spec/{state_machine_spec.rb → state_machines/state_machine_spec.rb} +0 -0
  44. data/spec/{state_spec.rb → state_machines/state_spec.rb} +0 -0
  45. data/spec/{transition_collection_spec.rb → state_machines/transition_collection_spec.rb} +0 -0
  46. data/spec/{transition_spec.rb → state_machines/transition_spec.rb} +0 -0
  47. data/spec/support/migration_helpers.rb +9 -0
  48. data/state_machines.gemspec +3 -1
  49. metadata +68 -45
  50. data/lib/state_machines/yard.rb +0 -8
  51. data/spec/errors/default_spec.rb +0 -14
  52. 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
- # Attempts to find an integration that matches the given class. This will
29
- # look through all of the built-in integrations under the StateMachines::Integrations
30
- # namespace and find one that successfully matches the class.
31
- #
32
- # == Examples
33
- #
34
- # class Vehicle
35
- # end
36
- #
37
- # class ActiveModelVehicle
38
- # include ActiveModel::Observing
39
- # include ActiveModel::Validations
40
- # end
41
- #
42
- # class ActiveRecordVehicle < ActiveRecord::Base
43
- # end
44
- #
45
- # class DataMapperVehicle
46
- # include DataMapper::Resource
47
- # end
48
- #
49
- # class MongoidVehicle
50
- # include Mongoid::Document
51
- # end
52
- #
53
- # class MongoMapperVehicle
54
- # include MongoMapper::Document
55
- # end
56
- #
57
- # class SequelVehicle < Sequel::Model
58
- # end
59
- #
60
- # StateMachines::Integrations.match(Vehicle) # => nil
61
- # StateMachines::Integrations.match(ActiveModelVehicle) # => StateMachines::Integrations::ActiveModel
62
- # StateMachines::Integrations.match(ActiveRecordVehicle) # => StateMachines::Integrations::ActiveRecord
63
- # StateMachines::Integrations.match(DataMapperVehicle) # => StateMachines::Integrations::DataMapper
64
- # StateMachines::Integrations.match(MongoidVehicle) # => StateMachines::Integrations::Mongoid
65
- # StateMachines::Integrations.match(MongoMapperVehicle) # => StateMachines::Integrations::MongoMapper
66
- # StateMachines::Integrations.match(SequelVehicle) # => StateMachines::Integrations::Sequel
67
- def self.match(klass)
68
- all.detect {|integration| integration.matches?(klass)}
69
- end
70
-
71
- # Attempts to find an integration that matches the given list of ancestors.
72
- # This will look through all of the built-in integrations under the StateMachines::Integrations
73
- # namespace and find one that successfully matches one of the ancestors.
74
- #
75
- # == Examples
76
- #
77
- # StateMachines::Integrations.match([]) # => nil
78
- # StateMachines::Integrations.match(['ActiveRecord::Base') # => StateMachines::Integrations::ActiveModel
79
- def self.match_ancestors(ancestors)
80
- all.detect {|integration| integration.matches_ancestors?(ancestors)}
81
- end
82
-
83
- # Finds an integration with the given name. If the integration cannot be
84
- # found, then a NameError exception will be raised.
85
- #
86
- # == Examples
87
- #
88
- # StateMachines::Integrations.find_by_name(:active_record) # => StateMachines::Integrations::ActiveRecord
89
- # StateMachines::Integrations.find_by_name(:active_model) # => StateMachines::Integrations::ActiveModel
90
- # StateMachines::Integrations.find_by_name(:data_mapper) # => StateMachines::Integrations::DataMapper
91
- # StateMachines::Integrations.find_by_name(:mongoid) # => StateMachines::Integrations::Mongoid
92
- # StateMachines::Integrations.find_by_name(:mongo_mapper) # => StateMachines::Integrations::MongoMapper
93
- # StateMachines::Integrations.find_by_name(:sequel) # => StateMachines::Integrations::Sequel
94
- # StateMachines::Integrations.find_by_name(:invalid) # => StateMachines::IntegrationNotFound: :invalid is an invalid integration
95
- def self.find_by_name(name)
96
- all.detect {|integration| integration.integration_name == name} || raise(IntegrationNotFound.new(name))
97
- end
98
-
99
- # Gets a list of all of the available integrations for use. This will
100
- # always list the ActiveModel integration last.
101
- #
102
- # == Example
103
- #
104
- # StateMachines::Integrations.all
105
- # # => [StateMachines::Integrations::ActiveRecord, StateMachines::Integrations::DataMapper
106
- # # StateMachines::Integrations::Mongoid, StateMachines::Integrations::MongoMapper,
107
- # # StateMachines::Integrations::Sequel, StateMachines::Integrations::ActiveModel]
108
- def self.all
109
- constants = self.constants.map {|c| c.to_s}.select {|c| c }.sort
110
- constants.map {|c| const_get(c)}
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
@@ -1,5 +1,3 @@
1
- require 'singleton'
2
-
3
1
  module StateMachines
4
2
  # Provides a general strategy pattern for determining whether a match is found
5
3
  # for a value. The algorithm that actually determines the match depends on
@@ -1,5 +1,3 @@
1
- require 'state_machines/assertions'
2
-
3
1
  module StateMachines
4
2
  # Represents a collection of nodes in a state machine, be it events or states.
5
3
  # Nodes will not differentiate between the String and Symbol versions of the
@@ -1,5 +1,3 @@
1
- require 'state_machines/path'
2
-
3
1
  module StateMachines
4
2
  # Represents a collection of paths that are generated based on a set of
5
3
  # requirements regarding what states to start and end on
@@ -1,6 +1,3 @@
1
- require 'state_machines/assertions'
2
- require 'state_machines/state_context'
3
-
4
1
  module StateMachines
5
2
  # A state defines a value that an attribute can be in after being transitioned
6
3
  # 0 or more times. States can represent a value of any type in Ruby, though
@@ -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
- # Gets the value for the given attribute on the node
108
- def value(node, attribute)
109
- attribute == :value ? node.value(false) : super
110
- end
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
- # A method was called in an invalid state context
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: