kloudless 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +62 -0
- data/Rakefile +10 -0
- data/TODO.md +26 -0
- data/kloudless.gemspec +25 -0
- data/lib/kloudless.rb +50 -0
- data/lib/kloudless/account.rb +33 -0
- data/lib/kloudless/account_key.rb +26 -0
- data/lib/kloudless/collection.rb +32 -0
- data/lib/kloudless/error.rb +172 -0
- data/lib/kloudless/event.rb +16 -0
- data/lib/kloudless/file.rb +50 -0
- data/lib/kloudless/folder.rb +41 -0
- data/lib/kloudless/http.rb +114 -0
- data/lib/kloudless/link.rb +34 -0
- data/lib/kloudless/model.rb +20 -0
- data/lib/kloudless/multipart_upload.rb +37 -0
- data/lib/kloudless/team.rb +9 -0
- data/lib/kloudless/version.rb +3 -0
- data/script/changelog +29 -0
- data/script/package +7 -0
- data/script/release +16 -0
- data/test/kloudless/account_key_test.rb +26 -0
- data/test/kloudless/account_test.rb +78 -0
- data/test/kloudless/collection_test.rb +49 -0
- data/test/kloudless/error_test.rb +23 -0
- data/test/kloudless/event_test.rb +20 -0
- data/test/kloudless/file_test.rb +63 -0
- data/test/kloudless/folder_test.rb +47 -0
- data/test/kloudless/http_test.rb +58 -0
- data/test/kloudless/link_test.rb +40 -0
- data/test/kloudless/multipart_upload_test.rb +38 -0
- data/test/kloudless/team_test.rb +11 -0
- data/test/kloudless_test.rb +25 -0
- data/test/test_helper.rb +17 -0
- metadata +150 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
module Kloudless
|
2
|
+
# https://developers.kloudless.com/docs#events
|
3
|
+
class Event < Model
|
4
|
+
# https://developers.kloudless.com/docs#events-list-file/folder-events
|
5
|
+
def self.list(account_id:, **params)
|
6
|
+
path = "/accounts/#{account_id}/events"
|
7
|
+
Kloudless::Collection.new(self, http.get(path, params: params))
|
8
|
+
end
|
9
|
+
|
10
|
+
# https://developers.kloudless.com/docs#events-retrieve-latest-cursor
|
11
|
+
def self.cursor(account_id:)
|
12
|
+
path = "/accounts/#{account_id}/events/latest"
|
13
|
+
new(http.get(path))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Kloudless
|
2
|
+
# https://developers.kloudless.com/docs#files
|
3
|
+
class File < Model
|
4
|
+
def self.upload(account_id:, **params)
|
5
|
+
path = "/accounts/#{account_id}/files"
|
6
|
+
new(http.post(path, params: params))
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.metadata(account_id:, file_id:)
|
10
|
+
path = "/accounts/#{account_id}/files/#{file_id}"
|
11
|
+
new(http.get(path))
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.rename(account_id:, file_id:, **params)
|
15
|
+
path = "/accounts/#{account_id}/files/#{file_id}"
|
16
|
+
new(http.patch(path, params: params))
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO: unclear how to post binary data over net-http
|
20
|
+
def self.update
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.download(account_id:, file_id:)
|
25
|
+
path = "/accounts/#{account_id}/files/#{file_id}/contents"
|
26
|
+
http.get_raw(path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.copy(account_id:, file_id:, parent_id:, **params)
|
30
|
+
path = "/accounts/#{account_id}/files/#{file_id}/copy"
|
31
|
+
params[:parent_id] = parent_id
|
32
|
+
new(http.post(path, params: params))
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.delete(account_id:, file_id:, **params)
|
36
|
+
path = "/accounts/#{account_id}/files/#{file_id}"
|
37
|
+
new(http.delete(path, params: params))
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.recent(account_ids:, **params)
|
41
|
+
path = "/accounts/#{account_ids.join(',')}/recent"
|
42
|
+
Kloudless::Collection.new(self, http.get(path, params: params))
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.search(account_ids:, **params)
|
46
|
+
path = "/accounts/#{account_ids.join(',')}/search"
|
47
|
+
Kloudless::Collection.new(self, http.get(path, params: params))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Kloudless
|
2
|
+
# https://developers.kloudless.com/docs#folders
|
3
|
+
class Folder < Model
|
4
|
+
# https://developers.kloudless.com/docs#folders-create-a-folder
|
5
|
+
def self.create(account_id:, **params)
|
6
|
+
path = "/accounts/#{account_id}/folders"
|
7
|
+
new(http.post(path, params: params))
|
8
|
+
end
|
9
|
+
|
10
|
+
# https://developers.kloudless.com/docs#folders-retrieve-folder-metadata
|
11
|
+
def self.metadata(account_id:, folder_id:)
|
12
|
+
path = "/accounts/#{account_id}/folders/#{folder_id}"
|
13
|
+
new(http.get(path))
|
14
|
+
end
|
15
|
+
|
16
|
+
# https://developers.kloudless.com/docs#folders-retrieve-folder-contents
|
17
|
+
def self.retrieve(account_id:, folder_id:, **params)
|
18
|
+
path = "/accounts/#{account_id}/folders/#{folder_id}/contents"
|
19
|
+
Kloudless::Collection.new(self, http.get(path, params: params))
|
20
|
+
end
|
21
|
+
|
22
|
+
# https://developers.kloudless.com/docs#folders-rename/move-a-folder
|
23
|
+
def self.rename(account_id:, folder_id:, **params)
|
24
|
+
path = "/accounts/#{account_id}/folders/#{folder_id}"
|
25
|
+
new(http.patch(path, params: params))
|
26
|
+
end
|
27
|
+
|
28
|
+
# https://developers.kloudless.com/docs#folders-copy-a-folder
|
29
|
+
def self.copy(account_id:, folder_id:, parent_id:, **params)
|
30
|
+
params[:parent_id] = parent_id
|
31
|
+
path = "/accounts/#{account_id}/folders/#{folder_id}/copy"
|
32
|
+
new(http.post(path, params: params))
|
33
|
+
end
|
34
|
+
|
35
|
+
# https://developers.kloudless.com/docs#folders-delete-a-folder
|
36
|
+
def self.delete(account_id:, folder_id:, **params)
|
37
|
+
path = "/accounts/#{account_id}/folders/#{folder_id}"
|
38
|
+
new(http.delete(path, params: params))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require "net/http"
|
2
|
+
|
3
|
+
module Kloudless
|
4
|
+
# Net::HTTP wrapper
|
5
|
+
class HTTP
|
6
|
+
# Public: Headers global to all requests
|
7
|
+
def self.headers
|
8
|
+
@headers ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.get(path, params: {}, headers: {})
|
12
|
+
uri = URI.parse(Kloudless::API_URL + path)
|
13
|
+
uri.query = URI.encode_www_form(params) if !params.empty?
|
14
|
+
|
15
|
+
request = Net::HTTP::Get.new(uri)
|
16
|
+
request.initialize_http_header(headers)
|
17
|
+
|
18
|
+
execute(request)
|
19
|
+
end
|
20
|
+
|
21
|
+
# TODO: decouple Kloudless::HTTP methods from #execute. Have methods return
|
22
|
+
# request object, defaults to json parsing, but allow the option for raw
|
23
|
+
def self.get_raw(path)
|
24
|
+
uri = URI.parse(Kloudless::API_URL + path)
|
25
|
+
request = Net::HTTP::Get.new(uri)
|
26
|
+
request.initialize_http_header(headers)
|
27
|
+
|
28
|
+
execute(request, parse_json: false)
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def self.post(path, params: {}, headers: {})
|
33
|
+
uri = URI.parse(Kloudless::API_URL + path)
|
34
|
+
headers["Content-Type"] = "application/json"
|
35
|
+
|
36
|
+
request = Net::HTTP::Post.new(uri)
|
37
|
+
request.initialize_http_header(headers)
|
38
|
+
request.set_form_data(params) if !params.empty?
|
39
|
+
|
40
|
+
execute(request)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.patch(path, params: {}, headers: {})
|
44
|
+
uri = URI.parse(Kloudless::API_URL + path)
|
45
|
+
headers["Content-Type"] = "application/json"
|
46
|
+
|
47
|
+
request = Net::HTTP::Post.new(uri)
|
48
|
+
request.initialize_http_header(headers)
|
49
|
+
request.set_form_data(params) if !params.empty?
|
50
|
+
|
51
|
+
execute(request)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.delete(path, params: {}, headers: {})
|
55
|
+
uri = URI.parse(Kloudless::API_URL + path)
|
56
|
+
uri.query = URI.encode_www_form(params) if !params.empty?
|
57
|
+
|
58
|
+
request = Net::HTTP::Delete.new(uri)
|
59
|
+
request.initialize_http_header(headers)
|
60
|
+
|
61
|
+
execute(request)
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.execute(request, parse_json: true)
|
65
|
+
uri = request.uri
|
66
|
+
@last_request = request
|
67
|
+
headers.each {|k,v| request[k] = v}
|
68
|
+
|
69
|
+
response = @mock_response || Net::HTTP.start(uri.hostname, uri.port, use_ssl: (uri.scheme == "https")) {|http|
|
70
|
+
http.request(request)
|
71
|
+
}
|
72
|
+
|
73
|
+
if parse_json
|
74
|
+
json = JSON.parse(response.body)
|
75
|
+
raise Kloudless::Error.from_json(json) if json["error_code"]
|
76
|
+
json
|
77
|
+
else
|
78
|
+
response
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Internal: Returns `response` the next time #execute is invoked for the
|
83
|
+
# duration of `blk`. Used for testing.
|
84
|
+
#
|
85
|
+
# mock = Struct.new(:body).new("{}")
|
86
|
+
# Kloudless::HTTP.mock_response(mock) do
|
87
|
+
# Kloudless::HTTP.get "/foo" # returns {}
|
88
|
+
# end
|
89
|
+
def self.mock_response(response = nil, &blk)
|
90
|
+
@mock_response = response || Struct.new(:body).new("{}")
|
91
|
+
blk.call
|
92
|
+
ensure
|
93
|
+
@mock_response = nil
|
94
|
+
end
|
95
|
+
|
96
|
+
require 'minitest/mock'
|
97
|
+
def self.expect(method, returns: nil, args: [], &blk)
|
98
|
+
begin
|
99
|
+
mock = MiniTest::Mock.new
|
100
|
+
Kloudless.http = mock
|
101
|
+
mock.expect(method, returns, args)
|
102
|
+
blk.call
|
103
|
+
mock.verify
|
104
|
+
ensure
|
105
|
+
Kloudless.http = Kloudless::HTTP
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Internal: Returns the last Net::HTTP request sent by #execute.
|
110
|
+
def self.last_request
|
111
|
+
@last_request
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Kloudless
|
2
|
+
# https://developers.kloudless.com/docs#links
|
3
|
+
class Link < Model
|
4
|
+
def self.list(account_ids:, **params)
|
5
|
+
path = "/accounts/#{account_ids.join(',')}/links"
|
6
|
+
Kloudless::Collection.new(self, http.get(path, params: params))
|
7
|
+
end
|
8
|
+
|
9
|
+
# https://developers.kloudless.com/docs#links-create-a-link
|
10
|
+
def self.create(account_id:, file_id:, **params)
|
11
|
+
params[:file_id] = file_id
|
12
|
+
path = "/accounts/#{account_id}/links"
|
13
|
+
new(http.post(path, params: params))
|
14
|
+
end
|
15
|
+
|
16
|
+
# https://developers.kloudless.com/docs#links-retrieve-a-link
|
17
|
+
def self.retrieve(account_id:, link_id:, **params)
|
18
|
+
path = "/accounts/#{account_id}/links/#{link_id}"
|
19
|
+
new(http.get(path, params: params))
|
20
|
+
end
|
21
|
+
|
22
|
+
# https://developers.kloudless.com/docs#links-update-a-link
|
23
|
+
def self.update(account_id:, link_id:, **params)
|
24
|
+
path = "/accounts/#{account_id}/links/#{link_id}"
|
25
|
+
new(http.patch(path, params: params))
|
26
|
+
end
|
27
|
+
|
28
|
+
# https://developers.kloudless.com/docs#links-delete-a-link
|
29
|
+
def self.delete(account_id:, link_id:)
|
30
|
+
path = "/accounts/#{account_id}/links/#{link_id}"
|
31
|
+
new(http.delete(path))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Kloudless
|
2
|
+
# Public: Base class for different API resources. e.g. Account, Team, Files.
|
3
|
+
class Model
|
4
|
+
def self.http
|
5
|
+
Kloudless.http
|
6
|
+
end
|
7
|
+
|
8
|
+
def http
|
9
|
+
self.class.http
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(attributes = {})
|
13
|
+
@attributes = attributes
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(name, *args, &blk)
|
17
|
+
@attributes[name.to_s] || super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Kloudless
|
2
|
+
# https://developers.kloudless.com/docs#multipart-upload
|
3
|
+
class MultipartUpload < Model
|
4
|
+
def self.init(account_id:, **params)
|
5
|
+
path = "/accounts/#{account_id}/multipart"
|
6
|
+
new(http.post(path, params: params))
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.retrieve(account_id:, multipart_id:)
|
10
|
+
path = "/accounts/#{account_id}/multipart/#{multipart_id}"
|
11
|
+
new(http.get(path))
|
12
|
+
end
|
13
|
+
|
14
|
+
# https://developers.kloudless.com/docs#multipart-upload-upload-part
|
15
|
+
def self.upload(account_id:, multipart_id:, part_number:, **params)
|
16
|
+
path = "/accounts/#{account_id}/multipart/#{multipart_id}"
|
17
|
+
params[:part_number] = part_number
|
18
|
+
new(http.put(path, params: params))
|
19
|
+
end
|
20
|
+
|
21
|
+
# https://developers.kloudless.com/docs#multipart-upload-finalize-multipart-session
|
22
|
+
def self.finalize(account_id:, multipart_id:)
|
23
|
+
path = "/accounts/#{account_id}/multipart/#{multipart_id}/complete"
|
24
|
+
new(http.post(path))
|
25
|
+
end
|
26
|
+
|
27
|
+
# https://developers.kloudless.com/docs#multipart-upload-abort-multipart-session
|
28
|
+
def self.abort(account_id:, multipart_id:)
|
29
|
+
path = "/accounts/#{account_id}/multipart/#{multipart_id}"
|
30
|
+
new(http.delete(path))
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
alias_method :delete, :abort
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/script/changelog
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env sh
|
2
|
+
# Usage: script/changelog [-r <repo>] [-b <base>] [-h <head>]
|
3
|
+
#
|
4
|
+
# repo: base string of GitHub repository url. e.g. "user_or_org/repository". Defaults to git remote url.
|
5
|
+
# base: git ref to compare from. e.g. "v1.3.1". Defaults to latest git tag.
|
6
|
+
# head: git ref to compare to. Defaults to "HEAD".
|
7
|
+
#
|
8
|
+
# Generate a changelog preview from pull requests merged between `base` and
|
9
|
+
# `head`.
|
10
|
+
#
|
11
|
+
set -e
|
12
|
+
|
13
|
+
[ $# -eq 0 ] && set -- --help
|
14
|
+
|
15
|
+
# parse args
|
16
|
+
repo=$(git remote -v | grep push | awk '{print $2}' | cut -d'/' -f4- | sed 's/\.git//')
|
17
|
+
base=$(git tag -l | sort -n | tail -n 1)
|
18
|
+
head="HEAD"
|
19
|
+
api_url="https://api.github.com"
|
20
|
+
|
21
|
+
echo "# $repo $base..$head"
|
22
|
+
echo
|
23
|
+
|
24
|
+
# get merged PR's. Better way is to query the API for these, but this is easier
|
25
|
+
for pr in $(git log --oneline $base..$head | grep "Merge pull request" | awk '{gsub("#",""); print $5}')
|
26
|
+
do
|
27
|
+
# frustrated with trying to pull out the right values, fell back to ruby
|
28
|
+
curl -s "$api_url/repos/$repo/pulls/$pr" | ruby -rjson -e 'pr=JSON.parse(STDIN.read); puts "* #{pr[%q(title)]} [##{pr[%q(number)]}](#{pr[%q(html_url)]})"'
|
29
|
+
done
|
data/script/package
ADDED
data/script/release
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
# Usage: script/release
|
3
|
+
# Build the package, tag a commit, push it to origin, and then release the
|
4
|
+
# package publicly.
|
5
|
+
|
6
|
+
set -e
|
7
|
+
|
8
|
+
version="$(script/package | grep Version: | awk '{print $2}')"
|
9
|
+
[ -n "$version" ] || exit 1
|
10
|
+
|
11
|
+
echo $version
|
12
|
+
git commit --allow-empty -a -m "Release $version"
|
13
|
+
git tag "v$version"
|
14
|
+
git push origin
|
15
|
+
git push origin "v$version"
|
16
|
+
gem push pkg/*-${version}.gem
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
require 'minitest'
|
3
|
+
|
4
|
+
class Kloudless::AccountKeyTest < Minitest::Test
|
5
|
+
def test_list_account_keys
|
6
|
+
Kloudless.http.expect(:get, returns: {"objects" => [{}]}, args: ["/accounts/1,2/keys", params: {}]) do
|
7
|
+
account_keys = Kloudless::AccountKey.list(account_ids: [1,2])
|
8
|
+
assert_kind_of Kloudless::Collection, account_keys
|
9
|
+
assert_kind_of Kloudless::AccountKey, account_keys.first
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_retrieve_account_key
|
14
|
+
Kloudless.http.expect(:get, args: ["/accounts/1/keys/2", params: {}]) do
|
15
|
+
account_key = Kloudless::AccountKey.retrieve(account_id: 1, key_id: 2)
|
16
|
+
assert_kind_of Kloudless::AccountKey, account_key
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_delete_account_key
|
21
|
+
Kloudless.http.expect(:delete, args: ["/accounts/1/keys/2"]) do
|
22
|
+
account_key = Kloudless::AccountKey.delete(account_id: 1, key_id: 2)
|
23
|
+
assert_kind_of Kloudless::AccountKey, account_key
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
|
3
|
+
class Kloudless::AccountTest < Minitest::Test
|
4
|
+
def test_list_accounts
|
5
|
+
json = JSON.parse <<-JSON
|
6
|
+
{
|
7
|
+
"total": 2,
|
8
|
+
"count": 2,
|
9
|
+
"page": 1,
|
10
|
+
"objects": [
|
11
|
+
{
|
12
|
+
"id": 90817234,
|
13
|
+
"account": "someone@gmail.com",
|
14
|
+
"service": "box",
|
15
|
+
"service_name": "Box",
|
16
|
+
"admin": false,
|
17
|
+
"active": true,
|
18
|
+
"created": "2015-03-17T20:42:17.954885Z",
|
19
|
+
"modified": "2015-03-17T20:42:17.954885Z",
|
20
|
+
"token_expiry": "2015-04-17T20:42:17.954885Z",
|
21
|
+
"refresh_token_expiry": null
|
22
|
+
},
|
23
|
+
{
|
24
|
+
"id": 6535892,
|
25
|
+
"account": "someone-else@gmail.com",
|
26
|
+
"service": "egnyte",
|
27
|
+
"service_name": "Egnyte",
|
28
|
+
"admin": false,
|
29
|
+
"active": true,
|
30
|
+
"created": "2015-03-17T20:42:18.627533Z",
|
31
|
+
"modified": "2015-03-17T20:42:18.627533Z",
|
32
|
+
"token_expiry": "2015-04-17T20:42:18.627533Z",
|
33
|
+
"refresh_token_expiry": null
|
34
|
+
}
|
35
|
+
]
|
36
|
+
}
|
37
|
+
JSON
|
38
|
+
|
39
|
+
Kloudless.http.expect(:get, returns: json, args: ["/accounts", {params: {}}]) do
|
40
|
+
accounts = Kloudless::Account.list
|
41
|
+
assert_kind_of Kloudless::Collection, accounts
|
42
|
+
assert_kind_of Kloudless::Account, accounts.first
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_import_account
|
47
|
+
attributes = {
|
48
|
+
account: "someone@gmail.com",
|
49
|
+
service: "copy",
|
50
|
+
token: "foo",
|
51
|
+
token_secret: "bar"
|
52
|
+
}
|
53
|
+
Kloudless.http.expect(:post, args: ["/accounts", {params: attributes}]) do
|
54
|
+
Kloudless::Account.import(attributes)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_retrieve_account
|
59
|
+
Kloudless.http.expect(:get, args: ["/accounts/1", {params: {active: false}}]) do
|
60
|
+
account = Kloudless::Account.retrieve(account_id: 1, active: false)
|
61
|
+
assert_kind_of Kloudless::Account, account
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_update_account
|
66
|
+
Kloudless.http.expect(:patch, args: ["/accounts/1", {params: {active: true}}]) do
|
67
|
+
account = Kloudless::Account.update(account_id: 1, active: true)
|
68
|
+
assert_kind_of Kloudless::Account, account
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_delete_account
|
73
|
+
Kloudless.http.expect(:delete, args: ["/accounts/1"]) do
|
74
|
+
account = Kloudless::Account.delete(account_id: 1)
|
75
|
+
assert_kind_of Kloudless::Account, account
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|