state_machines 0.6.0 → 0.30.0

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +205 -14
  3. data/lib/state_machines/branch.rb +20 -17
  4. data/lib/state_machines/callback.rb +13 -12
  5. data/lib/state_machines/core.rb +3 -3
  6. data/lib/state_machines/core_ext/class/state_machine.rb +2 -0
  7. data/lib/state_machines/core_ext.rb +2 -0
  8. data/lib/state_machines/error.rb +7 -4
  9. data/lib/state_machines/eval_helpers.rb +93 -26
  10. data/lib/state_machines/event.rb +41 -29
  11. data/lib/state_machines/event_collection.rb +6 -5
  12. data/lib/state_machines/extensions.rb +7 -5
  13. data/lib/state_machines/helper_module.rb +3 -1
  14. data/lib/state_machines/integrations/base.rb +3 -1
  15. data/lib/state_machines/integrations.rb +13 -14
  16. data/lib/state_machines/machine/action_hooks.rb +53 -0
  17. data/lib/state_machines/machine/callbacks.rb +59 -0
  18. data/lib/state_machines/machine/class_methods.rb +93 -0
  19. data/lib/state_machines/machine/configuration.rb +124 -0
  20. data/lib/state_machines/machine/event_methods.rb +59 -0
  21. data/lib/state_machines/machine/helper_generators.rb +125 -0
  22. data/lib/state_machines/machine/integration.rb +70 -0
  23. data/lib/state_machines/machine/parsing.rb +77 -0
  24. data/lib/state_machines/machine/rendering.rb +17 -0
  25. data/lib/state_machines/machine/scoping.rb +44 -0
  26. data/lib/state_machines/machine/state_methods.rb +101 -0
  27. data/lib/state_machines/machine/utilities.rb +85 -0
  28. data/lib/state_machines/machine/validation.rb +39 -0
  29. data/lib/state_machines/machine.rb +83 -673
  30. data/lib/state_machines/machine_collection.rb +23 -15
  31. data/lib/state_machines/macro_methods.rb +4 -2
  32. data/lib/state_machines/matcher.rb +8 -5
  33. data/lib/state_machines/matcher_helpers.rb +3 -1
  34. data/lib/state_machines/node_collection.rb +23 -18
  35. data/lib/state_machines/options_validator.rb +72 -0
  36. data/lib/state_machines/path.rb +7 -5
  37. data/lib/state_machines/path_collection.rb +7 -4
  38. data/lib/state_machines/state.rb +76 -47
  39. data/lib/state_machines/state_collection.rb +5 -3
  40. data/lib/state_machines/state_context.rb +11 -8
  41. data/lib/state_machines/stdio_renderer.rb +74 -0
  42. data/lib/state_machines/syntax_validator.rb +57 -0
  43. data/lib/state_machines/test_helper.rb +568 -0
  44. data/lib/state_machines/transition.rb +45 -41
  45. data/lib/state_machines/transition_collection.rb +27 -26
  46. data/lib/state_machines/version.rb +3 -1
  47. data/lib/state_machines.rb +4 -1
  48. metadata +32 -16
  49. data/lib/state_machines/assertions.rb +0 -40
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Configuration
6
+ # Initializes a new state machine with the given configuration.
7
+ def initialize(owner_class, *args, &)
8
+ options = args.last.is_a?(Hash) ? args.pop : {}
9
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
10
+
11
+ # Find an integration that matches this machine's owner class
12
+ @integration = if options.include?(:integration)
13
+ options[:integration] && StateMachines::Integrations.find_by_name(options[:integration])
14
+ else
15
+ StateMachines::Integrations.match(owner_class)
16
+ end
17
+
18
+ if @integration
19
+ extend @integration
20
+ options = (@integration.defaults || {}).merge(options)
21
+ end
22
+
23
+ # Add machine-wide defaults
24
+ options = { use_transactions: true, initialize: true }.merge(options)
25
+
26
+ # Set machine configuration
27
+ @name = args.first || :state
28
+ @attribute = options[:attribute] || @name
29
+ @events = EventCollection.new(self)
30
+ @states = StateCollection.new(self)
31
+ @callbacks = { before: [], after: [], failure: [] }
32
+ @namespace = options[:namespace]
33
+ @messages = options[:messages] || {}
34
+ @action = options[:action]
35
+ @use_transactions = options[:use_transactions]
36
+ @initialize_state = options[:initialize]
37
+ @action_hook_defined = false
38
+ self.owner_class = owner_class
39
+
40
+ # Merge with sibling machine configurations
41
+ add_sibling_machine_configs
42
+
43
+ # Define class integration
44
+ define_helpers
45
+ define_scopes(options[:plural])
46
+ after_initialize
47
+
48
+ # Evaluate DSL
49
+ instance_eval(&) if block_given?
50
+ self.initial_state = options[:initial] unless sibling_machines.any?
51
+ end
52
+
53
+ # Creates a copy of this machine in addition to copies of each associated
54
+ # event/states/callback, so that the modifications to those collections do
55
+ # not affect the original machine.
56
+ def initialize_copy(orig) # :nodoc:
57
+ super
58
+
59
+ @events = @events.dup
60
+ @events.machine = self
61
+ @states = @states.dup
62
+ @states.machine = self
63
+ @callbacks = { before: @callbacks[:before].dup, after: @callbacks[:after].dup, failure: @callbacks[:failure].dup }
64
+ end
65
+
66
+ # Sets the class which is the owner of this state machine. Any methods
67
+ # generated by states, events, or other parts of the machine will be defined
68
+ # on the given owner class.
69
+ def owner_class=(klass)
70
+ @owner_class = klass
71
+
72
+ # Create modules for extending the class with state/event-specific methods
73
+ @helper_modules = helper_modules = { instance: HelperModule.new(self, :instance), class: HelperModule.new(self, :class) }
74
+ owner_class.class_eval do
75
+ extend helper_modules[:class]
76
+ include helper_modules[:instance]
77
+ end
78
+
79
+ # Add class-/instance-level methods to the owner class for state initialization
80
+ unless owner_class < StateMachines::InstanceMethods
81
+ owner_class.class_eval do
82
+ extend StateMachines::ClassMethods
83
+ include StateMachines::InstanceMethods
84
+ end
85
+
86
+ define_state_initializer if @initialize_state
87
+ end
88
+
89
+ # Record this machine as matched to the name in the current owner class.
90
+ # This will override any machines mapped to the same name in any superclasses.
91
+ owner_class.state_machines[name] = self
92
+ end
93
+
94
+ # Sets the initial state of the machine. This can be either the static name
95
+ # of a state or a lambda block which determines the initial state at
96
+ # creation time.
97
+ def initial_state=(new_initial_state)
98
+ @initial_state = new_initial_state
99
+ add_states([@initial_state]) unless dynamic_initial_state?
100
+
101
+ # Update all states to reflect the new initial state
102
+ states.each { |state| state.initial = (state.name == @initial_state) }
103
+
104
+ # Output a warning if there are conflicting initial states for the machine's
105
+ # attribute
106
+ initial_state = states.detect(&:initial)
107
+ has_owner_default = !owner_class_attribute_default.nil?
108
+ has_conflicting_default = dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state)
109
+ return unless has_owner_default && has_conflicting_default
110
+
111
+ warn(
112
+ "Both #{owner_class.name} and its #{name.inspect} machine have defined " \
113
+ "a different default for \"#{attribute}\". Use only one or the other for " \
114
+ 'defining defaults to avoid unexpected behaviors.'
115
+ )
116
+ end
117
+
118
+ # Gets the attribute name for the given machine scope.
119
+ def attribute(name = :state)
120
+ name == :state ? @attribute : :"#{self.name}_#{name}"
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module EventMethods
6
+ # Defines one or more events for the machine and the transitions that can
7
+ # be performed when those events are run.
8
+ def event(*names, &)
9
+ options = names.last.is_a?(Hash) ? names.pop : {}
10
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
11
+
12
+ # Store the context so that it can be used for / matched against any event
13
+ # that gets added
14
+ @events.context(names, &) if block_given?
15
+
16
+ if names.first.is_a?(Matcher)
17
+ # Add any events referenced in the matcher. When matchers are used,
18
+ # events are not allowed to be configured.
19
+ raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
20
+
21
+ events = add_events(names.first.values)
22
+ else
23
+ events = add_events(names)
24
+
25
+ # Update the configuration for the event(s)
26
+ events.each do |event|
27
+ event.human_name = options[:human_name] if options.include?(:human_name)
28
+
29
+ # Add any states that may have been referenced within the event
30
+ add_states(event.known_states)
31
+ end
32
+ end
33
+
34
+ events.length == 1 ? events.first : events
35
+ end
36
+
37
+ alias on event
38
+
39
+ # Creates a new transition that determines what to change the current state
40
+ # to when an event fires.
41
+ def transition(options)
42
+ raise ArgumentError, 'Must specify :on event' unless options[:on]
43
+
44
+ branches = []
45
+ options = options.dup
46
+ event(*Array(options.delete(:on))) { branches << transition(options) }
47
+
48
+ branches.length == 1 ? branches.first : branches
49
+ end
50
+
51
+ # Gets the list of all possible transition paths from the current state to
52
+ # the given target state. If multiple target states are provided, then
53
+ # this will return all possible paths to those states.
54
+ def paths_for(object, requirements = {})
55
+ PathCollection.new(object, self, requirements)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module HelperGenerators
6
+ protected
7
+
8
+ # Adds helper methods for interacting with the state machine, including
9
+ # for states, events, and transitions
10
+ def define_helpers
11
+ define_state_accessor
12
+ define_state_predicate
13
+ define_event_helpers
14
+ define_path_helpers
15
+ define_action_helpers if define_action_helpers?
16
+ define_name_helpers
17
+ end
18
+
19
+ # Defines the initial values for state machine attributes. Static values
20
+ # are set prior to the original initialize method and dynamic values are
21
+ # set *after* the initialize method in case it is dependent on it.
22
+ def define_state_initializer
23
+ define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
24
+ def initialize(*)
25
+ self.class.state_machines.initialize_states(self) { super }
26
+ end
27
+ END_EVAL
28
+ end
29
+
30
+ # Adds reader/writer methods for accessing the state attribute
31
+ def define_state_accessor
32
+ attribute = self.attribute
33
+
34
+ @helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute)
35
+ @helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=")
36
+ end
37
+
38
+ # Adds predicate method to the owner class for determining the name of the
39
+ # current state
40
+ def define_state_predicate
41
+ call_super = owner_class_ancestor_has_method?(:instance, "#{name}?") ? true : false
42
+ define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
43
+ def #{name}?(*args)
44
+ args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
45
+ end
46
+ END_EVAL
47
+ end
48
+
49
+ # Adds helper methods for getting information about this state machine's
50
+ # events
51
+ def define_event_helpers
52
+ # Gets the events that are allowed to fire on the current object
53
+ define_helper(:instance, attribute(:events)) do |machine, object, *args|
54
+ machine.events.valid_for(object, *args).map(&:name)
55
+ end
56
+
57
+ # Gets the next possible transitions that can be run on the current
58
+ # object
59
+ define_helper(:instance, attribute(:transitions)) do |machine, object, *args|
60
+ machine.events.transitions_for(object, *args)
61
+ end
62
+
63
+ # Fire an arbitrary event for this machine
64
+ define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
65
+ machine.events.fetch(event).fire(object, *args)
66
+ end
67
+
68
+ # Add helpers for tracking the event / transition to invoke when the
69
+ # action is called
70
+ return unless action
71
+
72
+ event_attribute = attribute(:event)
73
+ define_helper(:instance, event_attribute) do |machine, object|
74
+ # Interpret non-blank events as present
75
+ event = machine.read(object, :event, true)
76
+ event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
77
+ end
78
+
79
+ # A roundabout way of writing the attribute is used here so that
80
+ # integrations can hook into this modification
81
+ define_helper(:instance, "#{event_attribute}=") do |machine, object, value|
82
+ machine.write(object, :event, value, true)
83
+ end
84
+
85
+ event_transition_attribute = attribute(:event_transition)
86
+ define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
87
+ protected; attr_accessor #{event_transition_attribute.inspect}
88
+ END_EVAL
89
+ end
90
+
91
+ # Adds helper methods for getting information about this state machine's
92
+ # available transition paths
93
+ def define_path_helpers
94
+ # Gets the paths of transitions available to the current object
95
+ define_helper(:instance, attribute(:paths)) do |machine, object, *args|
96
+ machine.paths_for(object, *args)
97
+ end
98
+ end
99
+
100
+ # Adds helper methods for accessing naming information about states and
101
+ # events on the owner class
102
+ def define_name_helpers
103
+ # Gets the humanized version of a state
104
+ define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state|
105
+ machine.states.fetch(state).human_name(klass)
106
+ end
107
+
108
+ # Gets the humanized version of an event
109
+ define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event|
110
+ machine.events.fetch(event).human_name(klass)
111
+ end
112
+
113
+ # Gets the state name for the current value
114
+ define_helper(:instance, attribute(:name)) do |machine, object|
115
+ machine.states.match!(object).name
116
+ end
117
+
118
+ # Gets the human state name for the current value
119
+ define_helper(:instance, "human_#{attribute(:name)}") do |machine, object|
120
+ machine.states.match!(object).human_name(object.class)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Integration
6
+ # Marks the given object as invalid with the given message.
7
+ #
8
+ # By default, this is a no-op.
9
+ def invalidate(_object, _attribute, _message, _values = []); end
10
+
11
+ # Gets a description of the errors for the given object. This is used to
12
+ # provide more detailed information when an InvalidTransition exception is
13
+ # raised.
14
+ def errors_for(_object)
15
+ ''
16
+ end
17
+
18
+ # Resets any errors previously added when invalidating the given object.
19
+ #
20
+ # By default, this is a no-op.
21
+ def reset(_object); end
22
+
23
+ # Generates a user-friendly name for the given message.
24
+ def generate_message(name, values = [])
25
+ format(@messages[name] || @messages[:invalid_transition] || default_messages[name] || default_messages[:invalid_transition], state: values.first)
26
+ end
27
+
28
+ # Runs a transaction, yielding the given block.
29
+ #
30
+ # By default, this is a no-op.
31
+ def within_transaction(object, &)
32
+ if use_transactions && respond_to?(:transaction, true)
33
+ transaction(object, &)
34
+ else
35
+ yield
36
+ end
37
+ end
38
+
39
+ protected
40
+
41
+ # Runs additional initialization hooks. By default, this is a no-op.
42
+ def after_initialize; end
43
+
44
+ # Always yields
45
+ def transaction(_object)
46
+ yield
47
+ end
48
+
49
+ # Gets the initial attribute value defined by the owner class (outside of
50
+ # the machine's definition). By default, this is always nil.
51
+ def owner_class_attribute_default
52
+ nil
53
+ end
54
+
55
+ # Checks whether the given state matches the attribute default specified
56
+ # by the owner class
57
+ def owner_class_attribute_default_matches?(state)
58
+ state.matches?(owner_class_attribute_default)
59
+ end
60
+
61
+ private
62
+
63
+ # Gets the default messages that can be used in the machine for invalid
64
+ # transitions.
65
+ def default_messages
66
+ { invalid_transition: '%<state>s cannot transition via "%<event>s"' }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Parsing
6
+ private
7
+
8
+ # Parses callback arguments for backward compatibility with both positional
9
+ # and keyword argument styles. Supports Ruby 3.2+ keyword arguments while
10
+ # maintaining full backward compatibility with the legacy API.
11
+ def parse_callback_arguments(args, options)
12
+ # Handle legacy positional args: before_transition(:method1, :method2, from: :state)
13
+ if args.any?
14
+ # Extract hash options from the end of args if present
15
+ parsed_options = args.last.is_a?(Hash) ? args.pop.dup : {}
16
+
17
+ # Merge any additional keyword options
18
+ parsed_options.merge!(options) if options.any?
19
+
20
+ # Remaining args become the :do option (method names to call)
21
+ parsed_options[:do] = args if args.any?
22
+
23
+ parsed_options
24
+ else
25
+ # Pure keyword argument style: before_transition(from: :state, to: :other, do: :method)
26
+ options.dup
27
+ end
28
+ end
29
+
30
+ # Adds a new transition callback of the given type.
31
+ def add_callback(type, options, &)
32
+ callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &)
33
+ add_states(callback.known_states)
34
+ callback
35
+ end
36
+
37
+ # Tracks the given set of states in the list of all known states for
38
+ # this machine
39
+ def add_states(new_states)
40
+ new_states.map do |new_state|
41
+ # Check for other states that use a different class type for their name.
42
+ # This typically prevents string / symbol misuse.
43
+ if new_state && (conflict = states.detect { |state| state.name && state.name.class != new_state.class })
44
+ raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent"
45
+ end
46
+
47
+ unless (state = states[new_state])
48
+ states << state = State.new(self, new_state)
49
+
50
+ # Copy states over to sibling machines
51
+ sibling_machines.each { |machine| machine.states << state }
52
+ end
53
+
54
+ state
55
+ end
56
+ end
57
+
58
+ # Tracks the given set of events in the list of all known events for
59
+ # this machine
60
+ def add_events(new_events)
61
+ new_events.map do |new_event|
62
+ # Check for other states that use a different class type for their name.
63
+ # This typically prevents string / symbol misuse.
64
+ if (conflict = events.detect { |event| event.name.class != new_event.class })
65
+ raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent"
66
+ end
67
+
68
+ unless (event = events[new_event])
69
+ events << event = Event.new(self, new_event)
70
+ end
71
+
72
+ event
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Rendering
6
+ # Gets the renderer for this machine.
7
+ def renderer
8
+ @renderer ||= StdioRenderer.new
9
+ end
10
+
11
+ # Generates a visual representation of this machine for a given format.
12
+ def draw(**)
13
+ renderer.draw(self, **)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Scoping
6
+ protected
7
+
8
+ # Defines the with/without scope helpers for this attribute. Both the
9
+ # singular and plural versions of the attribute are defined for each
10
+ # scope helper. A custom plural can be specified if it cannot be
11
+ # automatically determined by either calling +pluralize+ on the attribute
12
+ # name or adding an "s" to the end of the name.
13
+ def define_scopes(custom_plural = nil)
14
+ plural = custom_plural || pluralize(name)
15
+
16
+ %i[with without].each do |kind|
17
+ [name, plural].map(&:to_s).uniq.each do |suffix|
18
+ method = "#{kind}_#{suffix}"
19
+
20
+ next unless (scope = send("create_#{kind}_scope", method))
21
+
22
+ # Converts state names to their corresponding values so that they
23
+ # can be looked up properly
24
+ define_helper(:class, method) do |machine, klass, *states|
25
+ run_scope(scope, machine, klass, states)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ # Creates a scope for finding objects *with* a particular value or values
32
+ # for the attribute.
33
+ #
34
+ # By default, this is a no-op.
35
+ def create_with_scope(name); end
36
+
37
+ # Creates a scope for finding objects *without* a particular value or
38
+ # values for the attribute.
39
+ #
40
+ # By default, this is a no-op.
41
+ def create_without_scope(name); end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module StateMethods
6
+ # Gets the initial state of the machine for the given object. If a dynamic
7
+ # initial state was configured for this machine, then the object will be
8
+ # passed into the lambda block to help determine the actual state.
9
+ def initial_state(object)
10
+ states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?(:@initial_state)
11
+ end
12
+
13
+ # Whether a dynamic initial state is being used in the machine
14
+ def dynamic_initial_state?
15
+ instance_variable_defined?(:@initial_state) && @initial_state.is_a?(Proc)
16
+ end
17
+
18
+ # Initializes the state on the given object. Initial values are only set if
19
+ # the machine's attribute hasn't been previously initialized.
20
+ #
21
+ # Configuration options:
22
+ # * <tt>:force</tt> - Whether to initialize the state regardless of its
23
+ # current value
24
+ # * <tt>:to</tt> - A hash to set the initial value in instead of writing
25
+ # directly to the object
26
+ def initialize_state(object, options = {})
27
+ state = initial_state(object)
28
+ return unless state && (options[:force] || initialize_state?(object))
29
+
30
+ value = state.value
31
+
32
+ if (hash = options[:to])
33
+ hash[attribute.to_s] = value
34
+ else
35
+ write(object, :state, value)
36
+ end
37
+ end
38
+
39
+ # Customizes the definition of one or more states in the machine.
40
+ def state(*names, &)
41
+ options = names.last.is_a?(Hash) ? names.pop : {}
42
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :value, :cache, :if, :human_name)
43
+
44
+ # Store the context so that it can be used for / matched against any state
45
+ # that gets added
46
+ @states.context(names, &) if block_given?
47
+
48
+ if names.first.is_a?(Matcher)
49
+ # Add any states referenced in the matcher. When matchers are used,
50
+ # states are not allowed to be configured.
51
+ raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
52
+
53
+ states = add_states(names.first.values)
54
+ else
55
+ states = add_states(names)
56
+
57
+ # Update the configuration for the state(s)
58
+ states.each do |state|
59
+ if options.include?(:value)
60
+ state.value = options[:value]
61
+ self.states.update(state)
62
+ end
63
+
64
+ state.human_name = options[:human_name] if options.include?(:human_name)
65
+ state.cache = options[:cache] if options.include?(:cache)
66
+ state.matcher = options[:if] if options.include?(:if)
67
+ end
68
+ end
69
+
70
+ states.length == 1 ? states.first : states
71
+ end
72
+
73
+ alias other_states state
74
+
75
+ # Gets the current value stored in the given object's attribute.
76
+ def read(object, attribute, ivar = false)
77
+ attribute = self.attribute(attribute)
78
+ if ivar
79
+ object.instance_variable_defined?(:"@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
80
+ else
81
+ object.send(attribute)
82
+ end
83
+ end
84
+
85
+ # Sets a new value in the given object's attribute.
86
+ def write(object, attribute, value, ivar = false)
87
+ attribute = self.attribute(attribute)
88
+ ivar ? object.instance_variable_set(:"@#{attribute}", value) : object.send("#{attribute}=", value)
89
+ end
90
+
91
+ protected
92
+
93
+ # Determines if the machine's attribute needs to be initialized. This
94
+ # will only be true if the machine's attribute is blank.
95
+ def initialize_state?(object)
96
+ value = read(object, :state)
97
+ (value.nil? || (value.respond_to?(:empty?) && value.empty?)) && !states[value, :value]
98
+ end
99
+ end
100
+ end
101
+ end