state_machine 0.2.1 → 0.3.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.
- data/CHANGELOG.rdoc +15 -0
- data/README.rdoc +64 -14
- data/Rakefile +1 -1
- data/lib/state_machine.rb +25 -28
- data/lib/state_machine/event.rb +47 -111
- data/lib/state_machine/machine.rb +302 -67
- data/lib/state_machine/transition.rb +173 -68
- data/test/app_root/app/models/auto_shop.rb +2 -2
- data/test/app_root/app/models/switch_observer.rb +20 -0
- data/test/app_root/app/models/vehicle.rb +14 -7
- data/test/app_root/config/environment.rb +7 -0
- data/test/factory.rb +6 -0
- data/test/functional/state_machine_test.rb +53 -1
- data/test/unit/event_test.rb +43 -188
- data/test/unit/machine_test.rb +154 -30
- data/test/unit/state_machine_test.rb +39 -29
- data/test/unit/transition_test.rb +347 -218
- metadata +6 -3
data/CHANGELOG.rdoc
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
== master
|
2
2
|
|
3
|
+
== 0.3.0 / 2008-09-07
|
4
|
+
|
5
|
+
* No longer allow additional arguments to be passed into event actions
|
6
|
+
* Add support for can_#{event}? for checking whether an event can be fired based on the current state of the record
|
7
|
+
* Don't use callbacks for performing transitions
|
8
|
+
* Fix state machines in subclasses not knowing what states/events/transitions were defined by superclasses
|
9
|
+
* Replace all before/after_exit/enter/loopback callback hooks and :before/:after options for events with before_transition/after_transition callbacks, e.g.
|
10
|
+
|
11
|
+
before_transition :from => 'parked', :do => :lock_doors # was before_exit :parked, :lock_doors
|
12
|
+
after_transition :on => 'ignite', :do => :turn_on_radio # was event :ignite, :after => :turn_on_radio do
|
13
|
+
|
14
|
+
* Always save when an event is fired even if it results in a loopback [Jürgen Strobel]
|
15
|
+
* Ensure initial state callbacks are invoked in the proper order when an event is fired on a new record
|
16
|
+
* Add before_loopback and after_loopback hooks [Jürgen Strobel]
|
17
|
+
|
3
18
|
== 0.2.1 / 2008-07-05
|
4
19
|
|
5
20
|
* Add more descriptive exceptions
|
data/README.rdoc
CHANGED
@@ -40,14 +40,15 @@ making it so simple you don't even need to know what a state machine is :)
|
|
40
40
|
|
41
41
|
Below is an example of many of the features offered by this plugin, including
|
42
42
|
* Initial states
|
43
|
-
*
|
44
|
-
* Event callbacks
|
43
|
+
* Transition callbacks
|
45
44
|
* Conditional transitions
|
46
45
|
|
47
46
|
class Vehicle < ActiveRecord::Base
|
48
47
|
state_machine :state, :initial => 'idling' do
|
49
|
-
|
50
|
-
|
48
|
+
before_transition :from => %w(parked idling), :do => :put_on_seatbelt
|
49
|
+
after_transition :to => 'parked', :do => lambda {|vehicle| vehicle.update_attribute(:seatbelt_on, false)}
|
50
|
+
after_transition :on => 'crash', :do => :tow!
|
51
|
+
after_transition :on => 'repair', :do => :fix!
|
51
52
|
|
52
53
|
event :park do
|
53
54
|
transition :to => 'parked', :from => %w(idling first_gear)
|
@@ -73,19 +74,21 @@ Below is an example of many of the features offered by this plugin, including
|
|
73
74
|
transition :to => 'first_gear', :from => 'second_gear'
|
74
75
|
end
|
75
76
|
|
76
|
-
event :crash
|
77
|
+
event :crash do
|
77
78
|
transition :to => 'stalled', :from => %w(first_gear second_gear third_gear), :unless => :auto_shop_busy?
|
78
79
|
end
|
79
80
|
|
80
|
-
event :repair
|
81
|
+
event :repair do
|
81
82
|
transition :to => 'parked', :from => 'stalled', :if => :auto_shop_busy?
|
82
83
|
end
|
83
84
|
end
|
84
85
|
|
85
86
|
def tow!
|
87
|
+
# do something here
|
86
88
|
end
|
87
89
|
|
88
90
|
def fix!
|
91
|
+
# do something here
|
89
92
|
end
|
90
93
|
|
91
94
|
def auto_shop_busy?
|
@@ -96,16 +99,62 @@ Below is an example of many of the features offered by this plugin, including
|
|
96
99
|
Using the above model as an example, you can interact with the state machine
|
97
100
|
like so:
|
98
101
|
|
99
|
-
vehicle = Vehicle.create
|
100
|
-
vehicle.ignite
|
101
|
-
vehicle
|
102
|
-
vehicle.shift_up
|
103
|
-
vehicle
|
104
|
-
vehicle.shift_up
|
105
|
-
vehicle
|
102
|
+
vehicle = Vehicle.create # => #<Vehicle id: 1, seatbelt_on: false, state: "parked">
|
103
|
+
vehicle.ignite # => true
|
104
|
+
vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "idling">
|
105
|
+
vehicle.shift_up # => true
|
106
|
+
vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "first_gear">
|
107
|
+
vehicle.shift_up # => true
|
108
|
+
vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "second_gear">
|
106
109
|
|
107
110
|
# The bang (!) operator can raise exceptions if the event fails
|
108
|
-
vehicle.park!
|
111
|
+
vehicle.park! # => PluginAWeek::StateMachine::InvalidTransition: Cannot transition via :park from "second_gear"
|
112
|
+
|
113
|
+
=== With enumerations
|
114
|
+
|
115
|
+
Using the acts_as_enumeration[http://github.com/pluginaweek/acts_as_enumeration] plugin
|
116
|
+
states can be transparently stored using record ids in the database like so:
|
117
|
+
|
118
|
+
class VehicleState < ActiveRecord::Base
|
119
|
+
acts_as_enumeration
|
120
|
+
|
121
|
+
create :id => 1, :name => 'parked'
|
122
|
+
create :id => 2, :name => 'idling'
|
123
|
+
create :id => 3, :name => 'first_gear'
|
124
|
+
...
|
125
|
+
end
|
126
|
+
|
127
|
+
class Vehicle < ActiveRecord::Base
|
128
|
+
belongs_to :state, :class_name => 'VehicleState'
|
129
|
+
|
130
|
+
state_machine :state, :initial => 'idling' do
|
131
|
+
...
|
132
|
+
|
133
|
+
event :park do
|
134
|
+
transition :to => 'parked', :from => %w(idling first_gear)
|
135
|
+
end
|
136
|
+
|
137
|
+
event :ignite do
|
138
|
+
transition :to => 'stalled', :from => 'stalled'
|
139
|
+
transition :to => 'idling', :from => 'parked'
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
...
|
144
|
+
end
|
145
|
+
|
146
|
+
Notice in the above example, the state machine definition remains *exactly* the
|
147
|
+
same. However, when interacting with the records, the actual state will be
|
148
|
+
stored using the identifiers defined for the enumeration:
|
149
|
+
|
150
|
+
vehicle = Vehicle.create # => #<Vehicle id: 1, seatbelt_on: false, state_id: 1>
|
151
|
+
vehicle.ignite # => true
|
152
|
+
vehicle # => #<Vehicle id: 1, seatbelt_on: true, state_id: 2>
|
153
|
+
vehicle.shift_up # => true
|
154
|
+
vehicle # => #<Vehicle id: 1, seatbelt_on: true, state_id: 3>
|
155
|
+
|
156
|
+
This allows states to take on more complex functionality other than just being
|
157
|
+
a string value.
|
109
158
|
|
110
159
|
== Tools
|
111
160
|
|
@@ -130,3 +179,4 @@ To run against a specific version of Rails:
|
|
130
179
|
== References
|
131
180
|
|
132
181
|
* Scott Barron - acts_as_state_machine[http://elitists.textdriven.com/svn/plugins/acts_as_state_machine]
|
182
|
+
* acts_as_enumeration[http://github.com/pluginaweek/acts_as_enumeration]
|
data/Rakefile
CHANGED
@@ -5,7 +5,7 @@ require 'rake/contrib/sshpublisher'
|
|
5
5
|
|
6
6
|
spec = Gem::Specification.new do |s|
|
7
7
|
s.name = 'state_machine'
|
8
|
-
s.version = '0.
|
8
|
+
s.version = '0.3.0'
|
9
9
|
s.platform = Gem::Platform::RUBY
|
10
10
|
s.summary = 'Adds support for creating state machines for attributes within a model'
|
11
11
|
|
data/lib/state_machine.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'state_machine/machine'
|
2
2
|
|
3
3
|
module PluginAWeek #:nodoc:
|
4
|
-
# A state machine is a model of behavior composed of states,
|
5
|
-
#
|
6
|
-
# functionality within
|
4
|
+
# A state machine is a model of behavior composed of states, events, and
|
5
|
+
# transitions. This helper adds support for defining this type of
|
6
|
+
# functionality within ActiveRecord models.
|
7
7
|
module StateMachine
|
8
8
|
def self.included(base) #:nodoc:
|
9
9
|
base.class_eval do
|
@@ -19,10 +19,10 @@ module PluginAWeek #:nodoc:
|
|
19
19
|
# * +initial+ - The initial value of the attribute. This can either be the actual value or a Proc for dynamic initial states.
|
20
20
|
#
|
21
21
|
# This also requires a block which will be used to actually configure the
|
22
|
-
# events and transitions for the state machine. *Note* that this block
|
23
|
-
# be executed within the context of the state machine. As a result,
|
24
|
-
# not be able to access any class methods on the model unless you
|
25
|
-
# them directly (i.e. specifying the class name).
|
22
|
+
# events and transitions for the state machine. *Note* that this block
|
23
|
+
# will be executed within the context of the state machine. As a result,
|
24
|
+
# you will not be able to access any class methods on the model unless you
|
25
|
+
# refer to them directly (i.e. specifying the class name).
|
26
26
|
#
|
27
27
|
# For examples on the types of configured state machines and blocks, see
|
28
28
|
# the section below.
|
@@ -61,7 +61,7 @@ module PluginAWeek #:nodoc:
|
|
61
61
|
# With a dynamic initial state:
|
62
62
|
#
|
63
63
|
# class Switch < ActiveRecord::Base
|
64
|
-
# state_machine :status, :initial =>
|
64
|
+
# state_machine :status, :initial => lambda {|switch| (8..22).include?(Time.now.hour) ? 'on' : 'off'} do
|
65
65
|
# ...
|
66
66
|
# end
|
67
67
|
# end
|
@@ -70,24 +70,32 @@ module PluginAWeek #:nodoc:
|
|
70
70
|
#
|
71
71
|
# For more information about how to configure an event and its associated
|
72
72
|
# transitions, see PluginAWeek::StateMachine::Machine#event
|
73
|
+
#
|
74
|
+
# == Defining callbacks
|
75
|
+
#
|
76
|
+
# Within the +state_machine+ block, you can also define callbacks for
|
77
|
+
# particular states. For more information about defining these callbacks,
|
78
|
+
# see PluginAWeek::StateMachine::Machine#before_transition and
|
79
|
+
# PluginAWeek::StateMachine::Machine#after_transition.
|
73
80
|
def state_machine(*args, &block)
|
74
81
|
unless included_modules.include?(PluginAWeek::StateMachine::InstanceMethods)
|
75
82
|
write_inheritable_attribute :state_machines, {}
|
76
83
|
class_inheritable_reader :state_machines
|
77
84
|
|
78
|
-
after_create :run_initial_state_machine_actions
|
79
|
-
|
80
85
|
include PluginAWeek::StateMachine::InstanceMethods
|
81
86
|
end
|
82
87
|
|
83
88
|
options = args.extract_options!
|
84
89
|
attribute = args.any? ? args.first.to_s : 'state'
|
85
|
-
options[:initial] = state_machines[attribute].initial_state_without_processing if !options.include?(:initial) && state_machines[attribute]
|
86
90
|
|
87
|
-
#
|
88
|
-
#
|
89
|
-
|
91
|
+
# Creates the state machine for this class. If a superclass has already
|
92
|
+
# defined the machine, then a copy of it will be used with its context
|
93
|
+
# changed to this class. If no machine has been defined before for the
|
94
|
+
# attribute, a new one will be created.
|
95
|
+
original = state_machines[attribute]
|
96
|
+
machine = state_machines[attribute] = original ? original.within_context(self, options) : PluginAWeek::StateMachine::Machine.new(self, attribute, options)
|
90
97
|
machine.instance_eval(&block) if block
|
98
|
+
|
91
99
|
machine
|
92
100
|
end
|
93
101
|
end
|
@@ -103,26 +111,15 @@ module PluginAWeek #:nodoc:
|
|
103
111
|
def initialize_with_state_machine(attributes = nil)
|
104
112
|
initialize_without_state_machine(attributes)
|
105
113
|
|
106
|
-
attribute_keys = (attributes || {}).keys.map!(&:to_s)
|
107
|
-
|
108
114
|
# Set the initial value of each state machine as long as the value wasn't
|
109
|
-
# included in the
|
115
|
+
# included in the initial attributes
|
116
|
+
attributes = (attributes || {}).stringify_keys
|
110
117
|
self.class.state_machines.each do |attribute, machine|
|
111
|
-
unless
|
112
|
-
send("#{attribute}=", machine.initial_state(self))
|
113
|
-
end
|
118
|
+
send("#{attribute}=", machine.initial_state(self)) unless attributes.include?(attribute)
|
114
119
|
end
|
115
120
|
|
116
121
|
yield self if block_given?
|
117
122
|
end
|
118
|
-
|
119
|
-
# Records the transition for the record going into its initial state
|
120
|
-
def run_initial_state_machine_actions
|
121
|
-
self.class.state_machines.each do |attribute, machine|
|
122
|
-
callback = "after_enter_#{attribute}_#{self[attribute]}"
|
123
|
-
run_callbacks(callback) if self[attribute] && self.class.respond_to?(callback)
|
124
|
-
end
|
125
|
-
end
|
126
123
|
end
|
127
124
|
end
|
128
125
|
end
|
data/lib/state_machine/event.rb
CHANGED
@@ -6,7 +6,7 @@ module PluginAWeek #:nodoc:
|
|
6
6
|
# another
|
7
7
|
class Event
|
8
8
|
# The state machine for which this event is defined
|
9
|
-
|
9
|
+
attr_accessor :machine
|
10
10
|
|
11
11
|
# The name of the action that fires the event
|
12
12
|
attr_reader :name
|
@@ -17,22 +17,20 @@ module PluginAWeek #:nodoc:
|
|
17
17
|
delegate :owner_class,
|
18
18
|
:to => :machine
|
19
19
|
|
20
|
-
# Creates a new event within the context of the given machine
|
21
|
-
|
22
|
-
# Configuration options:
|
23
|
-
# * +before+ - Callbacks to invoke before the event is fired
|
24
|
-
# * +after+ - Callbacks to invoke after the event is fired
|
25
|
-
def initialize(machine, name, options = {})
|
26
|
-
options.assert_valid_keys(:before, :after)
|
27
|
-
|
20
|
+
# Creates a new event within the context of the given machine
|
21
|
+
def initialize(machine, name)
|
28
22
|
@machine = machine
|
29
23
|
@name = name
|
30
|
-
@options = options.stringify_keys
|
31
24
|
@transitions = []
|
32
25
|
|
33
|
-
|
34
|
-
|
35
|
-
|
26
|
+
add_actions
|
27
|
+
end
|
28
|
+
|
29
|
+
# Creates a copy of this event in addition to the list of associated
|
30
|
+
# transitions to prevent conflicts across different events.
|
31
|
+
def initialize_copy(orig) #:nodoc:
|
32
|
+
super
|
33
|
+
@transitions = @transitions.dup
|
36
34
|
end
|
37
35
|
|
38
36
|
# Creates a new transition to the specified state.
|
@@ -41,7 +39,7 @@ module PluginAWeek #:nodoc:
|
|
41
39
|
# * +to+ - The state that being transitioned to
|
42
40
|
# * +from+ - A state or array of states that can be transitioned from. If not specified, then the transition can occur for *any* from state
|
43
41
|
# * +except_from+ - A state or array of states that *cannot* be transitioned from.
|
44
|
-
# * +if+ - Specifies a method, proc or string to call to determine if the
|
42
|
+
# * +if+ - Specifies a method, proc or string to call to determine if the transition should occur (e.g. :if => :moving?, or :if => Proc.new {|car| car.speed > 60}). The method, proc or string should return or evaluate to a true or false value.
|
45
43
|
# * +unless+ - Specifies a method, proc or string to call to determine if the transition should not occur (e.g. :unless => :stopped?, or :unless => Proc.new {|car| car.speed <= 60}). The method, proc or string should return or evaluate to a true or false value.
|
46
44
|
#
|
47
45
|
# == Examples
|
@@ -52,128 +50,66 @@ module PluginAWeek #:nodoc:
|
|
52
50
|
# transition :to => 'parked', :from => 'first_gear', :if => :moving?
|
53
51
|
# transition :to => 'parked', :from => 'first_gear', :unless => :stopped?
|
54
52
|
# transition :to => 'parked', :except_from => 'parked'
|
55
|
-
def transition(options
|
56
|
-
|
57
|
-
options.symbolize_keys!
|
58
|
-
callback_options = {:if => options.delete(:if), :unless => options.delete(:unless)}
|
59
|
-
|
60
|
-
transition = Transition.new(self, options)
|
61
|
-
|
62
|
-
# Add the callback to the model. If the callback fails, then the next
|
63
|
-
# available callback for the event will run until one is successful.
|
64
|
-
callback = Proc.new {|record, *args| try_transition(transition, false, record, *args)}
|
65
|
-
owner_class.send("transition_on_#{name}", callback, callback_options)
|
66
|
-
|
67
|
-
# Add the callback! to the model similar to above
|
68
|
-
callback = Proc.new {|record, *args| try_transition(transition, true, record, *args)}
|
69
|
-
owner_class.send("transition_bang_on_#{name}", callback, callback_options)
|
70
|
-
|
71
|
-
transitions << transition
|
53
|
+
def transition(options)
|
54
|
+
transitions << transition = Transition.new(self, options)
|
72
55
|
transition
|
73
56
|
end
|
74
57
|
|
58
|
+
# Determines whether any transitions can be performed for this event based
|
59
|
+
# on the current state of the given record.
|
60
|
+
#
|
61
|
+
# If the event can't be fired, then this will return false, otherwise true.
|
62
|
+
def can_fire?(record)
|
63
|
+
transitions.any? {|transition| transition.can_perform?(record)}
|
64
|
+
end
|
65
|
+
|
75
66
|
# Attempts to perform one of the event's transitions for the given record.
|
76
67
|
# Any additional arguments will be passed to the event's callbacks.
|
77
|
-
def fire(record
|
78
|
-
|
68
|
+
def fire(record)
|
69
|
+
run(record, false) || false
|
79
70
|
end
|
80
71
|
|
81
72
|
# Attempts to perform one of the event's transitions for the given record.
|
82
73
|
# If the transition cannot be made, then a PluginAWeek::StateMachine::InvalidTransition
|
83
74
|
# error will be raised.
|
84
|
-
def fire!(record
|
85
|
-
|
75
|
+
def fire!(record)
|
76
|
+
run(record, true) || raise(PluginAWeek::StateMachine::InvalidTransition, "Cannot transition via :#{name} from \"#{record.send(machine.attribute)}\"")
|
86
77
|
end
|
87
78
|
|
88
79
|
private
|
89
|
-
# Fires the event
|
90
|
-
def fire_with_optional_bang(record, bang, *args)
|
91
|
-
record.class.transaction do
|
92
|
-
invoke_transition_callbacks(record, bang, *args) || raise(ActiveRecord::Rollback)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
80
|
# Add the various instance methods that can transition the record using
|
97
81
|
# the current event
|
98
|
-
def
|
82
|
+
def add_actions
|
83
|
+
attribute = machine.attribute
|
99
84
|
name = self.name
|
100
|
-
owner_class = self.owner_class
|
101
|
-
machine = self.machine
|
102
85
|
|
103
86
|
owner_class.class_eval do
|
104
|
-
|
105
|
-
define_method(name)
|
106
|
-
|
107
|
-
end
|
108
|
-
|
109
|
-
# Fires the event, raising an exception if it fails
|
110
|
-
define_method("#{name}!") do |*args|
|
111
|
-
owner_class.state_machines[machine.attribute].events[name].fire!(self, *args)
|
112
|
-
end
|
87
|
+
define_method(name) {self.class.state_machines[attribute].events[name].fire(self)}
|
88
|
+
define_method("#{name}!") {self.class.state_machines[attribute].events[name].fire!(self)}
|
89
|
+
define_method("can_#{name}?") {self.class.state_machines[attribute].events[name].can_fire?(self)}
|
113
90
|
end
|
114
91
|
end
|
115
92
|
|
116
|
-
#
|
117
|
-
def add_transition_callbacks
|
118
|
-
%W(transition transition_bang).each do |callback_name|
|
119
|
-
callback_name = "#{callback_name}_on_#{name}"
|
120
|
-
owner_class.define_callbacks(callback_name)
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
# Adds the before/after callbacks for when the event is performed
|
125
|
-
def add_event_callbacks
|
126
|
-
%w(before after).each do |type|
|
127
|
-
callback_name = "#{type}_#{name}"
|
128
|
-
owner_class.define_callbacks(callback_name)
|
129
|
-
|
130
|
-
# Add each defined callback
|
131
|
-
Array(@options[type]).each {|callback| owner_class.send(callback_name, callback)}
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
# Attempts to perform the given transition. If it can't be performed based
|
136
|
-
# on the state of the given record, then the transition will be skipped
|
137
|
-
# and the next available one will be tried.
|
93
|
+
# Attempts to find a transition that can be performed for this event.
|
138
94
|
#
|
139
|
-
#
|
140
|
-
#
|
141
|
-
def
|
142
|
-
|
143
|
-
return false if invoke_event_callbacks(:before, record, *args) == false
|
144
|
-
result = bang ? transition.perform!(record, *args) : transition.perform(record, *args)
|
145
|
-
invoke_event_callbacks(:after, record, *args)
|
146
|
-
result
|
147
|
-
else
|
148
|
-
# Indicate that the transition cannot be performed
|
149
|
-
:skip
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
# Invokes a particulary type of callback for the event
|
154
|
-
def invoke_event_callbacks(type, record, *args)
|
155
|
-
args = [record] + args
|
95
|
+
# +bang+ indicates whether +perform+ or <tt>perform!</tt> will be
|
96
|
+
# invoked on transitions.
|
97
|
+
def run(record, bang)
|
98
|
+
result = false
|
156
99
|
|
157
|
-
record.class.
|
158
|
-
|
159
|
-
|
100
|
+
record.class.transaction do
|
101
|
+
transitions.each do |transition|
|
102
|
+
if transition.can_perform?(record)
|
103
|
+
result = bang ? transition.perform!(record) : transition.perform(record)
|
104
|
+
break
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Rollback any changes if the transition failed
|
109
|
+
raise ActiveRecord::Rollback unless result
|
160
110
|
end
|
161
|
-
end
|
162
|
-
|
163
|
-
# Invokes the callbacks for each transition in order to find one that
|
164
|
-
# completes successfully.
|
165
|
-
#
|
166
|
-
# +bang+ indicates whether perform or perform! will be invoked on the
|
167
|
-
# transitions in the callback chain
|
168
|
-
def invoke_transition_callbacks(record, bang, *args)
|
169
|
-
args = [record] + args
|
170
|
-
callback_chain = "transition#{'_bang' if bang}_on_#{name}_callback_chain"
|
171
111
|
|
172
|
-
result
|
173
|
-
result = callback.call(*args)
|
174
|
-
break result if [true, false].include?(result)
|
175
|
-
end
|
176
|
-
result == true
|
112
|
+
result
|
177
113
|
end
|
178
114
|
end
|
179
115
|
end
|