state_machines-activemodel 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +14 -10
- data/Appraisals +4 -15
- data/Gemfile +3 -0
- data/LICENSE.txt +1 -1
- data/README.md +9 -6
- data/Rakefile +5 -6
- data/gemfiles/active_model_4.1.gemfile +5 -1
- data/gemfiles/active_model_4.2.gemfile +11 -0
- data/lib/state_machines/integrations/active_model.rb +105 -108
- data/lib/state_machines/integrations/active_model/locale.rb +8 -8
- data/lib/state_machines/integrations/{version.rb → active_model/version.rb} +1 -1
- data/state_machines-activemodel.gemspec +10 -9
- data/{spec/support → test/files}/en.yml +1 -1
- data/test/integration_test.rb +23 -0
- data/test/machine_by_default_test.rb +24 -0
- data/test/machine_errors_test.rb +19 -0
- data/test/machine_multiple_test.rb +18 -0
- data/test/machine_with_callbacks_test.rb +130 -0
- data/test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb +26 -0
- data/test/machine_with_dirty_attribute_and_state_events_test.rb +23 -0
- data/test/machine_with_dirty_attributes_and_custom_attribute_test.rb +34 -0
- data/test/machine_with_dirty_attributes_during_loopback_test.rb +49 -0
- data/test/machine_with_dirty_attributes_test.rb +33 -0
- data/test/machine_with_dynamic_initial_state_test.rb +14 -0
- data/test/machine_with_events_test.rb +13 -0
- data/test/machine_with_failed_after_callbacks_test.rb +31 -0
- data/test/machine_with_failed_before_callbacks_test.rb +32 -0
- data/test/machine_with_initialized_state_test.rb +35 -0
- data/test/machine_with_internationalization_test.rb +190 -0
- data/test/machine_with_model_state_attribute_test.rb +31 -0
- data/test/machine_with_non_model_state_attribute_undefined_test.rb +26 -0
- data/test/machine_with_state_driven_validations_test.rb +31 -0
- data/test/machine_with_states_test.rb +13 -0
- data/test/machine_with_static_initial_state_test.rb +13 -0
- data/test/machine_with_validations_and_custom_attribute_test.rb +22 -0
- data/test/machine_with_validations_test.rb +46 -0
- data/test/test_helper.rb +58 -0
- metadata +83 -36
- data/gemfiles/active_model_3.2.gemfile +0 -7
- data/gemfiles/active_model_3.2.gemfile.lock +0 -50
- data/gemfiles/active_model_4.0.gemfile +0 -7
- data/gemfiles/active_model_4.0.gemfile.lock +0 -56
- data/gemfiles/active_model_4.1.gemfile.lock +0 -57
- data/gemfiles/active_model_edge.gemfile +0 -7
- data/gemfiles/active_model_edge.gemfile.lock +0 -62
- data/spec/active_model_spec.rb +0 -801
- data/spec/integration_spec.rb +0 -26
- data/spec/spec_helper.rb +0 -8
- data/spec/support/helpers.rb +0 -48
- data/spec/support/migration_helpers.rb +0 -43
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class MachineWithDynamicInitialStateTest < BaseTestCase
|
4
|
+
def setup
|
5
|
+
@model = new_model
|
6
|
+
@machine = StateMachines::Machine.new(@model, initial: lambda { |_object| :parked }, integration: :active_model)
|
7
|
+
@machine.state :parked
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_should_set_initial_state_on_created_object
|
11
|
+
record = @model.new
|
12
|
+
assert_equal 'parked', record.state
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class MachineWithEventsTest < BaseTestCase
|
4
|
+
def setup
|
5
|
+
@model = new_model
|
6
|
+
@machine = StateMachines::Machine.new(@model)
|
7
|
+
@machine.event :shift_up
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_should_humanize_name
|
11
|
+
assert_equal 'shift up', @machine.event(:shift_up).human_name
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class MachineWithFailedAfterCallbacksTest < BaseTestCase
|
4
|
+
def setup
|
5
|
+
@callbacks = []
|
6
|
+
|
7
|
+
@model = new_model
|
8
|
+
@machine = StateMachines::Machine.new(@model, integration: :active_model)
|
9
|
+
@machine.state :parked, :idling
|
10
|
+
@machine.event :ignite
|
11
|
+
@machine.after_transition { @callbacks << :after_1; false }
|
12
|
+
@machine.after_transition { @callbacks << :after_2 }
|
13
|
+
@machine.around_transition { |block| @callbacks << :around_before; block.call; @callbacks << :around_after }
|
14
|
+
|
15
|
+
@record = @model.new(state: 'parked')
|
16
|
+
@transition = StateMachines::Transition.new(@record, @machine, :ignite, :parked, :idling)
|
17
|
+
@result = @transition.perform
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_should_be_successful
|
21
|
+
assert @result
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_should_change_current_state
|
25
|
+
assert_equal 'idling', @record.state
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_should_not_run_further_after_callbacks
|
29
|
+
assert_equal [:around_before, :around_after, :after_1], @callbacks
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class MachineWithFailedBeforeCallbacksTest < BaseTestCase
|
4
|
+
def setup
|
5
|
+
@callbacks = []
|
6
|
+
|
7
|
+
@model = new_model
|
8
|
+
@machine = StateMachines::Machine.new(@model, integration: :active_model)
|
9
|
+
@machine.state :parked, :idling
|
10
|
+
@machine.event :ignite
|
11
|
+
@machine.before_transition { @callbacks << :before_1; false }
|
12
|
+
@machine.before_transition { @callbacks << :before_2 }
|
13
|
+
@machine.after_transition { @callbacks << :after }
|
14
|
+
@machine.around_transition { |block| @callbacks << :around_before; block.call; @callbacks << :around_after }
|
15
|
+
|
16
|
+
@record = @model.new(state: 'parked')
|
17
|
+
@transition = StateMachines::Transition.new(@record, @machine, :ignite, :parked, :idling)
|
18
|
+
@result = @transition.perform
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_should_not_be_successful
|
22
|
+
assert !@result
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_should_not_change_current_state
|
26
|
+
assert_equal 'parked', @record.state
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_should_not_run_further_callbacks
|
30
|
+
assert_equal [:before_1], @callbacks
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class MachineWithInitializedStateTest < BaseTestCase
|
4
|
+
def setup
|
5
|
+
@model = new_model
|
6
|
+
@machine = StateMachines::Machine.new(@model, initial: :parked, integration: :active_model)
|
7
|
+
@machine.state :idling
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_should_allow_nil_initial_state_when_static
|
11
|
+
@machine.state nil
|
12
|
+
|
13
|
+
record = @model.new(state: nil)
|
14
|
+
assert_nil record.state
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_should_allow_nil_initial_state_when_dynamic
|
18
|
+
@machine.state nil
|
19
|
+
|
20
|
+
@machine.initial_state = lambda { :parked }
|
21
|
+
record = @model.new(state: nil)
|
22
|
+
assert_nil record.state
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_should_allow_different_initial_state_when_static
|
26
|
+
record = @model.new(state: 'idling')
|
27
|
+
assert_equal 'idling', record.state
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_should_allow_different_initial_state_when_dynamic
|
31
|
+
@machine.initial_state = lambda { :parked }
|
32
|
+
record = @model.new(state: 'idling')
|
33
|
+
assert_equal 'idling', record.state
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require 'i18n'
|
3
|
+
|
4
|
+
class MachineWithInternationalizationTest < BaseTestCase
|
5
|
+
def setup
|
6
|
+
I18n.backend = I18n::Backend::Simple.new
|
7
|
+
# Initialize the backend
|
8
|
+
I18n.backend.translate(:en, 'activemodel.errors.messages.invalid_transition', event: 'ignite', value: 'idling')
|
9
|
+
|
10
|
+
@model = new_model { include ActiveModel::Validations }
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_should_use_defaults
|
14
|
+
I18n.backend.store_translations(:en,
|
15
|
+
activemodel: { errors: { messages: { invalid_transition: 'cannot %{event}' } } }
|
16
|
+
)
|
17
|
+
|
18
|
+
machine = StateMachines::Machine.new(@model, action: :save)
|
19
|
+
machine.state :parked, :idling
|
20
|
+
machine.event :ignite
|
21
|
+
|
22
|
+
record = @model.new(state: 'idling')
|
23
|
+
|
24
|
+
machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
|
25
|
+
assert_equal ['State cannot ignite'], record.errors.full_messages
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_should_allow_customized_error_key
|
29
|
+
I18n.backend.store_translations(:en,
|
30
|
+
activemodel: { errors: { messages: { bad_transition: 'cannot %{event}' } } }
|
31
|
+
)
|
32
|
+
|
33
|
+
machine = StateMachines::Machine.new(@model, action: :save, messages: { invalid_transition: :bad_transition })
|
34
|
+
machine.state :parked, :idling
|
35
|
+
|
36
|
+
record = @model.new
|
37
|
+
record.state = 'idling'
|
38
|
+
|
39
|
+
machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
|
40
|
+
assert_equal ['State cannot ignite'], record.errors.full_messages
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_should_allow_customized_error_string
|
44
|
+
machine = StateMachines::Machine.new(@model, action: :save, messages: { invalid_transition: 'cannot %{event}' })
|
45
|
+
machine.state :parked, :idling
|
46
|
+
|
47
|
+
record = @model.new(state: 'idling')
|
48
|
+
|
49
|
+
machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
|
50
|
+
assert_equal ['State cannot ignite'], record.errors.full_messages
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_should_allow_customized_state_key_scoped_to_class_and_machine
|
54
|
+
I18n.backend.store_translations(:en,
|
55
|
+
activemodel: { state_machines: { :'foo' => { state: { states: { parked: 'shutdown' } } } } }
|
56
|
+
)
|
57
|
+
|
58
|
+
machine = StateMachines::Machine.new(@model)
|
59
|
+
machine.state :parked
|
60
|
+
assert_equal 'shutdown', machine.state(:parked).human_name
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_should_allow_customized_state_key_scoped_to_class
|
64
|
+
I18n.backend.store_translations(:en,
|
65
|
+
activemodel: { state_machines: { :'foo' => { states: { parked: 'shutdown' } } } }
|
66
|
+
)
|
67
|
+
|
68
|
+
machine = StateMachines::Machine.new(@model)
|
69
|
+
machine.state :parked
|
70
|
+
|
71
|
+
assert_equal 'shutdown', machine.state(:parked).human_name
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_should_allow_customized_state_key_scoped_to_machine
|
75
|
+
I18n.backend.store_translations(:en,
|
76
|
+
activemodel: { state_machines: { state: { states: { parked: 'shutdown' } } } }
|
77
|
+
)
|
78
|
+
|
79
|
+
machine = StateMachines::Machine.new(@model)
|
80
|
+
machine.state :parked
|
81
|
+
|
82
|
+
assert_equal 'shutdown', machine.state(:parked).human_name
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_should_allow_customized_state_key_unscoped
|
86
|
+
I18n.backend.store_translations(:en,
|
87
|
+
activemodel: { state_machines: { states: { parked: 'shutdown' } } }
|
88
|
+
)
|
89
|
+
|
90
|
+
machine = StateMachines::Machine.new(@model)
|
91
|
+
machine.state :parked
|
92
|
+
|
93
|
+
assert_equal 'shutdown', machine.state(:parked).human_name
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_should_support_nil_state_key
|
97
|
+
I18n.backend.store_translations(:en,
|
98
|
+
activemodel: { state_machines: { states: { nil: 'empty' } } }
|
99
|
+
)
|
100
|
+
|
101
|
+
machine = StateMachines::Machine.new(@model)
|
102
|
+
|
103
|
+
assert_equal 'empty', machine.state(nil).human_name
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_should_allow_customized_event_key_scoped_to_class_and_machine
|
107
|
+
I18n.backend.store_translations(:en,
|
108
|
+
activemodel: { state_machines: { :'foo' => { state: { events: { park: 'stop' } } } } }
|
109
|
+
)
|
110
|
+
|
111
|
+
machine = StateMachines::Machine.new(@model)
|
112
|
+
machine.event :park
|
113
|
+
|
114
|
+
assert_equal 'stop', machine.event(:park).human_name
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_should_allow_customized_event_key_scoped_to_class
|
118
|
+
I18n.backend.store_translations(:en,
|
119
|
+
activemodel: { state_machines: { :'foo' => { events: { park: 'stop' } } } }
|
120
|
+
)
|
121
|
+
|
122
|
+
machine = StateMachines::Machine.new(@model)
|
123
|
+
machine.event :park
|
124
|
+
|
125
|
+
assert_equal 'stop', machine.event(:park).human_name
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_should_allow_customized_event_key_scoped_to_machine
|
129
|
+
I18n.backend.store_translations(:en,
|
130
|
+
activemodel: { state_machines: { state: { events: { park: 'stop' } } } }
|
131
|
+
)
|
132
|
+
|
133
|
+
machine = StateMachines::Machine.new(@model)
|
134
|
+
machine.event :park
|
135
|
+
|
136
|
+
assert_equal 'stop', machine.event(:park).human_name
|
137
|
+
end
|
138
|
+
|
139
|
+
def test_should_allow_customized_event_key_unscoped
|
140
|
+
I18n.backend.store_translations(:en,
|
141
|
+
activemodel: { state_machines: { events: { park: 'stop' } } }
|
142
|
+
)
|
143
|
+
|
144
|
+
machine = StateMachines::Machine.new(@model)
|
145
|
+
machine.event :park
|
146
|
+
|
147
|
+
assert_equal 'stop', machine.event(:park).human_name
|
148
|
+
end
|
149
|
+
|
150
|
+
def test_should_only_add_locale_once_in_load_path
|
151
|
+
assert_equal 1, I18n.load_path.select { |path| path =~ %r{active_model/locale\.rb$} }.length
|
152
|
+
|
153
|
+
# Create another ActiveModel model that will triger the i18n feature
|
154
|
+
new_model
|
155
|
+
|
156
|
+
assert_equal 1, I18n.load_path.select { |path| path =~ %r{active_model/locale\.rb$} }.length
|
157
|
+
end
|
158
|
+
|
159
|
+
def test_should_add_locale_to_beginning_of_load_path
|
160
|
+
@original_load_path = I18n.load_path
|
161
|
+
I18n.backend = I18n::Backend::Simple.new
|
162
|
+
|
163
|
+
app_locale = File.dirname(__FILE__) + '/files/en.yml'
|
164
|
+
default_locale = File.dirname(__FILE__) + '/../lib/state_machines/integrations/active_model/locale.rb'
|
165
|
+
I18n.load_path = [app_locale]
|
166
|
+
|
167
|
+
StateMachines::Machine.new(@model)
|
168
|
+
|
169
|
+
assert_equal [default_locale, app_locale].map { |path| File.expand_path(path) }, I18n.load_path.map { |path| File.expand_path(path) }
|
170
|
+
ensure
|
171
|
+
I18n.load_path = @original_load_path
|
172
|
+
end
|
173
|
+
|
174
|
+
def test_should_prefer_other_locales_first
|
175
|
+
@original_load_path = I18n.load_path
|
176
|
+
I18n.backend = I18n::Backend::Simple.new
|
177
|
+
I18n.load_path = [File.dirname(__FILE__) + '/files/en.yml']
|
178
|
+
|
179
|
+
machine = StateMachines::Machine.new(@model)
|
180
|
+
machine.state :parked, :idling
|
181
|
+
machine.event :ignite
|
182
|
+
|
183
|
+
record = @model.new(state: 'idling')
|
184
|
+
|
185
|
+
machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
|
186
|
+
assert_equal ['State cannot ignite'], record.errors.full_messages
|
187
|
+
ensure
|
188
|
+
I18n.load_path = @original_load_path
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class MachineWithModelStateAttributeTest < BaseTestCase
|
4
|
+
def setup
|
5
|
+
@model = new_model
|
6
|
+
@machine = StateMachines::Machine.new(@model, initial: :parked, integration: :active_model)
|
7
|
+
@machine.other_states(:idling)
|
8
|
+
|
9
|
+
@record = @model.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_should_have_an_attribute_predicate
|
13
|
+
assert @record.respond_to?(:state?)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_should_raise_exception_for_predicate_without_parameters
|
17
|
+
assert_raises(ArgumentError) { @record.state? }
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_should_return_false_for_predicate_if_does_not_match_current_value
|
21
|
+
assert !@record.state?(:idling)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_should_return_true_for_predicate_if_matches_current_value
|
25
|
+
assert @record.state?(:parked)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_should_raise_exception_for_predicate_if_invalid_state_specified
|
29
|
+
assert_raises(IndexError) { @record.state?(:invalid) }
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class MachineWithNonModelStateAttributeUndefinedTest < BaseTestCase
|
4
|
+
def setup
|
5
|
+
@model = new_model do
|
6
|
+
def initialize
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
@machine = StateMachines::Machine.new(@model, :status, initial: :parked, integration: :active_model)
|
11
|
+
@machine.other_states(:idling)
|
12
|
+
@record = @model.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_should_not_define_a_reader_attribute_for_the_attribute
|
16
|
+
assert !@record.respond_to?(:status)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_should_not_define_a_writer_attribute_for_the_attribute
|
20
|
+
assert !@record.respond_to?(:status=)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_should_define_an_attribute_predicate
|
24
|
+
assert @record.respond_to?(:status?)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class MachineWithStateDrivenValidationsTest < BaseTestCase
|
4
|
+
def setup
|
5
|
+
@model = new_model do
|
6
|
+
include ActiveModel::Validations
|
7
|
+
attr_accessor :seatbelt
|
8
|
+
end
|
9
|
+
|
10
|
+
@machine = StateMachines::Machine.new(@model)
|
11
|
+
@machine.state :first_gear, :second_gear do
|
12
|
+
validates_presence_of :seatbelt
|
13
|
+
end
|
14
|
+
@machine.other_states :parked
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_should_be_valid_if_validation_fails_outside_state_scope
|
18
|
+
record = @model.new(state: 'parked', seatbelt: nil)
|
19
|
+
assert record.valid?
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_should_be_invalid_if_validation_fails_within_state_scope
|
23
|
+
record = @model.new(state: 'first_gear', seatbelt: nil)
|
24
|
+
assert !record.valid?
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_should_be_valid_if_validation_succeeds_within_state_scope
|
28
|
+
record = @model.new(state: 'second_gear', seatbelt: true)
|
29
|
+
assert record.valid?
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class MachineWithStatesTest < BaseTestCase
|
4
|
+
def setup
|
5
|
+
@model = new_model
|
6
|
+
@machine = StateMachines::Machine.new(@model)
|
7
|
+
@machine.state :first_gear
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_should_humanize_name
|
11
|
+
assert_equal 'first gear', @machine.state(:first_gear).human_name
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class MachineWithStaticInitialStateTest < BaseTestCase
|
4
|
+
def setup
|
5
|
+
@model = new_model
|
6
|
+
@machine = StateMachines::Machine.new(@model, initial: :parked, integration: :active_model)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_should_set_initial_state_on_created_object
|
10
|
+
record = @model.new
|
11
|
+
assert_equal 'parked', record.state
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
|
4
|
+
def setup
|
5
|
+
@model = new_model { include ActiveModel::Validations }
|
6
|
+
|
7
|
+
@machine = StateMachines::Machine.new(@model, :status, attribute: :state)
|
8
|
+
@machine.state :parked
|
9
|
+
|
10
|
+
@record = @model.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_should_add_validation_errors_to_custom_attribute
|
14
|
+
@record.state = 'invalid'
|
15
|
+
|
16
|
+
assert !@record.valid?
|
17
|
+
assert_equal ['State is invalid'], @record.errors.full_messages
|
18
|
+
|
19
|
+
@record.state = 'parked'
|
20
|
+
assert @record.valid?
|
21
|
+
end
|
22
|
+
end
|