statesman 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -1
- data/.travis.yml +19 -4
- data/CHANGELOG.md +30 -0
- data/README.md +11 -35
- data/lib/generators/statesman/add_constraints_to_most_recent_generator.rb +28 -0
- data/lib/generators/statesman/add_most_recent_generator.rb +25 -0
- data/lib/generators/statesman/generator_helpers.rb +6 -2
- data/lib/generators/statesman/templates/add_constraints_to_most_recent_migration.rb.erb +13 -0
- data/lib/generators/statesman/templates/add_most_recent_migration.rb.erb +9 -0
- data/lib/generators/statesman/templates/create_migration.rb.erb +4 -3
- data/lib/generators/statesman/templates/update_migration.rb.erb +5 -4
- data/lib/statesman.rb +1 -0
- data/lib/statesman/adapters/active_record.rb +23 -8
- data/lib/statesman/adapters/active_record_queries.rb +62 -15
- data/lib/statesman/railtie.rb +9 -0
- data/lib/statesman/version.rb +1 -1
- data/lib/tasks/statesman.rake +49 -0
- data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions.rb +13 -0
- data/spec/fixtures/add_most_recent_to_bacon_transitions.rb +9 -0
- data/spec/generators/statesman/active_record_transition_generator_spec.rb +0 -2
- data/spec/generators/statesman/add_constraints_to_most_recent_generator_spec.rb +38 -0
- data/spec/generators/statesman/add_most_recent_generator_spec.rb +35 -0
- data/spec/generators/statesman/migration_generator_spec.rb +10 -1
- data/spec/generators/statesman/mongoid_transition_generator_spec.rb +0 -2
- data/spec/spec_helper.rb +22 -7
- data/spec/statesman/adapters/active_record_queries_spec.rb +110 -28
- data/spec/statesman/adapters/active_record_spec.rb +61 -31
- data/spec/statesman/adapters/mongoid_spec.rb +8 -17
- data/spec/statesman/adapters/shared_examples.rb +10 -17
- data/spec/statesman/callback_spec.rb +2 -6
- data/spec/statesman/config_spec.rb +2 -5
- data/spec/statesman/guard_spec.rb +3 -9
- data/spec/statesman/machine_spec.rb +91 -129
- data/spec/support/active_record.rb +35 -4
- data/spec/support/generators_shared_examples.rb +1 -4
- data/statesman.gemspec +5 -3
- metadata +52 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7adec92ffe517ecf4227b1190dcf1289abce8fe
|
4
|
+
data.tar.gz: 2f5876f6d294df4f2d52272c473d5c4521884d1d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e9be2c9b771cbd4f2d19b7de27854f047659c33deb6907af3b38bb8130feda521f9faf22ffe7560dc01fed63390ab569aae6b8fe3469e1d4159efba430ba428
|
7
|
+
data.tar.gz: e4e509269a3fd7adcfc3cfa15be27cef5d5e36e8e04fea8acf3c1fb6cef213ed3d91174c0efee5985eb691ec15ab59d105579769e651b4e1ca1d6546eb2cdc99
|
data/.rubocop.yml
CHANGED
@@ -4,8 +4,11 @@ AllCops:
|
|
4
4
|
Include:
|
5
5
|
- Rakefile
|
6
6
|
- statesman.gemfile
|
7
|
+
- lib/tasks/*.rake
|
7
8
|
Exclude:
|
8
|
-
- vendor
|
9
|
+
- vendor/**/*
|
10
|
+
- .*/**
|
11
|
+
- spec/fixtures/**/*
|
9
12
|
|
10
13
|
StringLiterals:
|
11
14
|
Enabled: false
|
@@ -36,3 +39,6 @@ GuardClause:
|
|
36
39
|
|
37
40
|
SingleSpaceBeforeFirstArg:
|
38
41
|
Enabled: false
|
42
|
+
|
43
|
+
Style/DotPosition:
|
44
|
+
EnforcedStyle: 'trailing'
|
data/.travis.yml
CHANGED
@@ -1,11 +1,26 @@
|
|
1
|
+
language: ruby
|
2
|
+
|
1
3
|
rvm:
|
2
4
|
- 2.1
|
3
5
|
- 2.0.0
|
4
6
|
- 1.9.3
|
7
|
+
|
8
|
+
sudo: false
|
9
|
+
|
5
10
|
services: mongodb
|
11
|
+
|
12
|
+
before_script:
|
13
|
+
- mysql -e 'CREATE DATABASE statesman_test;'
|
14
|
+
- psql -c 'CREATE DATABASE statesman_test;' -U postgres
|
15
|
+
|
6
16
|
script:
|
7
|
-
|
8
|
-
|
17
|
+
- bundle exec rubocop
|
18
|
+
- bundle exec rake
|
19
|
+
|
9
20
|
env:
|
10
|
-
- "RAILS_VERSION=
|
11
|
-
- "RAILS_VERSION=4.
|
21
|
+
- "RAILS_VERSION=3.2.21"
|
22
|
+
- "RAILS_VERSION=4.0.12"
|
23
|
+
- "RAILS_VERSION=4.1.8"
|
24
|
+
- "RAILS_VERSION=4.2.0"
|
25
|
+
- "RAILS_VERSION=4.2.0 DATABASE_URL=mysql2://root@localhost/statesman_test"
|
26
|
+
- "RAILS_VERSION=4.2.0 DATABASE_URL=postgres://postgres@localhost/statesman_test"
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,33 @@
|
|
1
|
+
## v1.2.0 18 March 2015
|
2
|
+
|
3
|
+
*Changes*
|
4
|
+
|
5
|
+
- Add a `most_recent` column to transition tables to greatly speed up queries (ActiveRecord adapter only).
|
6
|
+
- All queries are backwards-compatible, so everything still works without the new column.
|
7
|
+
- The upgrade path is:
|
8
|
+
- Generate and run a migration for adding the column, by running `rails generate statesman:add_most_recent <ParentModel> <TransitionModel>`.
|
9
|
+
- Backfill the `most_recent` column on old records by running `rake statesman:backfill_most_recent[ParentModel] `.
|
10
|
+
- Add constraints and indexes to the transition table that make use of the new field, by running `rails g statesman:add_constraints_to_most_recent <ParentModel> <TransitionModel>`.
|
11
|
+
- The upgrade path has been designed to be zero-downtime, even on large tables. As a result, please note that queries will only use the `most_recent` field after the constraints have been added.
|
12
|
+
- `ActiveRecordQueries.{not_,}in_state` now accepts an array of states.
|
13
|
+
|
14
|
+
|
15
|
+
## v1.1.0 9 December 2014
|
16
|
+
*Fixes*
|
17
|
+
|
18
|
+
- Support for Rails 4.2.0.rc2:
|
19
|
+
- Remove use of serialized_attributes when using 4.2+. (patch by [@greysteil](https://github.com/greysteil))
|
20
|
+
- Use reflect_on_association rather than directly using the reflections hash. (patch by [@timrogers](https://github.com/timrogers))
|
21
|
+
- Fix `ActiveRecordQueries.in_state` when `Model.initial_state` is defined as a symbol. (patch by [@isaacseymour](https://github.com/isaacseymour))
|
22
|
+
|
23
|
+
*Changes*
|
24
|
+
|
25
|
+
- Transition metadata now defaults to `{}` rather than `nil`. (patch by [@greysteil](https://github.com/greysteil))
|
26
|
+
|
27
|
+
## v1.0.0 21 November 2014
|
28
|
+
|
29
|
+
No changes from v1.0.0.beta2
|
30
|
+
|
1
31
|
## v1.0.0.beta2 10 October 2014
|
2
32
|
*Breaking changes*
|
3
33
|
|
data/README.md
CHANGED
@@ -60,7 +60,7 @@ class Order < ActiveRecord::Base
|
|
60
60
|
has_many :order_transitions
|
61
61
|
|
62
62
|
def state_machine
|
63
|
-
OrderStateMachine.new(self, transition_class: OrderTransition)
|
63
|
+
@state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition)
|
64
64
|
end
|
65
65
|
|
66
66
|
private
|
@@ -222,6 +222,13 @@ It is also possible to use the PostgreSQL JSON column if you are using Rails 4.
|
|
222
222
|
* Remove `include Statesman::Adapters::ActiveRecordTransition` statement from your
|
223
223
|
transition model
|
224
224
|
|
225
|
+
#### Creating transitions without using `#transition_to` with ActiveRecord
|
226
|
+
|
227
|
+
By default, Statesman will include a `most_recent` column on the transitions
|
228
|
+
table, and update its value each time `#transition_to` is called. If you create
|
229
|
+
transitions manually (for example to backfill for a new state) you will need to
|
230
|
+
set the `most_recent` attribute manually.
|
231
|
+
|
225
232
|
|
226
233
|
## Configuration
|
227
234
|
|
@@ -289,6 +296,9 @@ model object and transition object are passed as arguments to the callback.
|
|
289
296
|
This callback can have side-effects as it will only be run once immediately
|
290
297
|
after the transition.
|
291
298
|
|
299
|
+
If you specify `after_commit: true`, the callback will be executed once the
|
300
|
+
transition has been committed to the database.
|
301
|
+
|
292
302
|
#### `Machine.new`
|
293
303
|
```ruby
|
294
304
|
my_machine = Machine.new(my_model, transition_class: MyTransitionModel)
|
@@ -423,40 +433,6 @@ describe "some callback" do
|
|
423
433
|
end
|
424
434
|
```
|
425
435
|
|
426
|
-
#### Creating models in certain states
|
427
|
-
|
428
|
-
Sometimes you'll want to test a guard/transition from one state to another, where the state you want to go from is not the initial state of the model. In this instance you'll need to construct a model instance in the state required. However, if you have strict guards, this can be a pain. One way to get around this in tests is to directly create the transitions in the database, hence avoiding the guards.
|
429
|
-
|
430
|
-
We use [FactoryGirl](https://github.com/thoughtbot/factory_girl) for creating our test objects. Given an `Order` model that is backed by Statesman, we can easily set it up to be in a particular state:
|
431
|
-
|
432
|
-
```ruby
|
433
|
-
factory :order do
|
434
|
-
property "value"
|
435
|
-
...
|
436
|
-
|
437
|
-
trait :shipped do
|
438
|
-
after(:create) do |order|
|
439
|
-
FactoryGirl.create(:order_transition, :shipped, order: order)
|
440
|
-
end
|
441
|
-
end
|
442
|
-
end
|
443
|
-
|
444
|
-
factory :order_transition do
|
445
|
-
order
|
446
|
-
...
|
447
|
-
|
448
|
-
trait :shipped do
|
449
|
-
to_state "shipped"
|
450
|
-
end
|
451
|
-
end
|
452
|
-
```
|
453
|
-
|
454
|
-
This means you can easily create an `Order` in the `shipped` state:
|
455
|
-
|
456
|
-
```ruby
|
457
|
-
let(:shipped_order) { FactoryGirl.create(:order, :shipped) }
|
458
|
-
```
|
459
|
-
|
460
436
|
---
|
461
437
|
|
462
438
|
GoCardless ♥ open source. If you do too, come [join us](https://gocardless.com/jobs#software-engineer).
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "generators/statesman/generator_helpers"
|
3
|
+
|
4
|
+
module Statesman
|
5
|
+
class AddConstraintsToMostRecentGenerator < Rails::Generators::Base
|
6
|
+
include Statesman::GeneratorHelpers
|
7
|
+
|
8
|
+
desc "Adds uniqueness and not-null constraints to the most recent column " \
|
9
|
+
"for a statesman transition"
|
10
|
+
|
11
|
+
argument :parent, type: :string, desc: "Your parent model name"
|
12
|
+
argument :klass, type: :string, desc: "Your transition model name"
|
13
|
+
|
14
|
+
source_root File.expand_path('../templates', __FILE__)
|
15
|
+
|
16
|
+
def create_model_file
|
17
|
+
template("add_constraints_to_most_recent_migration.rb.erb",
|
18
|
+
migration_file_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def migration_file_name
|
24
|
+
"db/migrate/#{next_migration_number}_"\
|
25
|
+
"add_constraints_to_most_recent_for_#{table_name}.rb"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "generators/statesman/generator_helpers"
|
3
|
+
|
4
|
+
module Statesman
|
5
|
+
class AddMostRecentGenerator < Rails::Generators::Base
|
6
|
+
include Statesman::GeneratorHelpers
|
7
|
+
|
8
|
+
desc "Adds most_recent to a statesman transition model"
|
9
|
+
|
10
|
+
argument :parent, type: :string, desc: "Your parent model name"
|
11
|
+
argument :klass, type: :string, desc: "Your transition model name"
|
12
|
+
|
13
|
+
source_root File.expand_path('../templates', __FILE__)
|
14
|
+
|
15
|
+
def create_model_file
|
16
|
+
template("add_most_recent_migration.rb.erb", migration_file_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def migration_file_name
|
22
|
+
"db/migrate/#{next_migration_number}_add_most_recent_to_#{table_name}.rb"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -28,9 +28,13 @@ module Statesman
|
|
28
28
|
klass.demodulize.underscore.pluralize
|
29
29
|
end
|
30
30
|
|
31
|
+
def index_name(index_id)
|
32
|
+
"index_#{table_name}_#{index_id}"
|
33
|
+
end
|
34
|
+
|
31
35
|
def mysql?
|
32
|
-
ActiveRecord::Base.configurations[Rails.env]
|
33
|
-
|
36
|
+
ActiveRecord::Base.configurations[Rails.env].
|
37
|
+
try(:[], "adapter").try(:match, /mysql/)
|
34
38
|
end
|
35
39
|
end
|
36
40
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class AddConstraintsToMostRecentFor<%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
disable_ddl_transaction!
|
3
|
+
|
4
|
+
def up
|
5
|
+
add_index :<%= table_name %>, [:<%= parent_id %>, :most_recent], unique: true, where: "most_recent", name: "index_<%= table_name %>_parent_most_recent", algorithm: :concurrently
|
6
|
+
change_column_null :<%= table_name %>, :most_recent, false
|
7
|
+
end
|
8
|
+
|
9
|
+
def down
|
10
|
+
remove_index :<%= table_name %>, name: "index_<%= table_name %>_parent_most_recent"
|
11
|
+
change_column_null :<%= table_name %>, :most_recent, true
|
12
|
+
end
|
13
|
+
end
|
@@ -5,10 +5,11 @@ class Create<%= migration_class_name %> < ActiveRecord::Migration
|
|
5
5
|
t.text :metadata<%= ", default: \"{}\"" unless mysql? %>
|
6
6
|
t.integer :sort_key, null: false
|
7
7
|
t.integer :<%= parent_id %>, null: false
|
8
|
-
t.
|
8
|
+
t.boolean :most_recent, null: false
|
9
|
+
t.timestamps null: false
|
9
10
|
end
|
10
11
|
|
11
|
-
add_index :<%= table_name %>, :<%= parent_id %>
|
12
|
-
add_index :<%= table_name %>, [
|
12
|
+
add_index :<%= table_name %>, [:<%= parent_id %>, :sort_key], unique: true, name: "<%= index_name :parent_sort %>"
|
13
|
+
add_index :<%= table_name %>, [:<%= parent_id %>, :most_recent], unique: true, where: "most_recent", name: "<%= index_name :parent_most_recent %>"
|
13
14
|
end
|
14
15
|
end
|
@@ -4,10 +4,11 @@ class AddStatesmanTo<%= migration_class_name %> < ActiveRecord::Migration
|
|
4
4
|
add_column :<%= table_name %>, :metadata, :text<%= ", default: \"{}\"" unless mysql? %>
|
5
5
|
add_column :<%= table_name %>, :sort_key, :integer, null: false
|
6
6
|
add_column :<%= table_name %>, :<%= parent_id %>, :integer, null: false
|
7
|
-
add_column :<%= table_name %>, :
|
8
|
-
add_column :<%= table_name %>, :
|
7
|
+
add_column :<%= table_name %>, :most_recent, null: false
|
8
|
+
add_column :<%= table_name %>, :created_at, :datetime, null: false
|
9
|
+
add_column :<%= table_name %>, :updated_at, :datetime, null: false
|
9
10
|
|
10
|
-
add_index :<%= table_name %>, :<%= parent_id %>
|
11
|
-
add_index :<%= table_name %>, [
|
11
|
+
add_index :<%= table_name %>, [:<%= parent_id %>, :sort_key], unique: true, name: "<%= index_name :parent_sort %>"
|
12
|
+
add_index :<%= table_name %>, [:<%= parent_id %>, :most_recent], unique: true, where: "most_recent", name: "<%= index_name :parent_most_recent %>"
|
12
13
|
end
|
13
14
|
end
|
data/lib/statesman.rb
CHANGED
@@ -27,8 +27,8 @@ module Statesman
|
|
27
27
|
to = to.to_s
|
28
28
|
create_transition(from, to, metadata)
|
29
29
|
rescue ::ActiveRecord::RecordNotUnique => e
|
30
|
-
if e.message.include?(
|
31
|
-
e.message.include?(
|
30
|
+
if e.message.include?(@transition_class.table_name) &&
|
31
|
+
e.message.include?('sort_key') || e.message.include?('most_recent')
|
32
32
|
raise TransitionConflictError, e.message
|
33
33
|
else raise
|
34
34
|
end
|
@@ -53,11 +53,16 @@ module Statesman
|
|
53
53
|
private
|
54
54
|
|
55
55
|
def create_transition(from, to, metadata)
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
transition_attributes = { to_state: to,
|
57
|
+
sort_key: next_sort_key,
|
58
|
+
metadata: metadata }
|
59
|
+
|
60
|
+
transition_attributes.merge!(most_recent: true) if most_recent_column?
|
61
|
+
|
62
|
+
transition = transitions_for_parent.build(transition_attributes)
|
59
63
|
|
60
64
|
::ActiveRecord::Base.transaction do
|
65
|
+
unset_old_most_recent
|
61
66
|
@observer.execute(:before, from, to, transition)
|
62
67
|
transition.save!
|
63
68
|
@last_transition = transition
|
@@ -72,14 +77,24 @@ module Statesman
|
|
72
77
|
@parent_model.send(@transition_class.table_name)
|
73
78
|
end
|
74
79
|
|
80
|
+
def unset_old_most_recent
|
81
|
+
return unless most_recent_column?
|
82
|
+
transitions_for_parent.update_all(most_recent: false)
|
83
|
+
end
|
84
|
+
|
85
|
+
def most_recent_column?
|
86
|
+
transition_class.columns_hash.include?("most_recent")
|
87
|
+
end
|
88
|
+
|
75
89
|
def next_sort_key
|
76
90
|
(last && last.sort_key + 10) || 0
|
77
91
|
end
|
78
92
|
|
79
93
|
def serialized?(transition_class)
|
80
|
-
if ::ActiveRecord.gem_version
|
81
|
-
|
82
|
-
|
94
|
+
if ::ActiveRecord.respond_to?(:gem_version) &&
|
95
|
+
::ActiveRecord.gem_version >= Gem::Version.new('4.2.0.a')
|
96
|
+
transition_class.columns_hash["metadata"].
|
97
|
+
cast_type.is_a?(::ActiveRecord::Type::Serialized)
|
83
98
|
else
|
84
99
|
transition_class.serialized_attributes.include?("metadata")
|
85
100
|
end
|
@@ -7,25 +7,51 @@ module Statesman
|
|
7
7
|
|
8
8
|
module ClassMethods
|
9
9
|
def in_state(*states)
|
10
|
-
states = states.map(&:to_s)
|
10
|
+
states = states.flatten.map(&:to_s)
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
if use_most_recent_column?
|
13
|
+
in_state_with_most_recent(states)
|
14
|
+
else
|
15
|
+
in_state_without_most_recent(states)
|
16
|
+
end
|
16
17
|
end
|
17
18
|
|
18
19
|
def not_in_state(*states)
|
19
|
-
states = states.map(&:to_s)
|
20
|
+
states = states.flatten.map(&:to_s)
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
if use_most_recent_column?
|
23
|
+
not_in_state_with_most_recent(states)
|
24
|
+
else
|
25
|
+
not_in_state_without_most_recent(states)
|
26
|
+
end
|
25
27
|
end
|
26
28
|
|
27
29
|
private
|
28
30
|
|
31
|
+
def in_state_with_most_recent(states)
|
32
|
+
joins(most_recent_transition_join).
|
33
|
+
where(states_where('last_transition', states), states)
|
34
|
+
end
|
35
|
+
|
36
|
+
def not_in_state_with_most_recent(states)
|
37
|
+
joins(most_recent_transition_join).
|
38
|
+
where("NOT (#{states_where('last_transition', states)})", states)
|
39
|
+
end
|
40
|
+
|
41
|
+
def in_state_without_most_recent(states)
|
42
|
+
joins(transition1_join).
|
43
|
+
joins(transition2_join).
|
44
|
+
where(states_where('transition1', states), states).
|
45
|
+
where("transition2.id" => nil)
|
46
|
+
end
|
47
|
+
|
48
|
+
def not_in_state_without_most_recent(states)
|
49
|
+
joins(transition1_join).
|
50
|
+
joins(transition2_join).
|
51
|
+
where("NOT (#{states_where('transition1', states)})", states).
|
52
|
+
where("transition2.id" => nil)
|
53
|
+
end
|
54
|
+
|
29
55
|
def transition_class
|
30
56
|
raise NotImplementedError, "A transition_class method should be " \
|
31
57
|
"defined on the model"
|
@@ -55,15 +81,36 @@ module Statesman
|
|
55
81
|
AND transition2.sort_key > transition1.sort_key"
|
56
82
|
end
|
57
83
|
|
58
|
-
def
|
84
|
+
def most_recent_transition_join
|
85
|
+
"LEFT OUTER JOIN #{transition_name} AS last_transition
|
86
|
+
ON #{table_name}.id = last_transition.#{model_foreign_key}
|
87
|
+
AND last_transition.most_recent = #{db_true}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def states_where(temporary_table_name, states)
|
59
91
|
if initial_state.to_s.in?(states.map(&:to_s))
|
60
|
-
|
61
|
-
|
92
|
+
"#{temporary_table_name}.to_state IN (?) OR " \
|
93
|
+
"#{temporary_table_name}.to_state IS NULL"
|
62
94
|
else
|
63
|
-
|
64
|
-
|
95
|
+
"#{temporary_table_name}.to_state IN (?) AND " \
|
96
|
+
"#{temporary_table_name}.to_state IS NOT NULL"
|
65
97
|
end
|
66
98
|
end
|
99
|
+
|
100
|
+
def db_true
|
101
|
+
::ActiveRecord::Base.connection.quote(true)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Only use the most_recent column if it has a unique index guaranteeing
|
105
|
+
# it has good data
|
106
|
+
def use_most_recent_column?
|
107
|
+
::ActiveRecord::Base.connection.index_exists?(
|
108
|
+
transition_name,
|
109
|
+
[model_foreign_key, :most_recent],
|
110
|
+
unique: true,
|
111
|
+
name: "index_#{transition_name}_parent_most_recent"
|
112
|
+
)
|
113
|
+
end
|
67
114
|
end
|
68
115
|
end
|
69
116
|
end
|