state_machines 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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: