fx 0.2.0 → 0.6.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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.hound.yml +2 -0
- data/.rubocop.yml +648 -0
- data/.travis.yml +18 -6
- data/Appraisals +21 -10
- data/LICENSE +18 -0
- data/README.md +45 -13
- data/bin/setup +1 -0
- data/gemfiles/rails42.gemfile +2 -1
- data/gemfiles/rails50.gemfile +1 -1
- data/gemfiles/rails51.gemfile +8 -0
- data/gemfiles/rails52.gemfile +8 -0
- data/gemfiles/rails60.gemfile +8 -0
- data/gemfiles/rails_edge.gemfile +8 -0
- data/lib/fx.rb +22 -0
- data/lib/fx/adapters/postgres.rb +26 -1
- data/lib/fx/adapters/postgres/functions.rb +5 -2
- data/lib/fx/adapters/postgres/triggers.rb +2 -2
- data/lib/fx/command_recorder.rb +0 -5
- data/lib/fx/configuration.rb +10 -0
- data/lib/fx/definition.rb +13 -3
- data/lib/fx/function.rb +2 -0
- data/lib/fx/railtie.rb +15 -0
- data/lib/fx/schema_dumper.rb +0 -5
- data/lib/fx/schema_dumper/function.rb +14 -5
- data/lib/fx/statements.rb +0 -5
- data/lib/fx/statements/function.rb +4 -2
- data/lib/fx/statements/trigger.rb +2 -0
- data/lib/fx/trigger.rb +2 -0
- data/lib/fx/version.rb +1 -1
- data/lib/generators/fx/function/USAGE +2 -0
- data/lib/generators/fx/function/function_generator.rb +14 -1
- data/lib/generators/fx/trigger/USAGE +2 -0
- data/lib/generators/fx/trigger/trigger_generator.rb +14 -1
- data/spec/acceptance/user_manages_functions_spec.rb +22 -2
- data/spec/acceptance/user_manages_triggers_spec.rb +6 -6
- data/spec/acceptance_helper.rb +2 -1
- data/spec/dummy/Rakefile +7 -0
- data/spec/features/functions/migrations_spec.rb +2 -2
- data/spec/features/functions/revert_spec.rb +2 -2
- data/spec/features/triggers/migrations_spec.rb +3 -3
- data/spec/features/triggers/revert_spec.rb +4 -4
- data/spec/fx/adapters/postgres/functions_spec.rb +37 -0
- data/spec/fx/adapters/postgres/triggers_spec.rb +45 -0
- data/spec/fx/adapters/postgres_spec.rb +46 -49
- data/spec/fx/definition_spec.rb +25 -2
- data/spec/fx/function_spec.rb +55 -0
- data/spec/fx/schema_dumper/function_spec.rb +58 -2
- data/spec/fx/schema_dumper/trigger_spec.rb +3 -3
- data/spec/fx/trigger_spec.rb +55 -0
- data/spec/generators/fx/function/function_generator_spec.rb +12 -0
- data/spec/generators/fx/trigger/trigger_generator_spec.rb +12 -0
- metadata +19 -11
- data/.ruby-version +0 -1
- data/gemfiles/rails40.gemfile +0 -8
- data/gemfiles/rails40.gemfile.lock +0 -111
- data/gemfiles/rails41.gemfile +0 -8
- data/gemfiles/rails41.gemfile.lock +0 -113
- data/gemfiles/rails42.gemfile.lock +0 -130
- data/gemfiles/rails50.gemfile.lock +0 -126
@@ -0,0 +1,45 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Fx
|
4
|
+
module Adapters
|
5
|
+
describe Postgres::Triggers, :db do
|
6
|
+
describe ".all" do
|
7
|
+
it "returns `Trigger` objects" do
|
8
|
+
connection = ActiveRecord::Base.connection
|
9
|
+
connection.execute <<-EOS.strip_heredoc
|
10
|
+
CREATE TABLE users (
|
11
|
+
id int PRIMARY KEY,
|
12
|
+
name varchar(256),
|
13
|
+
upper_name varchar(256)
|
14
|
+
);
|
15
|
+
EOS
|
16
|
+
connection.execute <<-EOS.strip_heredoc
|
17
|
+
CREATE OR REPLACE FUNCTION uppercase_users_name()
|
18
|
+
RETURNS trigger AS $$
|
19
|
+
BEGIN
|
20
|
+
NEW.upper_name = UPPER(NEW.name);
|
21
|
+
RETURN NEW;
|
22
|
+
END;
|
23
|
+
$$ LANGUAGE plpgsql;
|
24
|
+
EOS
|
25
|
+
connection.execute <<-EOS.strip_heredoc
|
26
|
+
CREATE TRIGGER uppercase_users_name
|
27
|
+
BEFORE INSERT ON users
|
28
|
+
FOR EACH ROW
|
29
|
+
EXECUTE PROCEDURE uppercase_users_name();
|
30
|
+
EOS
|
31
|
+
|
32
|
+
triggers = Postgres::Triggers.new(connection).all
|
33
|
+
|
34
|
+
first = triggers.first
|
35
|
+
expect(triggers.size).to eq 1
|
36
|
+
expect(first.name).to eq "uppercase_users_name"
|
37
|
+
expect(first.definition).to include("BEFORE INSERT")
|
38
|
+
expect(first.definition).to match(/ON [public\.users|users]/)
|
39
|
+
expect(first.definition).to include("FOR EACH ROW")
|
40
|
+
expect(first.definition).to include("EXECUTE PROCEDURE uppercase_users_name()")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -6,7 +6,7 @@ module Fx::Adapters
|
|
6
6
|
it "successfully creates a function" do
|
7
7
|
adapter = Postgres.new
|
8
8
|
adapter.create_function(
|
9
|
-
|
9
|
+
<<-EOS
|
10
10
|
CREATE OR REPLACE FUNCTION test()
|
11
11
|
RETURNS text AS $$
|
12
12
|
BEGIN
|
@@ -22,7 +22,7 @@ module Fx::Adapters
|
|
22
22
|
|
23
23
|
describe "#create_trigger" do
|
24
24
|
it "successfully creates a trigger" do
|
25
|
-
connection.execute
|
25
|
+
connection.execute <<-EOS
|
26
26
|
CREATE TABLE users (
|
27
27
|
id int PRIMARY KEY,
|
28
28
|
name varchar(256),
|
@@ -30,7 +30,7 @@ module Fx::Adapters
|
|
30
30
|
);
|
31
31
|
EOS
|
32
32
|
adapter = Postgres.new
|
33
|
-
adapter.create_function
|
33
|
+
adapter.create_function <<-EOS
|
34
34
|
CREATE OR REPLACE FUNCTION uppercase_users_name()
|
35
35
|
RETURNS trigger AS $$
|
36
36
|
BEGIN
|
@@ -40,7 +40,7 @@ module Fx::Adapters
|
|
40
40
|
$$ LANGUAGE plpgsql;
|
41
41
|
EOS
|
42
42
|
adapter.create_trigger(
|
43
|
-
|
43
|
+
<<-EOS
|
44
44
|
CREATE TRIGGER uppercase_users_name
|
45
45
|
BEFORE INSERT ON users
|
46
46
|
FOR EACH ROW
|
@@ -53,22 +53,44 @@ module Fx::Adapters
|
|
53
53
|
end
|
54
54
|
|
55
55
|
describe "#drop_function" do
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
56
|
+
context "when the function has arguments" do
|
57
|
+
it "successfully drops a function with the entire function signature" do
|
58
|
+
adapter = Postgres.new
|
59
|
+
adapter.create_function(
|
60
|
+
<<-EOS
|
61
|
+
CREATE FUNCTION adder(x int, y int)
|
62
|
+
RETURNS int AS $$
|
63
|
+
BEGIN
|
64
|
+
RETURN $1 + $2;
|
65
|
+
END;
|
66
|
+
$$ LANGUAGE plpgsql;
|
67
|
+
EOS
|
68
|
+
)
|
69
|
+
|
70
|
+
adapter.drop_function(:adder)
|
68
71
|
|
69
|
-
|
72
|
+
expect(adapter.functions.map(&:name)).not_to include("adder")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context "when the function does not have arguments" do
|
77
|
+
it "successfully drops a function" do
|
78
|
+
adapter = Postgres.new
|
79
|
+
adapter.create_function(
|
80
|
+
<<-EOS
|
81
|
+
CREATE OR REPLACE FUNCTION test()
|
82
|
+
RETURNS text AS $$
|
83
|
+
BEGIN
|
84
|
+
RETURN 'test';
|
85
|
+
END;
|
86
|
+
$$ LANGUAGE plpgsql;
|
87
|
+
EOS
|
88
|
+
)
|
89
|
+
|
90
|
+
adapter.drop_function(:test)
|
70
91
|
|
71
|
-
|
92
|
+
expect(adapter.functions.map(&:name)).not_to include("test")
|
93
|
+
end
|
72
94
|
end
|
73
95
|
end
|
74
96
|
|
@@ -76,7 +98,7 @@ module Fx::Adapters
|
|
76
98
|
it "finds functions and builds Fx::Function objects" do
|
77
99
|
adapter = Postgres.new
|
78
100
|
adapter.create_function(
|
79
|
-
|
101
|
+
<<-EOS
|
80
102
|
CREATE OR REPLACE FUNCTION test()
|
81
103
|
RETURNS text AS $$
|
82
104
|
BEGIN
|
@@ -86,27 +108,13 @@ module Fx::Adapters
|
|
86
108
|
EOS
|
87
109
|
)
|
88
110
|
|
89
|
-
expect(adapter.functions).to eq
|
90
|
-
Fx::Function.new(
|
91
|
-
"name" => "test",
|
92
|
-
"definition" => <<~EOS
|
93
|
-
CREATE OR REPLACE FUNCTION public.test()
|
94
|
-
RETURNS text
|
95
|
-
LANGUAGE plpgsql
|
96
|
-
AS $function$
|
97
|
-
BEGIN
|
98
|
-
RETURN 'test';
|
99
|
-
END;
|
100
|
-
$function$
|
101
|
-
EOS
|
102
|
-
),
|
103
|
-
])
|
111
|
+
expect(adapter.functions.map(&:name)).to eq ["test"]
|
104
112
|
end
|
105
113
|
end
|
106
114
|
|
107
115
|
describe "#triggers" do
|
108
116
|
it "finds triggers and builds Fx::Trigger objects" do
|
109
|
-
connection.execute
|
117
|
+
connection.execute <<-EOS
|
110
118
|
CREATE TABLE users (
|
111
119
|
id int PRIMARY KEY,
|
112
120
|
name varchar(256),
|
@@ -114,7 +122,7 @@ module Fx::Adapters
|
|
114
122
|
);
|
115
123
|
EOS
|
116
124
|
adapter = Postgres.new
|
117
|
-
adapter.create_function
|
125
|
+
adapter.create_function <<-EOS
|
118
126
|
CREATE OR REPLACE FUNCTION uppercase_users_name()
|
119
127
|
RETURNS trigger AS $$
|
120
128
|
BEGIN
|
@@ -123,7 +131,7 @@ module Fx::Adapters
|
|
123
131
|
END;
|
124
132
|
$$ LANGUAGE plpgsql;
|
125
133
|
EOS
|
126
|
-
sql_definition =
|
134
|
+
sql_definition = <<-EOS
|
127
135
|
CREATE TRIGGER uppercase_users_name
|
128
136
|
BEFORE INSERT ON users
|
129
137
|
FOR EACH ROW
|
@@ -131,19 +139,8 @@ module Fx::Adapters
|
|
131
139
|
EOS
|
132
140
|
adapter.create_trigger(sql_definition)
|
133
141
|
|
134
|
-
expect(adapter.triggers).to eq
|
135
|
-
[
|
136
|
-
Fx::Trigger.new(
|
137
|
-
"name" => "uppercase_users_name",
|
138
|
-
"definition" => format_trigger(sql_definition),
|
139
|
-
),
|
140
|
-
],
|
141
|
-
)
|
142
|
+
expect(adapter.triggers.map(&:name)).to eq ["uppercase_users_name"]
|
142
143
|
end
|
143
144
|
end
|
144
|
-
|
145
|
-
def format_trigger(trigger)
|
146
|
-
trigger.gsub("\n", "").gsub(" ", " ")
|
147
|
-
end
|
148
145
|
end
|
149
146
|
end
|
data/spec/fx/definition_spec.rb
CHANGED
@@ -4,7 +4,7 @@ describe Fx::Definition do
|
|
4
4
|
describe "#to_sql" do
|
5
5
|
context "representing a function definition" do
|
6
6
|
it "returns the content of a function definition" do
|
7
|
-
sql_definition =
|
7
|
+
sql_definition = <<-EOS
|
8
8
|
CREATE OR REPLACE FUNCTION test()
|
9
9
|
RETURNS text AS $$
|
10
10
|
BEGIN
|
@@ -28,11 +28,34 @@ describe Fx::Definition do
|
|
28
28
|
%r(Define function in db/functions/test_v01.sql before migrating),
|
29
29
|
)
|
30
30
|
end
|
31
|
+
|
32
|
+
context "when definition is at Rails engine" do
|
33
|
+
it "returns the content of a function definition" do
|
34
|
+
sql_definition = <<-EOS
|
35
|
+
CREATE OR REPLACE FUNCTION test()
|
36
|
+
RETURNS text AS $$
|
37
|
+
BEGIN
|
38
|
+
RETURN 'test';
|
39
|
+
END;
|
40
|
+
$$ LANGUAGE plpgsql;
|
41
|
+
EOS
|
42
|
+
engine_path = Rails.root.join("tmp", "engine")
|
43
|
+
FileUtils.mkdir_p(engine_path.join("db", "functions"))
|
44
|
+
File.write(engine_path.join("db", "functions", "custom_test_v01.sql"), sql_definition)
|
45
|
+
Rails.application.config.paths["db/migrate"].push(engine_path.join("db", "migrate"))
|
46
|
+
|
47
|
+
definition = Fx::Definition.new(name: "custom_test", version: 1)
|
48
|
+
|
49
|
+
expect(definition.to_sql).to eq sql_definition
|
50
|
+
|
51
|
+
FileUtils.rm_rf(engine_path)
|
52
|
+
end
|
53
|
+
end
|
31
54
|
end
|
32
55
|
|
33
56
|
context "representing a trigger definition" do
|
34
57
|
it "returns the content of a trigger definition" do
|
35
|
-
sql_definition =
|
58
|
+
sql_definition = <<-EOS
|
36
59
|
CREATE TRIGGER check_update
|
37
60
|
BEFORE UPDATE ON accounts
|
38
61
|
FOR EACH ROW
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "fx/function"
|
3
|
+
|
4
|
+
module Fx
|
5
|
+
describe Function do
|
6
|
+
describe "#<=>" do
|
7
|
+
it "delegates to `name`" do
|
8
|
+
function_a = Function.new(
|
9
|
+
"name" => "name_a",
|
10
|
+
"definition" => "some defintion",
|
11
|
+
)
|
12
|
+
function_b = Function.new(
|
13
|
+
"name" => "name_b",
|
14
|
+
"definition" => "some defintion",
|
15
|
+
)
|
16
|
+
function_c = Function.new(
|
17
|
+
"name" => "name_c",
|
18
|
+
"definition" => "some defintion",
|
19
|
+
)
|
20
|
+
|
21
|
+
expect(function_b).to be_between(function_a, function_c)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#==" do
|
26
|
+
it "compares `name` and `definition`" do
|
27
|
+
function_a = Function.new(
|
28
|
+
"name" => "name_a",
|
29
|
+
"definition" => "some defintion",
|
30
|
+
)
|
31
|
+
function_b = Function.new(
|
32
|
+
"name" => "name_b",
|
33
|
+
"definition" => "some other defintion",
|
34
|
+
)
|
35
|
+
|
36
|
+
expect(function_a).not_to eq(function_b)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#to_schema" do
|
41
|
+
it "returns a schema compatible version of the function" do
|
42
|
+
function = Function.new(
|
43
|
+
"name" => "uppercase_users_name",
|
44
|
+
"definition" => "CREATE OR REPLACE TRIGGER uppercase_users_name ...",
|
45
|
+
)
|
46
|
+
|
47
|
+
expect(function.to_schema).to eq <<-EOS
|
48
|
+
create_function :uppercase_users_name, sql_definition: <<-\SQL
|
49
|
+
CREATE OR REPLACE TRIGGER uppercase_users_name ...
|
50
|
+
SQL
|
51
|
+
EOS
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -2,15 +2,70 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
describe Fx::SchemaDumper::Function, :db do
|
4
4
|
it "dumps a create_function for a function in the database" do
|
5
|
-
sql_definition =
|
6
|
-
CREATE OR REPLACE FUNCTION
|
5
|
+
sql_definition = <<-EOS
|
6
|
+
CREATE OR REPLACE FUNCTION my_function()
|
7
7
|
RETURNS text AS $$
|
8
8
|
BEGIN
|
9
9
|
RETURN 'test';
|
10
10
|
END;
|
11
11
|
$$ LANGUAGE plpgsql;
|
12
12
|
EOS
|
13
|
+
connection.create_function :my_function, sql_definition: sql_definition
|
14
|
+
connection.create_table :my_table
|
15
|
+
stream = StringIO.new
|
16
|
+
output = stream.string
|
17
|
+
|
18
|
+
ActiveRecord::SchemaDumper.dump(connection, stream)
|
19
|
+
|
20
|
+
expect(output).to(
|
21
|
+
match(/table "my_table".*function :my_function.*RETURN 'test';/m),
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "dumps a create_function for a function in the database" do
|
26
|
+
Fx.configuration.dump_functions_at_beginning_of_schema = true
|
27
|
+
sql_definition = <<-EOS
|
28
|
+
CREATE OR REPLACE FUNCTION my_function()
|
29
|
+
RETURNS text AS $$
|
30
|
+
BEGIN
|
31
|
+
RETURN 'test';
|
32
|
+
END;
|
33
|
+
$$ LANGUAGE plpgsql;
|
34
|
+
EOS
|
35
|
+
connection.create_function :my_function, sql_definition: sql_definition
|
36
|
+
connection.create_table :my_table
|
37
|
+
stream = StringIO.new
|
38
|
+
output = stream.string
|
39
|
+
|
40
|
+
ActiveRecord::SchemaDumper.dump(connection, stream)
|
41
|
+
|
42
|
+
expect(output).to(
|
43
|
+
match(/function :my_function.*RETURN 'test';.*table "my_table"/m),
|
44
|
+
)
|
45
|
+
ensure
|
46
|
+
Fx.configuration.dump_functions_at_beginning_of_schema = false
|
47
|
+
end
|
48
|
+
|
49
|
+
it "does not dump a create_function for aggregates in the database" do
|
50
|
+
sql_definition = <<-EOS
|
51
|
+
CREATE OR REPLACE FUNCTION test(text, text)
|
52
|
+
RETURNS text AS $$
|
53
|
+
BEGIN
|
54
|
+
RETURN 'test';
|
55
|
+
END;
|
56
|
+
$$ LANGUAGE plpgsql;
|
57
|
+
EOS
|
58
|
+
|
59
|
+
aggregate_sql_definition = <<-EOS
|
60
|
+
CREATE AGGREGATE aggregate_test(text)
|
61
|
+
(
|
62
|
+
sfunc = test,
|
63
|
+
stype = text
|
64
|
+
);
|
65
|
+
EOS
|
66
|
+
|
13
67
|
connection.create_function :test, sql_definition: sql_definition
|
68
|
+
connection.execute aggregate_sql_definition
|
14
69
|
stream = StringIO.new
|
15
70
|
|
16
71
|
ActiveRecord::SchemaDumper.dump(connection, stream)
|
@@ -18,5 +73,6 @@ describe Fx::SchemaDumper::Function, :db do
|
|
18
73
|
output = stream.string
|
19
74
|
expect(output).to include "create_function :test, sql_definition: <<-SQL"
|
20
75
|
expect(output).to include "RETURN 'test';"
|
76
|
+
expect(output).not_to include "aggregate_test"
|
21
77
|
end
|
22
78
|
end
|
@@ -2,14 +2,14 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
describe Fx::SchemaDumper::Trigger, :db do
|
4
4
|
it "dumps a create_trigger for a trigger in the database" do
|
5
|
-
connection.execute
|
5
|
+
connection.execute <<-EOS
|
6
6
|
CREATE TABLE users (
|
7
7
|
id int PRIMARY KEY,
|
8
8
|
name varchar(256),
|
9
9
|
upper_name varchar(256)
|
10
10
|
);
|
11
11
|
EOS
|
12
|
-
Fx.database.create_function
|
12
|
+
Fx.database.create_function <<-EOS
|
13
13
|
CREATE OR REPLACE FUNCTION uppercase_users_name()
|
14
14
|
RETURNS trigger AS $$
|
15
15
|
BEGIN
|
@@ -18,7 +18,7 @@ describe Fx::SchemaDumper::Trigger, :db do
|
|
18
18
|
END;
|
19
19
|
$$ LANGUAGE plpgsql;
|
20
20
|
EOS
|
21
|
-
sql_definition =
|
21
|
+
sql_definition = <<-EOS
|
22
22
|
CREATE TRIGGER uppercase_users_name
|
23
23
|
BEFORE INSERT ON users
|
24
24
|
FOR EACH ROW
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "fx/trigger"
|
3
|
+
|
4
|
+
module Fx
|
5
|
+
describe Trigger do
|
6
|
+
describe "#<=>" do
|
7
|
+
it "delegates to `name`" do
|
8
|
+
trigger_a = Trigger.new(
|
9
|
+
"name" => "name_a",
|
10
|
+
"definition" => "some defintion",
|
11
|
+
)
|
12
|
+
trigger_b = Trigger.new(
|
13
|
+
"name" => "name_b",
|
14
|
+
"definition" => "some defintion",
|
15
|
+
)
|
16
|
+
trigger_c = Trigger.new(
|
17
|
+
"name" => "name_c",
|
18
|
+
"definition" => "some defintion",
|
19
|
+
)
|
20
|
+
|
21
|
+
expect(trigger_b).to be_between(trigger_a, trigger_c)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#==" do
|
26
|
+
it "compares `name` and `definition`" do
|
27
|
+
trigger_a = Trigger.new(
|
28
|
+
"name" => "name_a",
|
29
|
+
"definition" => "some defintion",
|
30
|
+
)
|
31
|
+
trigger_b = Trigger.new(
|
32
|
+
"name" => "name_b",
|
33
|
+
"definition" => "some other defintion",
|
34
|
+
)
|
35
|
+
|
36
|
+
expect(trigger_a).not_to eq(trigger_b)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#to_schema" do
|
41
|
+
it "returns a schema compatible version of the trigger" do
|
42
|
+
trigger = Trigger.new(
|
43
|
+
"name" => "uppercase_users_name",
|
44
|
+
"definition" => "CREATE TRIGGER uppercase_users_name ...",
|
45
|
+
)
|
46
|
+
|
47
|
+
expect(trigger.to_schema).to eq <<-EOS
|
48
|
+
create_trigger :uppercase_users_name, sql_definition: <<-\SQL
|
49
|
+
CREATE TRIGGER uppercase_users_name ...
|
50
|
+
SQL
|
51
|
+
EOS
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|