pgbundle 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +11 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +45 -0
- data/Rakefile +7 -0
- data/bin/pgbundle +61 -0
- data/lib/pgbundle.rb +59 -0
- data/lib/pgbundle/base_source.rb +26 -0
- data/lib/pgbundle/database.rb +133 -0
- data/lib/pgbundle/definition.rb +70 -0
- data/lib/pgbundle/dsl.rb +44 -0
- data/lib/pgbundle/extension.rb +270 -0
- data/lib/pgbundle/github_source.rb +32 -0
- data/lib/pgbundle/path_source.rb +15 -0
- data/lib/pgbundle/version.rb +3 -0
- data/pgbundle.gemspec +30 -0
- data/spec/Pgfile +6 -0
- data/spec/definition_spec.rb +19 -0
- data/spec/dsl_spec.rb +8 -0
- data/spec/extension_spec.rb +76 -0
- data/spec/sample_extensions/bar/Makefile +5 -0
- data/spec/sample_extensions/bar/bar--0.0.2.sql +2 -0
- data/spec/sample_extensions/bar/bar.control +5 -0
- data/spec/sample_extensions/baz/Makefile +5 -0
- data/spec/sample_extensions/baz/baz--0.0.2.sql +2 -0
- data/spec/sample_extensions/baz/baz.control +5 -0
- data/spec/sample_extensions/foo/Makefile +5 -0
- data/spec/sample_extensions/foo/foo--0.0.1--0.0.2.sql +2 -0
- data/spec/sample_extensions/foo/foo--0.0.1.sql +2 -0
- data/spec/sample_extensions/foo/foo--0.0.2--0.0.1.sql +2 -0
- data/spec/sample_extensions/foo/foo--0.0.2.sql +2 -0
- data/spec/sample_extensions/foo/foo.control +4 -0
- data/spec/source_spec.rb +10 -0
- data/spec/spec_helper.rb +47 -0
- metadata +210 -0
data/lib/pgbundle/dsl.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module PgBundle
|
2
|
+
# The Dsl class defines the Domain Specific Language for the PgFile
|
3
|
+
# it's mainly user to parse a PgFile and return a Definition Object
|
4
|
+
class Dsl
|
5
|
+
def initialize
|
6
|
+
@definition = Definition.new
|
7
|
+
@extensions = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def eval_pgfile(pgfile, contents=nil)
|
11
|
+
contents ||= File.read(pgfile.to_s)
|
12
|
+
instance_eval(contents)
|
13
|
+
@definition
|
14
|
+
rescue SyntaxError => e
|
15
|
+
syntax_msg = e.message.gsub("#{pgfile}:", 'on line ')
|
16
|
+
raise PgfileError, "Pgfile syntax error #{syntax_msg}"
|
17
|
+
rescue ScriptError, RegexpError, NameError, ArgumentError => e
|
18
|
+
e.backtrace[0] = "#{e.backtrace[0]}: #{e.message} (#{e.class})"
|
19
|
+
puts e.backtrace.join("\n ")
|
20
|
+
raise PgfileError, "There was an error in your Pgfile," \
|
21
|
+
" and pgbundle cannot continue. " \
|
22
|
+
+ e.message
|
23
|
+
end
|
24
|
+
|
25
|
+
def database(*args)
|
26
|
+
opts = extract_options!(args)
|
27
|
+
@definition.database = Database.new(args.first, opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
def pgx(*args)
|
31
|
+
opts = extract_options!(args)
|
32
|
+
ext = Extension.new(*args, opts)
|
33
|
+
@definition.extensions[ext.name] = ext
|
34
|
+
end
|
35
|
+
|
36
|
+
def extract_options!(arr)
|
37
|
+
if arr.last.is_a? Hash
|
38
|
+
arr.pop
|
39
|
+
else
|
40
|
+
{}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,270 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
module PgBundle
|
3
|
+
# The Extension class provides the api for defining an Extension
|
4
|
+
# it installation source, and dependencies
|
5
|
+
# example:
|
6
|
+
# define an extension named 'foo' at version '0.1.1', with source on github depending on hstore
|
7
|
+
# Extension.new('foo', '0.1.1', github: 'me/foo', requires: 'hstore')
|
8
|
+
# you can then check if the Extension is available on a given database
|
9
|
+
# extension.available?(database)
|
10
|
+
# or install it along with it's dependencies
|
11
|
+
# extension.install(database)
|
12
|
+
class Extension
|
13
|
+
attr_accessor :name, :version, :source, :resolving_dependencies
|
14
|
+
def initialize(*args)
|
15
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
16
|
+
@name, @version = args
|
17
|
+
self.dependencies = opts[:requires]
|
18
|
+
set_source(opts)
|
19
|
+
end
|
20
|
+
|
21
|
+
def dependencies
|
22
|
+
@dependencies
|
23
|
+
end
|
24
|
+
|
25
|
+
# set dependency hash with different options
|
26
|
+
# dependencies= {foo: Extension.new('foo'), bar: Extension.new('bar')}
|
27
|
+
# => {'foo' => Extension.new('foo'), 'bar' Extension.new('bar')}
|
28
|
+
# dependencies= 'foo'
|
29
|
+
# => {foo: Extension.new('foo')}
|
30
|
+
# dependencies= Extension.new('foo')
|
31
|
+
# => {foo: Extension.new('foo')}
|
32
|
+
# dependencies= ['foo', 'bar']
|
33
|
+
# => {'foo' => Extension.new('foo'), 'bar' Extension.new('bar')}
|
34
|
+
def dependencies=(obj = nil)
|
35
|
+
@dependencies = case obj
|
36
|
+
when nil
|
37
|
+
{}
|
38
|
+
when Hash
|
39
|
+
Hash[obj.map { |k, v| [k.to_s, v] }]
|
40
|
+
when String, Symbol
|
41
|
+
{ obj.to_s => Extension.new(obj.to_s) }
|
42
|
+
when Extension
|
43
|
+
{ obj.name => obj }
|
44
|
+
when Array
|
45
|
+
hashable = obj.map do |o|
|
46
|
+
case o
|
47
|
+
when String, Symbol
|
48
|
+
[o.to_s, Extension.new(obj.to_s)]
|
49
|
+
when Extension
|
50
|
+
[o.name, o]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
Hash[hashable]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# returns true if extension is available for installation on a given database
|
58
|
+
def available?(database)
|
59
|
+
return false unless installed?(database)
|
60
|
+
return true if created?(database)
|
61
|
+
|
62
|
+
created_any_version?(database) ? updatable?(database) : creatable?(database)
|
63
|
+
rescue ExtensionCreateError
|
64
|
+
false
|
65
|
+
end
|
66
|
+
|
67
|
+
# returns true if extension is already created with the correct version in the given database
|
68
|
+
def created?(database)
|
69
|
+
if version
|
70
|
+
result = database.execute("SELECT * FROM pg_available_extension_versions WHERE name ='#{name}' AND version = '#{version}' AND installed").to_a
|
71
|
+
else
|
72
|
+
result = database.execute("SELECT * FROM pg_available_extension_versions WHERE name ='#{name}' AND installed").to_a
|
73
|
+
end
|
74
|
+
|
75
|
+
if result.empty?
|
76
|
+
false
|
77
|
+
else
|
78
|
+
true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# returns if the extension is already installed on the database system
|
83
|
+
# if it is also already created it returns the installed version
|
84
|
+
def installed?(database)
|
85
|
+
if version
|
86
|
+
result = database.execute("SELECT * FROM pg_available_extension_versions WHERE name ='#{name}' AND version = '#{version}'").to_a
|
87
|
+
else
|
88
|
+
result = database.execute("SELECT * FROM pg_available_extension_versions WHERE name ='#{name}'").to_a
|
89
|
+
end
|
90
|
+
|
91
|
+
if result.empty?
|
92
|
+
false
|
93
|
+
else
|
94
|
+
true
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# checks that all dependencies are installed on a given database
|
99
|
+
def dependencies_installed?(database)
|
100
|
+
dependencies.all?{|_, d| d.installed?(database)}
|
101
|
+
end
|
102
|
+
|
103
|
+
# installs extension and all dependencies using make install
|
104
|
+
# returns true if Extension can successfully be created using CREATE EXTENSION
|
105
|
+
def install(database)
|
106
|
+
unless dependencies.empty?
|
107
|
+
install_dependencies(database)
|
108
|
+
end
|
109
|
+
|
110
|
+
make_install(database)
|
111
|
+
raise ExtensionNotFound.new(name, version) unless installed?(database)
|
112
|
+
|
113
|
+
add_missing_required_dependencies(database)
|
114
|
+
|
115
|
+
creatable?(database)
|
116
|
+
end
|
117
|
+
|
118
|
+
# completely removes extension be running make uninstall and DROP EXTENSION
|
119
|
+
def uninstall!(database)
|
120
|
+
drop_extension(database)
|
121
|
+
make_uninstall(database)
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
# create extension on a given database connection
|
127
|
+
def create!(con)
|
128
|
+
con.exec create_stmt
|
129
|
+
end
|
130
|
+
|
131
|
+
# create the dependency graph on the given connection
|
132
|
+
def create_dependencies(con)
|
133
|
+
@resolving_dependencies = true
|
134
|
+
dependencies.each do |_, d|
|
135
|
+
fail CircularDependencyError.new(name, d.name) if d.resolving_dependencies
|
136
|
+
d.create_dependencies(con)
|
137
|
+
d.create!(con)
|
138
|
+
end
|
139
|
+
@resolving_dependencies = false
|
140
|
+
end
|
141
|
+
|
142
|
+
# checks if Extension is already installed at any version thus need ALTER EXTENSION to install
|
143
|
+
def created_any_version?(database)
|
144
|
+
result = database.execute("SELECT * FROM pg_available_extension_versions WHERE name ='#{name}' AND installed").to_a
|
145
|
+
if result.empty?
|
146
|
+
false
|
147
|
+
else
|
148
|
+
true
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# adds dependencies that are required but not defined yet
|
153
|
+
def add_missing_required_dependencies(database)
|
154
|
+
requires = requires(database)
|
155
|
+
requires.each do |name|
|
156
|
+
unless dependencies[name]
|
157
|
+
dependencies[name] = Extension.new(name)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# returns an array of required Extension specified in the extensions control file
|
163
|
+
def requires(database)
|
164
|
+
fail "Extension #{name} not (yet) installed" unless installed?(database)
|
165
|
+
|
166
|
+
stmt = if version
|
167
|
+
<<-SQL
|
168
|
+
SELECT unnest(requires) as name FROM
|
169
|
+
( SELECT requires FROM pg_available_extension_versions where name='#{name}' AND version ='#{version}') t
|
170
|
+
SQL
|
171
|
+
else
|
172
|
+
<<-SQL
|
173
|
+
SELECT unnest(requires) as name FROM
|
174
|
+
(SELECT requires FROM
|
175
|
+
pg_available_extensions a
|
176
|
+
JOIN pg_available_extension_versions v ON v.name = a.name AND a.default_version = v.version
|
177
|
+
WHERE v.name = '#{name}')t
|
178
|
+
SQL
|
179
|
+
end
|
180
|
+
|
181
|
+
result = database.execute(stmt).to_a
|
182
|
+
|
183
|
+
requires = result.map{|r| r['name']}
|
184
|
+
end
|
185
|
+
|
186
|
+
# loads the source and runs make uninstall
|
187
|
+
# returns: self
|
188
|
+
def make_uninstall(database)
|
189
|
+
database.make_uninstall(source, name)
|
190
|
+
self
|
191
|
+
end
|
192
|
+
|
193
|
+
def drop_extension(database)
|
194
|
+
database.drop_extension(name)
|
195
|
+
end
|
196
|
+
|
197
|
+
# loads the source and runs make install
|
198
|
+
# returns: self
|
199
|
+
def make_install(database)
|
200
|
+
return self if installed?(database)
|
201
|
+
|
202
|
+
fail SourceNotFound, name if source.nil?
|
203
|
+
|
204
|
+
database.make_install(source, name)
|
205
|
+
self
|
206
|
+
end
|
207
|
+
|
208
|
+
def create_stmt
|
209
|
+
stmt = "CREATE EXTENSION #{name}"
|
210
|
+
stmt += " VERSION '#{version}'" unless version.nil? || version.empty?
|
211
|
+
|
212
|
+
stmt
|
213
|
+
end
|
214
|
+
|
215
|
+
def install_dependencies(database)
|
216
|
+
begin
|
217
|
+
dependencies.each do |_, d|
|
218
|
+
d.install(database)
|
219
|
+
end
|
220
|
+
rescue InstallError, ExtensionCreateError => e
|
221
|
+
raise DependencyNotFound.new(name, e.message)
|
222
|
+
end
|
223
|
+
|
224
|
+
true
|
225
|
+
end
|
226
|
+
|
227
|
+
def creatable?(database)
|
228
|
+
dependencies_installed?(database) && installed?(database)
|
229
|
+
end
|
230
|
+
|
231
|
+
# hard checks that the dependency can be created running CREATE command in a transaction
|
232
|
+
def creatable!(database)
|
233
|
+
database.transaction_rollback do |con|
|
234
|
+
begin
|
235
|
+
create_dependencies(con)
|
236
|
+
create!(con)
|
237
|
+
rescue PG::UndefinedFile => err
|
238
|
+
raise ExtensionNotFound.new(name, version)
|
239
|
+
rescue PG::UndefinedObject => err
|
240
|
+
raise MissingDependency.new(name, err.message)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
true
|
245
|
+
end
|
246
|
+
|
247
|
+
# checks that the extension can be updated running ALTER EXTENSION command in a transaction
|
248
|
+
def updatable?(database)
|
249
|
+
result = true
|
250
|
+
database.execute 'BEGIN'
|
251
|
+
begin
|
252
|
+
database.execute "ALTER EXTENSION #{name} UPDATE TO '#{version}'"
|
253
|
+
rescue PG::UndefinedFile, PG::UndefinedObject => err
|
254
|
+
@error = err.message
|
255
|
+
result = false
|
256
|
+
end
|
257
|
+
database.execute 'ROLLBACK'
|
258
|
+
|
259
|
+
result
|
260
|
+
end
|
261
|
+
|
262
|
+
def set_source(opts)
|
263
|
+
if opts[:path]
|
264
|
+
@source = PathSource.new(opts[:path])
|
265
|
+
elsif opts[:github]
|
266
|
+
@source = GithubSource.new(opts[:github])
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
module PgBundle
|
3
|
+
# The GithubSource class defines a Github Source
|
4
|
+
class GithubSource < BaseSource
|
5
|
+
def load(host, user, dest)
|
6
|
+
clone(dest)
|
7
|
+
if host == 'localhost'
|
8
|
+
copy_local(clone_dir, dest)
|
9
|
+
else
|
10
|
+
copy_to_remote(host, user, clone_dir, dest)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def branch_name
|
15
|
+
@branch || 'master'
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def clone(dest)
|
21
|
+
# git clone user@git-server:project_name.git -b branch_name /some/folder
|
22
|
+
%x((git clone git@github.com:#{path}.git -b #{branch_name} --quiet --depth=1 #{clone_dir} && rm -rf #{clone_dir}/.git}) 2>&1)
|
23
|
+
unless $?.success?
|
24
|
+
fail GitCommandError
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def clone_dir
|
29
|
+
@clone_dir ||= Dir.mktmpdir
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
require 'net/scp'
|
3
|
+
module PgBundle
|
4
|
+
# The PathSource class defines a local Path Source
|
5
|
+
# eg. PathSource.new('/my/local/path')
|
6
|
+
class PathSource < BaseSource
|
7
|
+
def load(host, user, dest)
|
8
|
+
if host == 'localhost'
|
9
|
+
copy_local(path, dest)
|
10
|
+
else
|
11
|
+
copy_to_remote(host, user, path, dest)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/pgbundle.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pgbundle/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pgbundle"
|
8
|
+
spec.version = Pgbundle::VERSION
|
9
|
+
spec.authors = ["Manuel Kniep"]
|
10
|
+
spec.email = ["manuel@adjust.com"]
|
11
|
+
spec.summary = %q{bundling postgres extension}
|
12
|
+
spec.description = %q{bundler like postgres extension manager}
|
13
|
+
spec.homepage = "http://github.com/adjust/pgbundle"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'thor'
|
22
|
+
spec.add_dependency 'net-ssh'
|
23
|
+
spec.add_dependency 'net-scp'
|
24
|
+
#https://bitbucket.org/ged/ruby-pg/wiki/Home
|
25
|
+
spec.add_dependency 'pg', '> 0.17'
|
26
|
+
spec.add_development_dependency 'rspec', '~> 2.14.0'
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
28
|
+
spec.add_development_dependency "rake"
|
29
|
+
spec.add_development_dependency "pry"
|
30
|
+
end
|
data/spec/Pgfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe PgBundle::Definition do
|
3
|
+
subject do
|
4
|
+
d = PgBundle::Definition.new
|
5
|
+
d.database = database
|
6
|
+
d.extensions['bar'] = PgBundle::Extension.new('bar', path: './spec/sample_extensions//bar', requires: 'ltree')
|
7
|
+
d.extensions['baz'] = PgBundle::Extension.new('baz', path: './spec/sample_extensions/baz', requires: 'foo')
|
8
|
+
d.extensions['foo'] = PgBundle::Extension.new('foo', '0.0.2', path: './spec/sample_extensions/foo')
|
9
|
+
d
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'missing_extensions' do
|
13
|
+
subject.missing_extensions.map(&:name).should eq %w(bar baz foo)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should install missing extension' do
|
17
|
+
subject.install.map(&:name).should eq %w(bar baz foo)
|
18
|
+
end
|
19
|
+
end
|
data/spec/dsl_spec.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe PgBundle::Extension do
|
3
|
+
describe 'Basiscs' do
|
4
|
+
subject { PgBundle::Extension.new('foo', '1.2.3', github: 'bar/foo', requires: 'baz') }
|
5
|
+
|
6
|
+
its(:name) { should eq 'foo' }
|
7
|
+
its(:version) { should eq '1.2.3' }
|
8
|
+
its('dependencies.first.last') { should be_a PgBundle::Extension }
|
9
|
+
its(:source) { should be_a PgBundle::GithubSource }
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'Installation' do
|
13
|
+
|
14
|
+
context 'version available' do
|
15
|
+
subject { PgBundle::Extension.new('foo', '0.0.2', path: './spec/sample_extensions/foo') }
|
16
|
+
|
17
|
+
it 'should not be available before install' do
|
18
|
+
subject.available?(database).should be false
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should be available after install' do
|
22
|
+
subject.install(database)
|
23
|
+
subject.available?(database).should be true
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'wrong version installed' do
|
27
|
+
subject { PgBundle::Extension.new('foo', '0.0.2', path: './spec/sample_extensions/foo') }
|
28
|
+
let(:wrong_version) { PgBundle::Extension.new('foo', '0.0.1', path: './spec/sample_extensions/foo') }
|
29
|
+
before do
|
30
|
+
wrong_version.install(database)
|
31
|
+
database.connection.exec "CREATE EXTENSION foo VERSION '0.0.2'"
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should be already installed' do
|
35
|
+
subject.should be_created_any_version(database)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should be available' do
|
39
|
+
subject.should be_available(database)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'with require' do
|
44
|
+
let(:dependend) { PgBundle::Extension.new('bar', path: './spec/sample_extensions/bar', requires: 'ltree') }
|
45
|
+
subject { PgBundle::Extension.new('foo', '0.0.2', path: './spec/sample_extensions/foo', requires: dependend) }
|
46
|
+
|
47
|
+
it 'requires should be installable' do
|
48
|
+
dependend.should_not be_available(database)
|
49
|
+
subject.install(database).should be true
|
50
|
+
dependend.should be_available(database)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'version not available' do
|
57
|
+
subject { PgBundle::Extension.new('foo', '0.0.3', path: './spec/sample_extensions/foo') }
|
58
|
+
|
59
|
+
it 'should raise ExtensionNotFound' do
|
60
|
+
expect { subject.install(database) }.to raise_error PgBundle::ExtensionNotFound, 'specified Version 0.0.3 for Extension foo not available'
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should not be available although it is installed' do
|
64
|
+
subject.available?(database).should be false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'require not found' do
|
69
|
+
subject { PgBundle::Extension.new('foo', '0.0.2', path: './spec/sample_extensions/foo', requires: PgBundle::Extension.new('noope')) }
|
70
|
+
|
71
|
+
it 'should raise DependencyNotFound' do
|
72
|
+
expect { subject.install(database) }.to raise_error PgBundle::DependencyNotFound, "Can't install Dependency for Extension foo: Source for Extension noope not found"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|