joelind-state_machine 0.8.1

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 (78) hide show
  1. data/CHANGELOG.rdoc +297 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +466 -0
  4. data/Rakefile +98 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +388 -0
  28. data/lib/state_machine/assertions.rb +36 -0
  29. data/lib/state_machine/callback.rb +189 -0
  30. data/lib/state_machine/condition_proxy.rb +94 -0
  31. data/lib/state_machine/eval_helpers.rb +67 -0
  32. data/lib/state_machine/event.rb +252 -0
  33. data/lib/state_machine/event_collection.rb +122 -0
  34. data/lib/state_machine/extensions.rb +149 -0
  35. data/lib/state_machine/guard.rb +230 -0
  36. data/lib/state_machine/integrations.rb +68 -0
  37. data/lib/state_machine/integrations/active_record.rb +492 -0
  38. data/lib/state_machine/integrations/active_record/locale.rb +11 -0
  39. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  40. data/lib/state_machine/integrations/data_mapper.rb +351 -0
  41. data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  42. data/lib/state_machine/integrations/sequel.rb +322 -0
  43. data/lib/state_machine/machine.rb +1467 -0
  44. data/lib/state_machine/machine_collection.rb +155 -0
  45. data/lib/state_machine/matcher.rb +123 -0
  46. data/lib/state_machine/matcher_helpers.rb +54 -0
  47. data/lib/state_machine/node_collection.rb +152 -0
  48. data/lib/state_machine/state.rb +249 -0
  49. data/lib/state_machine/state_collection.rb +112 -0
  50. data/lib/state_machine/transition.rb +394 -0
  51. data/tasks/state_machine.rake +1 -0
  52. data/tasks/state_machine.rb +30 -0
  53. data/test/classes/switch.rb +11 -0
  54. data/test/functional/state_machine_test.rb +941 -0
  55. data/test/test_helper.rb +4 -0
  56. data/test/unit/assertions_test.rb +40 -0
  57. data/test/unit/callback_test.rb +455 -0
  58. data/test/unit/condition_proxy_test.rb +328 -0
  59. data/test/unit/eval_helpers_test.rb +120 -0
  60. data/test/unit/event_collection_test.rb +326 -0
  61. data/test/unit/event_test.rb +743 -0
  62. data/test/unit/guard_test.rb +908 -0
  63. data/test/unit/integrations/active_record_test.rb +1374 -0
  64. data/test/unit/integrations/data_mapper_test.rb +962 -0
  65. data/test/unit/integrations/sequel_test.rb +859 -0
  66. data/test/unit/integrations_test.rb +42 -0
  67. data/test/unit/invalid_event_test.rb +7 -0
  68. data/test/unit/invalid_transition_test.rb +7 -0
  69. data/test/unit/machine_collection_test.rb +938 -0
  70. data/test/unit/machine_test.rb +2004 -0
  71. data/test/unit/matcher_helpers_test.rb +37 -0
  72. data/test/unit/matcher_test.rb +155 -0
  73. data/test/unit/node_collection_test.rb +207 -0
  74. data/test/unit/state_collection_test.rb +280 -0
  75. data/test/unit/state_machine_test.rb +31 -0
  76. data/test/unit/state_test.rb +795 -0
  77. data/test/unit/transition_test.rb +1212 -0
  78. metadata +163 -0
@@ -0,0 +1,122 @@
1
+ module StateMachine
2
+ # Represents a collection of events in a state machine
3
+ class EventCollection < NodeCollection
4
+ def initialize(machine) #:nodoc:
5
+ super(machine, :index => [:name, :qualified_name])
6
+ end
7
+
8
+ # Gets the list of events that can be fired on the given object.
9
+ #
10
+ # == Examples
11
+ #
12
+ # class Vehicle
13
+ # state_machine :initial => :parked do
14
+ # event :park do
15
+ # transition :idling => :parked
16
+ # end
17
+ #
18
+ # event :ignite do
19
+ # transition :parked => :idling
20
+ # end
21
+ # end
22
+ # end
23
+ #
24
+ # events = Vehicle.state_machine(:state).events
25
+ #
26
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
27
+ # events.valid_for(vehicle) # => [#<StateMachine::Event name=:ignite transitions=[:parked => :idling]>]
28
+ #
29
+ # vehicle.state = 'idling'
30
+ # events.valid_for(vehicle) # => [#<StateMachine::Event name=:park transitions=[:idling => :parked]>]
31
+ def valid_for(object)
32
+ select {|event| event.can_fire?(object)}
33
+ end
34
+
35
+ # Gets the list of transitions that can be run on the given object.
36
+ #
37
+ # Valid requirement options:
38
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
39
+ # are specified, then this will be the object's current state.
40
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
41
+ # specified, then this will match any to state.
42
+ # * <tt>:on</tt> - One or more events that fire the transition. If none
43
+ # are specified, then this will match any event.
44
+ #
45
+ # == Examples
46
+ #
47
+ # class Vehicle
48
+ # state_machine :initial => :parked do
49
+ # event :park do
50
+ # transition :idling => :parked
51
+ # end
52
+ #
53
+ # event :ignite do
54
+ # transition :parked => :idling
55
+ # end
56
+ # end
57
+ # end
58
+ #
59
+ # events = Vehicle.state_machine.events
60
+ #
61
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
62
+ # events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
63
+ #
64
+ # vehicle.state = 'idling'
65
+ # events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
66
+ #
67
+ # # Search for explicit transitions regardless of the current state
68
+ # events.transitions_for(vehicle, :from => :parked) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
69
+ def transitions_for(object, requirements = {})
70
+ map {|event| event.transition_for(object, requirements)}.compact
71
+ end
72
+
73
+ # Gets the transition that should be performed for the event stored in the
74
+ # given object's event attribute. This also takes an additional parameter
75
+ # for automatically invalidating the object if the event or transition are
76
+ # invalid. By default, this is turned off.
77
+ #
78
+ # *Note* that if a transition has already been generated for the event, then
79
+ # that transition will be used.
80
+ #
81
+ # == Examples
82
+ #
83
+ # class Vehicle < ActiveRecord::Base
84
+ # state_machine :initial => :parked do
85
+ # event :ignite do
86
+ # transition :parked => :idling
87
+ # end
88
+ # end
89
+ # end
90
+ #
91
+ # vehicle = Vehicle.new # => #<Vehicle id: nil, state: "parked">
92
+ # events = Vehicle.state_machine.events
93
+ #
94
+ # vehicle.state_event = nil
95
+ # events.attribute_transition_for(vehicle) # => nil # Event isn't defined
96
+ #
97
+ # vehicle.state_event = 'invalid'
98
+ # events.attribute_transition_for(vehicle) # => false # Event is invalid
99
+ #
100
+ # vehicle.state_event = 'ignite'
101
+ # events.attribute_transition_for(vehicle) # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
102
+ def attribute_transition_for(object, invalidate = false)
103
+ return unless machine.action
104
+
105
+ result = machine.read(object, :event_transition) || if event_name = machine.read(object, :event)
106
+ if event = self[event_name.to_sym, :name]
107
+ event.transition_for(object) || begin
108
+ # No valid transition: invalidate
109
+ machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).name || 'nil']]) if invalidate
110
+ false
111
+ end
112
+ else
113
+ # Event is unknown: invalidate
114
+ machine.invalidate(object, :event, :invalid) if invalidate
115
+ false
116
+ end
117
+ end
118
+
119
+ result
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,149 @@
1
+ require 'state_machine/machine_collection'
2
+
3
+ module StateMachine
4
+ module ClassMethods
5
+ def self.extended(base) #:nodoc:
6
+ base.class_eval do
7
+ @state_machines = MachineCollection.new
8
+ end
9
+ end
10
+
11
+ # Gets the current list of state machines defined for this class. This
12
+ # class-level attribute acts like an inheritable attribute. The attribute
13
+ # is available to each subclass, each having a copy of its superclass's
14
+ # attribute.
15
+ #
16
+ # The hash of state machines maps <tt>:attribute</tt> => +machine+, e.g.
17
+ #
18
+ # Vehicle.state_machines # => {:state => #<StateMachine::Machine:0xb6f6e4a4 ...>}
19
+ def state_machines
20
+ @state_machines ||= superclass.state_machines.dup
21
+ end
22
+ end
23
+
24
+ module InstanceMethods
25
+ # Runs one or more events in parallel. All events will run through the
26
+ # following steps:
27
+ # * Before callbacks
28
+ # * Persist state
29
+ # * Invoke action
30
+ # * After callbacks
31
+ #
32
+ # For example, if two events (for state machines A and B) are run in
33
+ # parallel, the order in which steps are run is:
34
+ # * A - Before transition callbacks
35
+ # * B - Before transition callbacks
36
+ # * A - Persist new state
37
+ # * B - Persist new state
38
+ # * A - Invoke action
39
+ # * B - Invoke action (only if different than A's action)
40
+ # * A - After transition callbacks
41
+ # * B - After transition callbacks
42
+ #
43
+ # *Note* that multiple events on the same state machine / attribute cannot
44
+ # be run in parallel. If this is attempted, an ArgumentError will be
45
+ # raised.
46
+ #
47
+ # == Halting callbacks
48
+ #
49
+ # When running multiple events in parallel, special consideration should
50
+ # be taken with regard to how halting within callbacks affects the flow.
51
+ #
52
+ # For *before* callbacks, any <tt>:halt</tt> error that's thrown will
53
+ # immediately cancel the perform for all transitions. As a result, it's
54
+ # possible for one event's transition to affect the continuation of
55
+ # another.
56
+ #
57
+ # On the other hand, any <tt>:halt</tt> error that's thrown within an
58
+ # *after* callback with only affect that event's transition. Other
59
+ # transitions will continue to run their own callbacks.
60
+ #
61
+ # == Example
62
+ #
63
+ # class Vehicle
64
+ # state_machine :initial => :parked do
65
+ # event :ignite do
66
+ # transition :parked => :idling
67
+ # end
68
+ #
69
+ # event :park do
70
+ # transition :idling => :parked
71
+ # end
72
+ # end
73
+ #
74
+ # state_machine :alarm_state, :namespace => 'alarm', :initial => :on do
75
+ # event :enable do
76
+ # transition all => :active
77
+ # end
78
+ #
79
+ # event :disable do
80
+ # transition all => :off
81
+ # end
82
+ # end
83
+ # end
84
+ #
85
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
86
+ # vehicle.state # => "parked"
87
+ # vehicle.alarm_state # => "active"
88
+ #
89
+ # vehicle.fire_events(:ignite, :disable_alarm) # => true
90
+ # vehicle.state # => "idling"
91
+ # vehicle.alarm_state # => "off"
92
+ #
93
+ # # If any event fails, the entire event chain fails
94
+ # vehicle.fire_events(:ignite, :enable_alarm) # => false
95
+ # vehicle.state # => "idling"
96
+ # vehicle.alarm_state # => "off"
97
+ #
98
+ # # Exception raised on invalid event
99
+ # vehicle.fire_events(:park, :invalid) # => StateMachine::InvalidEvent: :invalid is an unknown event
100
+ # vehicle.state # => "idling"
101
+ # vehicle.alarm_state # => "off"
102
+ def fire_events(*events)
103
+ self.class.state_machines.fire_events(self, *events)
104
+ end
105
+
106
+ # Run one or more events in parallel. If any event fails to run, then
107
+ # a StateMachine::InvalidTransition exception will be raised.
108
+ #
109
+ # See StateMachine::InstanceMethods#fire_events for more information.
110
+ #
111
+ # == Example
112
+ #
113
+ # class Vehicle
114
+ # state_machine :initial => :parked do
115
+ # event :ignite do
116
+ # transition :parked => :idling
117
+ # end
118
+ #
119
+ # event :park do
120
+ # transition :idling => :parked
121
+ # end
122
+ # end
123
+ #
124
+ # state_machine :alarm_state, :namespace => 'alarm', :initial => :active do
125
+ # event :enable do
126
+ # transition all => :active
127
+ # end
128
+ #
129
+ # event :disable do
130
+ # transition all => :off
131
+ # end
132
+ # end
133
+ # end
134
+ #
135
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
136
+ # vehicle.fire_events(:ignite, :disable_alarm) # => true
137
+ #
138
+ # vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachine::InvalidTranstion: Cannot run events in parallel: ignite, disable_alarm
139
+ def fire_events!(*events)
140
+ run_action = [true, false].include?(events.last) ? events.pop : true
141
+ fire_events(*(events + [run_action])) || raise(StateMachine::InvalidTransition, "Cannot run events in parallel: #{events * ', '}")
142
+ end
143
+
144
+ protected
145
+ def initialize_state_machines(options = {}) #:nodoc:
146
+ self.class.state_machines.initialize_states(self, options)
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,230 @@
1
+ require 'state_machine/matcher'
2
+ require 'state_machine/eval_helpers'
3
+ require 'state_machine/assertions'
4
+
5
+ module StateMachine
6
+ # Represents a set of requirements that must be met in order for a transition
7
+ # or callback to occur. Guards verify that the event, from state, and to
8
+ # state of the transition match, in addition to if/unless conditionals for
9
+ # an object's state.
10
+ class Guard
11
+ include Assertions
12
+ include EvalHelpers
13
+
14
+ # The condition that must be met on an object
15
+ attr_reader :if_condition
16
+
17
+ # The condition that must *not* be met on an object
18
+ attr_reader :unless_condition
19
+
20
+ # The requirement for verifying the event being guarded
21
+ attr_reader :event_requirement
22
+
23
+ # One or more requirements for verifying the states being guarded. All
24
+ # requirements contain a mapping of {:from => matcher, :to => matcher}.
25
+ attr_reader :state_requirements
26
+
27
+ # The requirement for verifying the success of the event
28
+ attr_reader :success_requirement
29
+
30
+ # A list of all of the states known to this guard. This will pull states
31
+ # from the following options (in the same order):
32
+ # * +from+ / +except_from+
33
+ # * +to+ / +except_to+
34
+ attr_reader :known_states
35
+
36
+ # Creates a new guard
37
+ def initialize(options = {}) #:nodoc:
38
+ # Build conditionals
39
+ @if_condition = options.delete(:if)
40
+ @unless_condition = options.delete(:unless)
41
+
42
+ # Build event requirement
43
+ @event_requirement = build_matcher(options, :on, :except_on)
44
+
45
+ # Build success requirement
46
+ @success_requirement = options.delete(:include_failures) ? AllMatcher.instance : WhitelistMatcher.new([true])
47
+
48
+ if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on]).empty?
49
+ # Explicit from/to requirements specified
50
+ @state_requirements = [{:from => build_matcher(options, :from, :except_from), :to => build_matcher(options, :to, :except_to)}]
51
+ else
52
+ # Separate out the event requirement
53
+ options.delete(:on)
54
+ options.delete(:except_on)
55
+
56
+ # Implicit from/to requirements specified
57
+ @state_requirements = options.collect do |from, to|
58
+ from = WhitelistMatcher.new(from) unless from.is_a?(Matcher)
59
+ to = WhitelistMatcher.new(to) unless to.is_a?(Matcher)
60
+ {:from => from, :to => to}
61
+ end
62
+ end
63
+
64
+ # Track known states. The order that requirements are iterated is based
65
+ # on the priority in which tracked states should be added.
66
+ @known_states = []
67
+ @state_requirements.each do |state_requirement|
68
+ [:from, :to].each {|option| @known_states |= state_requirement[option].values}
69
+ end
70
+ end
71
+
72
+ # Determines whether the given object / query matches the requirements
73
+ # configured for this guard. In addition to matching the event, from state,
74
+ # and to state, this will also check whether the configured :if/:unless
75
+ # conditions pass on the given object.
76
+ #
77
+ # == Examples
78
+ #
79
+ # guard = StateMachine::Guard.new(:parked => :idling, :on => :ignite)
80
+ #
81
+ # # Successful
82
+ # guard.matches?(object, :on => :ignite) # => true
83
+ # guard.matches?(object, :from => nil) # => true
84
+ # guard.matches?(object, :from => :parked) # => true
85
+ # guard.matches?(object, :to => :idling) # => true
86
+ # guard.matches?(object, :from => :parked, :to => :idling) # => true
87
+ # guard.matches?(object, :on => :ignite, :from => :parked, :to => :idling) # => true
88
+ #
89
+ # # Unsuccessful
90
+ # guard.matches?(object, :on => :park) # => false
91
+ # guard.matches?(object, :from => :idling) # => false
92
+ # guard.matches?(object, :to => :first_gear) # => false
93
+ # guard.matches?(object, :from => :parked, :to => :first_gear) # => false
94
+ # guard.matches?(object, :on => :park, :from => :parked, :to => :idling) # => false
95
+ def matches?(object, query = {})
96
+ !match(object, query).nil?
97
+ end
98
+
99
+ # Attempts to match the given object / query against the set of requirements
100
+ # configured for this guard. In addition to matching the event, from state,
101
+ # and to state, this will also check whether the configured :if/:unless
102
+ # conditions pass on the given object.
103
+ #
104
+ # If a match is found, then the event/state requirements that the query
105
+ # passed successfully will be returned. Otherwise, nil is returned if there
106
+ # was no match.
107
+ #
108
+ # Query options:
109
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
110
+ # are specified, then this will always match.
111
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
112
+ # specified, then this will always match.
113
+ # * <tt>:on</tt> - One or more events that fired the transition. If none
114
+ # are specified, then this will always match.
115
+ #
116
+ # == Examples
117
+ #
118
+ # guard = StateMachine::Guard.new(:parked => :idling, :on => :ignite)
119
+ #
120
+ # guard.match(object, :on => :ignite) # => {:to => ..., :from => ..., :on => ...}
121
+ # guard.match(object, :on => :park) # => nil
122
+ def match(object, query = {})
123
+ if (match = match_query(query)) && matches_conditions?(object)
124
+ match
125
+ end
126
+ end
127
+
128
+ # Draws a representation of this guard on the given graph. This will draw
129
+ # an edge between every state this guard matches *from* to either the
130
+ # configured to state or, if none specified, then a loopback to the from
131
+ # state.
132
+ #
133
+ # For example, if the following from states are configured:
134
+ # * +idling+
135
+ # * +first_gear+
136
+ # * +backing_up+
137
+ #
138
+ # ...and the to state is +parked+, then the following edges will be created:
139
+ # * +idling+ -> +parked+
140
+ # * +first_gear+ -> +parked+
141
+ # * +backing_up+ -> +parked+
142
+ #
143
+ # Each edge will be labeled with the name of the event that would cause the
144
+ # transition.
145
+ #
146
+ # The collection of edges generated on the graph will be returned.
147
+ def draw(graph, event, valid_states)
148
+ state_requirements.inject([]) do |edges, state_requirement|
149
+ # From states determined based on the known valid states
150
+ from_states = state_requirement[:from].filter(valid_states)
151
+
152
+ # If a to state is not specified, then it's a loopback and each from
153
+ # state maps back to itself
154
+ if state_requirement[:to].values.empty?
155
+ loopback = true
156
+ else
157
+ to_state = state_requirement[:to].values.first
158
+ to_state = to_state ? to_state.to_s : 'nil'
159
+ loopback = false
160
+ end
161
+
162
+ # Generate an edge between each from and to state
163
+ from_states.each do |from_state|
164
+ from_state = from_state ? from_state.to_s : 'nil'
165
+ edges << graph.add_edge(from_state, loopback ? from_state : to_state, :label => event.to_s)
166
+ end
167
+
168
+ edges
169
+ end
170
+ end
171
+
172
+ protected
173
+ # Builds a matcher strategy to use for the given options. If neither a
174
+ # whitelist nor a blacklist option is specified, then an AllMatcher is
175
+ # built.
176
+ def build_matcher(options, whitelist_option, blacklist_option)
177
+ assert_exclusive_keys(options, whitelist_option, blacklist_option)
178
+
179
+ if options.include?(whitelist_option)
180
+ WhitelistMatcher.new(options[whitelist_option])
181
+ elsif options.include?(blacklist_option)
182
+ BlacklistMatcher.new(options[blacklist_option])
183
+ else
184
+ AllMatcher.instance
185
+ end
186
+ end
187
+
188
+ # Verifies that all configured requirements (event and state) match the
189
+ # given query. If a match is found, then a hash containing the
190
+ # event/state requirements that passed will be returned; otherwise, nil.
191
+ def match_query(query)
192
+ query ||= {}
193
+
194
+ if match_success(query) && match_event(query) && (state_requirement = match_states(query))
195
+ state_requirement.merge(:on => event_requirement)
196
+ end
197
+ end
198
+
199
+ # Verifies that the success requirement matches the given query
200
+ def match_success(query)
201
+ matches_requirement?(query, :success, success_requirement)
202
+ end
203
+
204
+ # Verifies that the event requirement matches the given query
205
+ def match_event(query)
206
+ matches_requirement?(query, :on, event_requirement)
207
+ end
208
+
209
+ # Verifies that the state requirements match the given query. If a
210
+ # matching requirement is found, then it is returned.
211
+ def match_states(query)
212
+ state_requirements.detect do |state_requirement|
213
+ [:from, :to].all? {|option| matches_requirement?(query, option, state_requirement[option])}
214
+ end
215
+ end
216
+
217
+ # Verifies that an option in the given query matches the values required
218
+ # for that option
219
+ def matches_requirement?(query, option, requirement)
220
+ !query.include?(option) || requirement.matches?(query[option], query)
221
+ end
222
+
223
+ # Verifies that the conditionals for this guard evaluate to true for the
224
+ # given object
225
+ def matches_conditions?(object)
226
+ Array(if_condition).all? {|condition| evaluate_method(object, condition)} &&
227
+ !Array(unless_condition).any? {|condition| evaluate_method(object, condition)}
228
+ end
229
+ end
230
+ end