Class: StateFu::Transition

Inherits:
Object show all
Includes:
Enumerable, HasOptions, Applicable
Defined in:
lib/transition.rb

Overview

TODO - make transition evaluate as true if accepted, false if failed, or nil unless fired

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from HasOptions

#[], #[]=, included

Methods included from Applicable

included

Constructor Details

#initialize(binding, event, target = nil, *argument_list, &block) ⇒ Transition

Returns a new instance of Transition.

Raises:



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
# File 'lib/transition.rb', line 28

def initialize( binding, event, target=nil, *argument_list, &block )
  # ensure we have an Event
  event = binding.machine.events[event] if event.is_a?(Symbol)
  raise( UnknownTarget.new(self, "Not an event: #{event} #{self.inspect}" )) unless event.is_a? Event 
   
  @binding    = binding
  @machine    = binding.machine
  @object     = binding.object
  @origin     = binding.current_state
        
  # ensure we have a target
  target = find_event_target( event, target ) || raise( UnknownTarget.new(self, "target cannot be determined: #{target.inspect} #{self.inspect}"))

  @target     = target
  @event      = event
  @errors     = []
        
  if event.target_for_origin(origin) == target
    # it's a "sequence"
    # which is a hacky way of emulating simpler state machines with
    # state-local events - and in which case, the targets & origins are
    # valid. Quite likely this notion will be removed in time.
  else
    # ensure target is valid for the event
    unless event.targets.include? target 
      raise IllegalTransition.new self, "Illegal target #{target} for #{event}" 
    end

    # ensure current_state is a valid origin for the event
    unless event.origins.include? origin 
      raise IllegalTransition.new( self, "Illegal event #{event.name} for current state #{binding.state_name}" )
    end
  end 
  
  machine.inject_helpers_into( self )
  self.args = argument_list            
  apply!(argument_list, &block )       
end

Instance Attribute Details

#argsObject Also known as: arguments

Returns the value of attribute args.



15
16
17
# File 'lib/transition.rb', line 15

def args
  @args
end

#bindingObject (readonly)

Returns the value of attribute binding.



15
16
17
# File 'lib/transition.rb', line 15

def binding
  @binding
end

#current_hookObject (readonly)

Returns the value of attribute current_hook.



15
16
17
# File 'lib/transition.rb', line 15

def current_hook
  @current_hook
end

#current_hook_slotObject (readonly)

Returns the value of attribute current_hook_slot.



15
16
17
# File 'lib/transition.rb', line 15

def current_hook_slot
  @current_hook_slot
end

#errorsObject (readonly)

Returns the value of attribute errors.



15
16
17
# File 'lib/transition.rb', line 15

def errors
  @errors
end

#eventObject (readonly)

Returns the value of attribute event.



15
16
17
# File 'lib/transition.rb', line 15

def event
  @event
end

#machineObject (readonly)

Returns the value of attribute machine.



15
16
17
# File 'lib/transition.rb', line 15

def machine
  @machine
end

#objectObject (readonly) Also known as: obj, instance, model

Returns the value of attribute object.



15
16
17
# File 'lib/transition.rb', line 15

def object
  @object
end

#originObject (readonly) Also known as: original_state, initial_state, from

Returns the value of attribute origin.



15
16
17
# File 'lib/transition.rb', line 15

def origin
  @origin
end

#targetObject (readonly) Also known as: destination, final_state, to

Returns the value of attribute target.



15
16
17
# File 'lib/transition.rb', line 15

def target
  @target
end

Instance Method Details

#==(other) ⇒ Object

an accepted transition == true an unaccepted transition == false same for === (for case equality)



267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/transition.rb', line 267

def == other
  case other
  when true
    accepted?
  when false
    !accepted?
  when State, Symbol
    current_state == other.to_sym
  when Transition
    inspect == other.inspect
  else
    super( other )
  end
end

#accepted?Boolean Also known as: complete?

Returns:

  • (Boolean)


229
230
231
# File 'lib/transition.rb', line 229

def accepted?
  !!@accepted
end

#check_requirements!(revalidate = false, fail_fast = true) ⇒ Object

raise a RequirementError unless all requirements are met.

Raises:



136
137
138
# File 'lib/transition.rb', line 136

def check_requirements!(revalidate=false, fail_fast=true) # TODO
  raise RequirementError.new( self, unmet_requirement_messages.inspect ) unless requirements_met?(revalidate, fail_fast)
end

#current_stateObject



234
235
236
# File 'lib/transition.rb', line 234

def current_state
  binding.current_state
end

#cycle?Boolean

Returns:

  • (Boolean)


260
261
262
# File 'lib/transition.rb', line 260

def cycle?
  origin == target
end

#each(*a, &b) ⇒ Object



217
218
219
# File 'lib/transition.rb', line 217

def each *a, &b 
  options.each *a, &b 
end

#evaluate(method_name_or_proc) ⇒ Object Also known as: call



295
296
297
# File 'lib/transition.rb', line 295

def evaluate(method_name_or_proc)
  executioner.evaluate(method_name_or_proc)
end

#fire!(*arguments) ⇒ Object

actually fire the transition



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/transition.rb', line 176

def fire!(*arguments) # block? 
  raise TransitionAlreadyFired.new(self) if fired?
  self.args = arguments unless arguments.empty?
        
  check_requirements!
  @fired = true
  begin
    # duplicated: see #hooks method
    StateFu::Hooks::ALL_HOOKS.map do |owner, slot|
      [ [owner, slot], send(owner).hooks[slot] ]
    end.each do |address, hooks|
      Logging.info("running #{address.inspect} hooks for #{object.class} #{object}")
      owner,slot = *address
      hooks.each do |hook|
        Logging.info("running hook #{hooks} for #{object.class} #{object}")
        @current_hook_slot = address
        @current_hook      = hook
        run_hook hook 
      end
      if slot == :entry
        @accepted                        = true
        @binding.persister.current_state = @target
        Logging.info("State is now :#{@target.name} for #{object.class} #{object}")
      end
    end
    # transition complete
    @current_hook_slot               = nil
    @current_hook                    = nil
  rescue TransitionHalted => e
    Logging.info("Transition halted for #{object.class} #{object}: #{e.inspect}")
    @errors << e
  end
  self
end

#fired?Boolean

Returns:

  • (Boolean)


225
226
227
# File 'lib/transition.rb', line 225

def fired?
  !!@fired
end

#first_unmet_requirement(revalidate = false) ⇒ Object



113
114
115
# File 'lib/transition.rb', line 113

def first_unmet_requirement(revalidate=false)
  unmet_requirements(revalidate, fail_fast=true)[0]
end

#first_unmet_requirement_message(revalidate = false) ⇒ Object



131
132
133
# File 'lib/transition.rb', line 131

def first_unmet_requirement_message(revalidate=false)
  evaluate_requirement_message(first_unmet_requirement(revalidate), revalidate)
end

#halt!(message) ⇒ Object

halt a transition with a message can be used to back out of a transition inside eg a state entry hook



167
168
169
# File 'lib/transition.rb', line 167

def halt! message 
  raise StateFu::TransitionHalted.new( self, message )
end

#halted?Boolean

Returns:

  • (Boolean)


221
222
223
# File 'lib/transition.rb', line 221

def halted?
  !@errors.empty?
end

#hooksObject



151
152
153
154
155
# File 'lib/transition.rb', line 151

def hooks
  StateFu::Hooks::ALL_HOOKS.map do |owner, slot|
    [ [owner, slot], send(owner).hooks[slot] ]
  end
end

#hooks_for(element, slot) ⇒ Object

Hooks



147
148
149
# File 'lib/transition.rb', line 147

def hooks_for(element, slot)
  send(element).hooks[slot]
end

#inspectObject

display nice and short



283
284
285
286
287
288
289
290
291
292
293
# File 'lib/transition.rb', line 283

def inspect
  s = self.to_s
  s = s[0,s.length-1]
  s << " event=#{event.to_sym.inspect}" if event
  s << " origin=#{origin.to_sym.inspect}" if origin
  s << " target=#{target.to_sym.inspect}" if target
  s << " args=#{args.inspect}" if args
  s << " options=#{options.inspect}" if options
  s << ">"
  s
end

#requirement_errors(revalidate = false, fail_fast = false) ⇒ Object

return a hash of requirement_name => evaluated message



125
126
127
128
129
# File 'lib/transition.rb', line 125

def requirement_errors(revalidate=false, fail_fast=false)
  unmet_requirements(revalidate, fail_fast).
    map { |requirement| [requirement, evaluate_requirement_message(requirement)]}.
    to_h
end

#requirementsObject

Requirements



92
93
94
# File 'lib/transition.rb', line 92

def requirements
  origin.exit_requirements + target.entry_requirements + event.requirements
end

#requirements_met?(revalidate = false, fail_fast = false) ⇒ Boolean

TODO

Returns:

  • (Boolean)


140
141
142
# File 'lib/transition.rb', line 140

def requirements_met?(revalidate=false, fail_fast=false) # TODO
  unmet_requirements(revalidate, fail_fast).empty?
end

#run_hook(hook) ⇒ Object



157
158
159
# File 'lib/transition.rb', line 157

def run_hook hook 
  evaluate hook 
end

#unmet_requirement_messages(revalidate = false, fail_fast = false) ⇒ Object Also known as: error_messages

TODO



117
118
119
120
121
# File 'lib/transition.rb', line 117

def unmet_requirement_messages(revalidate=false, fail_fast=false) # TODO
  unmet_requirements(revalidate, fail_fast).map do |requirement|
    evaluate_requirement_message(requirement, revalidate)
  end.extend MessageArray
end

#unmet_requirements(revalidate = false, fail_fast = false) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/transition.rb', line 96

def unmet_requirements(revalidate=false, fail_fast=false) 
  if revalidate
    @unmet_requirements = nil
  else
    return @unmet_requirements if @unmet_requirements
  end
  result = [requirements].flatten.uniq.inject([]) do |unmet, requirement|
    unmet << requirement unless evaluate(requirement)
    break(unmet) if fail_fast && !unmet.empty?
    unmet
  end
  raise self.inspect if result.nil?
  # don't cache result if it might 
  @unmet_requirements = result unless (fail_fast && unmet_requirements.length != 0)
  result
end

#valid?(*args) ⇒ Boolean

Returns:

  • (Boolean)


68
69
70
71
# File 'lib/transition.rb', line 68

def valid?(*args)
  self.args = args unless args.empty?      
  requirements_met?(true, true) # revalidate; exit on first failure
end

#with(*args) ⇒ Object



78
79
80
81
# File 'lib/transition.rb', line 78

def with(*args)
  self.args = args unless args.empty?
  self
end

#with?(*args) ⇒ Boolean Also known as: valid_with?

Returns:

  • (Boolean)


83
84
85
# File 'lib/transition.rb', line 83

def with?(*args)
  valid?
end