esearch 0.2.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.
- data/.gitignore +5 -0
- data/.rspec +6 -0
- data/.travis.yml +19 -0
- data/Changelog.md +27 -0
- data/Gemfile +6 -0
- data/Gemfile.devtools +60 -0
- data/Guardfile +18 -0
- data/LICENSE +20 -0
- data/README.md +93 -0
- data/Rakefile +2 -0
- data/TODO +3 -0
- data/config/devtools.yml +2 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +3 -0
- data/config/mutant.yml +3 -0
- data/config/reek.yml +103 -0
- data/config/yardstick.yml +2 -0
- data/esearch.gemspec +26 -0
- data/lib/esearch.rb +43 -0
- data/lib/esearch/cluster.rb +85 -0
- data/lib/esearch/command.rb +158 -0
- data/lib/esearch/command/cluster.rb +28 -0
- data/lib/esearch/command/document.rb +111 -0
- data/lib/esearch/command/exist.rb +37 -0
- data/lib/esearch/command/index.rb +66 -0
- data/lib/esearch/command/search.rb +24 -0
- data/lib/esearch/command/status.rb +24 -0
- data/lib/esearch/connection.rb +36 -0
- data/lib/esearch/document.rb +53 -0
- data/lib/esearch/index.rb +61 -0
- data/lib/esearch/indices.rb +52 -0
- data/lib/esearch/mixin.rb +111 -0
- data/lib/esearch/presenter.rb +43 -0
- data/lib/esearch/presenter/aspect.rb +17 -0
- data/lib/esearch/presenter/aspect/range.rb +63 -0
- data/lib/esearch/presenter/aspect/term.rb +19 -0
- data/lib/esearch/presenter/cluster.rb +84 -0
- data/lib/esearch/presenter/document.rb +102 -0
- data/lib/esearch/presenter/facet.rb +72 -0
- data/lib/esearch/presenter/hit.rb +70 -0
- data/lib/esearch/presenter/hits.rb +60 -0
- data/lib/esearch/presenter/index.rb +23 -0
- data/lib/esearch/presenter/search.rb +32 -0
- data/lib/esearch/presenter/status.rb +18 -0
- data/lib/esearch/request.rb +90 -0
- data/lib/esearch/type.rb +40 -0
- data/spec/integration/esearch/spike_spec.rb +50 -0
- data/spec/spec_helper.rb +65 -0
- data/spec/support/example_group_methods.rb +7 -0
- data/spec/support/ice_nine_config.rb +6 -0
- data/spec/unit/esearch/cluster/class_methods/connect_spec.rb +16 -0
- data/spec/unit/esearch/cluster/health_spec.rb +10 -0
- data/spec/unit/esearch/cluster/index_spec.rb +11 -0
- data/spec/unit/esearch/cluster/indices_spec.rb +11 -0
- data/spec/unit/esearch/cluster/path_spec.rb +11 -0
- data/spec/unit/esearch/command/class_methods/run_spec.rb +16 -0
- data/spec/unit/esearch/command/cluster/health/run_spec.rb +14 -0
- data/spec/unit/esearch/command/document/delete/run_spec.rb +13 -0
- data/spec/unit/esearch/command/document/get/result_spec.rb +27 -0
- data/spec/unit/esearch/command/document/index/create/run_spec.rb +17 -0
- data/spec/unit/esearch/command/document/index/run_create_spec.rb +17 -0
- data/spec/unit/esearch/command/document/index/run_spec.rb +15 -0
- data/spec/unit/esearch/command/document/index/run_update_spec.rb +15 -0
- data/spec/unit/esearch/command/document/index/update/run_spec.rb +15 -0
- data/spec/unit/esearch/command/exist/result_spec.rb +39 -0
- data/spec/unit/esearch/command/index/create/run_spec.rb +14 -0
- data/spec/unit/esearch/command/index/delete/run_spec.rb +13 -0
- data/spec/unit/esearch/command/index/refresh/run_spec.rb +13 -0
- data/spec/unit/esearch/command/result_spec.rb +68 -0
- data/spec/unit/esearch/command/search/run_spec.rb +14 -0
- data/spec/unit/esearch/command/status/run_spec.rb +13 -0
- data/spec/unit/esearch/connection/class_methods/build_spec.rb +29 -0
- data/spec/unit/esearch/connection/run_spec.rb +36 -0
- data/spec/unit/esearch/document/connection_spec.rb +12 -0
- data/spec/unit/esearch/document/delete_spec.rb +12 -0
- data/spec/unit/esearch/document/get_spec.rb +12 -0
- data/spec/unit/esearch/index/create_spec.rb +12 -0
- data/spec/unit/esearch/index/delete_spec.rb +11 -0
- data/spec/unit/esearch/index/type_spec.rb +12 -0
- data/spec/unit/esearch/indices/all/path_spec.rb +12 -0
- data/spec/unit/esearch/mixin/document/index_create_spec.rb +31 -0
- data/spec/unit/esearch/mixin/document/index_spec.rb +31 -0
- data/spec/unit/esearch/mixin/document/index_update_spec.rb +31 -0
- data/spec/unit/esearch/mixin/exist/exist_predicate_spec.rb +16 -0
- data/spec/unit/esearch/mixin/index/refresh_spec.rb +16 -0
- data/spec/unit/esearch/mixin/index/status_spec.rb +16 -0
- data/spec/unit/esearch/mixin/search/search_spec.rb +18 -0
- data/spec/unit/esearch/presenter/aspect/range/from_spec.rb +24 -0
- data/spec/unit/esearch/presenter/aspect/range/to_spec.rb +24 -0
- data/spec/unit/esearch/presenter/class_methods/new_spec.rb +37 -0
- data/spec/unit/esearch/presenter/facet/build_spec.rb +26 -0
- data/spec/unit/esearch/presenter/facet/class_methods/build_spec.rb +26 -0
- data/spec/unit/esearch/presenter/hit/fields_spec.rb +24 -0
- data/spec/unit/esearch/presenter/hit/source_spec.rb +24 -0
- data/spec/unit/esearch/presenter/hits/each_spec.rb +15 -0
- data/spec/unit/esearch/presenter/hits/size_spec.rb +13 -0
- data/spec/unit/esearch/request/initialize_spec.rb +39 -0
- data/spec/unit/esearch/request/run_spec.rb +39 -0
- data/spec/unit/esearch/type/connection_spec.rb +15 -0
- data/spec/unit/esearch/type/document_spec.rb +12 -0
- metadata +330 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module Esearch
|
|
2
|
+
# Handle for an elasticsearch cluster
|
|
3
|
+
class Cluster
|
|
4
|
+
|
|
5
|
+
include Adamantium::Flat, Concord.new(:connection)
|
|
6
|
+
|
|
7
|
+
# Return connection
|
|
8
|
+
#
|
|
9
|
+
# @return [Connection]
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
#
|
|
13
|
+
attr_reader :connection
|
|
14
|
+
|
|
15
|
+
# Return handler for index
|
|
16
|
+
#
|
|
17
|
+
# @param [String] name
|
|
18
|
+
#
|
|
19
|
+
# @return [Index]
|
|
20
|
+
#
|
|
21
|
+
# @api private
|
|
22
|
+
#
|
|
23
|
+
def index(name)
|
|
24
|
+
Index.new(connection, name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Return handler for indices
|
|
28
|
+
#
|
|
29
|
+
# @param [Enumerable<String>] names
|
|
30
|
+
#
|
|
31
|
+
# @return [Indices]
|
|
32
|
+
#
|
|
33
|
+
# @api private
|
|
34
|
+
#
|
|
35
|
+
def indices(names)
|
|
36
|
+
Indices.new(connection, names)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Return cluster health
|
|
40
|
+
#
|
|
41
|
+
# @param [Hash] options
|
|
42
|
+
#
|
|
43
|
+
# @return [Presenter::Health]
|
|
44
|
+
#
|
|
45
|
+
# @api private
|
|
46
|
+
#
|
|
47
|
+
def health(options = {})
|
|
48
|
+
Command::Cluster::Health.run(self, options)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Return path
|
|
52
|
+
#
|
|
53
|
+
# @return [Pathname]
|
|
54
|
+
#
|
|
55
|
+
# @api private
|
|
56
|
+
#
|
|
57
|
+
def path
|
|
58
|
+
self.class::PATH
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
PATH = Pathname.new('/')
|
|
62
|
+
|
|
63
|
+
# Return handler for all indices
|
|
64
|
+
#
|
|
65
|
+
# @return [Indices::All]
|
|
66
|
+
#
|
|
67
|
+
# @api private
|
|
68
|
+
#
|
|
69
|
+
def all_indices
|
|
70
|
+
Indices::All.new(connection)
|
|
71
|
+
end
|
|
72
|
+
memoize :all_indices
|
|
73
|
+
|
|
74
|
+
# Build connection
|
|
75
|
+
#
|
|
76
|
+
# @return [Connection]
|
|
77
|
+
#
|
|
78
|
+
# @api private
|
|
79
|
+
#
|
|
80
|
+
def self.connect(*args)
|
|
81
|
+
new(Connection.build(*args))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
module Esearch
|
|
2
|
+
# Abstract base class for elasticsearch commands
|
|
3
|
+
class Command
|
|
4
|
+
include Adamantium::Flat, AbstractType, Concord.new(:context)
|
|
5
|
+
|
|
6
|
+
EXPECTED_STATI = [ 200 ].freeze
|
|
7
|
+
JSON_CONTENT_TYPE = 'application/json; charset=UTF-8'.freeze
|
|
8
|
+
|
|
9
|
+
# Run command
|
|
10
|
+
#
|
|
11
|
+
# @return [Object]
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
#
|
|
15
|
+
def self.run(*args)
|
|
16
|
+
new(*args).result
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Return result of request
|
|
20
|
+
#
|
|
21
|
+
# @return [Presenter]
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
#
|
|
25
|
+
def result
|
|
26
|
+
assert_success
|
|
27
|
+
presenter.new(parsed_json)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Test for json content type
|
|
33
|
+
#
|
|
34
|
+
# @return [true]
|
|
35
|
+
# if content type is json
|
|
36
|
+
#
|
|
37
|
+
# @return [false]
|
|
38
|
+
# otherwise
|
|
39
|
+
#
|
|
40
|
+
# @api private
|
|
41
|
+
#
|
|
42
|
+
def json_content_type?
|
|
43
|
+
content_type.eql?(JSON_CONTENT_TYPE)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Return presenter
|
|
47
|
+
#
|
|
48
|
+
# @return [Class:Presenter]
|
|
49
|
+
#
|
|
50
|
+
# @api private
|
|
51
|
+
#
|
|
52
|
+
def presenter
|
|
53
|
+
self.class::PRESENTER
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Return connection
|
|
57
|
+
#
|
|
58
|
+
# @return [Faraday::Connection]
|
|
59
|
+
#
|
|
60
|
+
# @api private
|
|
61
|
+
#
|
|
62
|
+
def connection
|
|
63
|
+
context.connection
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Return context path
|
|
67
|
+
#
|
|
68
|
+
# @return [Pathname]
|
|
69
|
+
#
|
|
70
|
+
# @api private
|
|
71
|
+
#
|
|
72
|
+
def context_path
|
|
73
|
+
context.path
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Return response content type
|
|
77
|
+
#
|
|
78
|
+
# @return [String]
|
|
79
|
+
#
|
|
80
|
+
# @api private
|
|
81
|
+
#
|
|
82
|
+
def content_type
|
|
83
|
+
response.headers['content-type']
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Return expected response stati
|
|
87
|
+
#
|
|
88
|
+
# @return [Enumerable<Fixnum>]
|
|
89
|
+
#
|
|
90
|
+
# @api private
|
|
91
|
+
#
|
|
92
|
+
def expected_response_stati
|
|
93
|
+
self.class::EXPECTED_STATI
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Test for success
|
|
97
|
+
#
|
|
98
|
+
# @return [undefined]
|
|
99
|
+
#
|
|
100
|
+
# @api private
|
|
101
|
+
#
|
|
102
|
+
def assert_success
|
|
103
|
+
unless expected_response_stati.include?(response.status)
|
|
104
|
+
raise_status_error
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Raise remote status error
|
|
109
|
+
#
|
|
110
|
+
# @return [undefined]
|
|
111
|
+
#
|
|
112
|
+
# @api private
|
|
113
|
+
#
|
|
114
|
+
def raise_status_error
|
|
115
|
+
raise ProtocolError, "expected response stati: #{expected_response_stati.inspect} but got: #{response.status}, remote message: #{remote_message.inspect}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Return remote message
|
|
119
|
+
#
|
|
120
|
+
# @return [String]
|
|
121
|
+
#
|
|
122
|
+
# @api private
|
|
123
|
+
#
|
|
124
|
+
def remote_message
|
|
125
|
+
if json_content_type?
|
|
126
|
+
parsed_json
|
|
127
|
+
else
|
|
128
|
+
response.body
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Return parsed json
|
|
133
|
+
#
|
|
134
|
+
# @return [Hash]
|
|
135
|
+
#
|
|
136
|
+
# @api private
|
|
137
|
+
#
|
|
138
|
+
def parsed_json
|
|
139
|
+
unless json_content_type?
|
|
140
|
+
raise ProtocolError, "Expected json content type, but got: #{content_type.inspect}"
|
|
141
|
+
end
|
|
142
|
+
MultiJson.load(response.body)
|
|
143
|
+
end
|
|
144
|
+
memoize :parsed_json
|
|
145
|
+
|
|
146
|
+
# Return response
|
|
147
|
+
#
|
|
148
|
+
# @return [Faraday::Response]
|
|
149
|
+
#
|
|
150
|
+
# @api private
|
|
151
|
+
#
|
|
152
|
+
def response
|
|
153
|
+
connection.run(request)
|
|
154
|
+
end
|
|
155
|
+
memoize :response
|
|
156
|
+
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Esearch
|
|
2
|
+
class Command
|
|
3
|
+
# Base class for cluster commands
|
|
4
|
+
class Cluster < self
|
|
5
|
+
|
|
6
|
+
# Command for accessing health
|
|
7
|
+
class Health < self
|
|
8
|
+
include Concord.new(:context, :options)
|
|
9
|
+
|
|
10
|
+
PRESENTER = Presenter::Cluster::Health
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# Return request
|
|
15
|
+
#
|
|
16
|
+
# @return [Request]
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
#
|
|
20
|
+
def request
|
|
21
|
+
Request.get('/_cluster/health', {}, options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
module Esearch
|
|
2
|
+
class Command
|
|
3
|
+
|
|
4
|
+
# Base class for commands on documents
|
|
5
|
+
class Document < self
|
|
6
|
+
|
|
7
|
+
# Document index command
|
|
8
|
+
class Index < self
|
|
9
|
+
include Concord.new(:context, :document, :options)
|
|
10
|
+
|
|
11
|
+
EXPECTED_STATI = [ 200, 201 ].freeze
|
|
12
|
+
PRESENTER = Presenter::Document::Operation::Index
|
|
13
|
+
|
|
14
|
+
FORCE_OPTIONS = {}
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# Return request
|
|
19
|
+
#
|
|
20
|
+
# @return [Request]
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
#
|
|
24
|
+
def request
|
|
25
|
+
Request.post(context_path, document, effective_options)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Return effective options
|
|
29
|
+
#
|
|
30
|
+
# @return [Hash]
|
|
31
|
+
#
|
|
32
|
+
# @api private
|
|
33
|
+
#
|
|
34
|
+
def effective_options
|
|
35
|
+
options.merge(self.class::FORCE_OPTIONS)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Document index update command
|
|
39
|
+
class Update < self
|
|
40
|
+
EXPECTED_STATI = [ 200 ].freeze
|
|
41
|
+
FORCE_OPTIONS = { :op_type => :index }.freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Document index create command
|
|
45
|
+
class Create < self
|
|
46
|
+
EXPECTED_STATI = [ 201 ].freeze
|
|
47
|
+
FORCE_OPTIONS = { :op_type => :create }.freeze
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Present get document command result
|
|
52
|
+
class Get < self
|
|
53
|
+
|
|
54
|
+
EXPECTED_STATI = [ 200, 404 ].freeze
|
|
55
|
+
PRESENTER = Presenter::Document::Get
|
|
56
|
+
|
|
57
|
+
# Return result
|
|
58
|
+
#
|
|
59
|
+
# @return [true]
|
|
60
|
+
# if object exists
|
|
61
|
+
#
|
|
62
|
+
# @return [false]
|
|
63
|
+
# otherwise
|
|
64
|
+
#
|
|
65
|
+
# @api private
|
|
66
|
+
#
|
|
67
|
+
def result
|
|
68
|
+
assert_success
|
|
69
|
+
if response.status == 200
|
|
70
|
+
presenter.new(parsed_json)
|
|
71
|
+
else
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Return request
|
|
79
|
+
#
|
|
80
|
+
# @return [Request]
|
|
81
|
+
#
|
|
82
|
+
# @api private
|
|
83
|
+
#
|
|
84
|
+
def request
|
|
85
|
+
Request.get(context_path)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Present delete document command result
|
|
91
|
+
class Delete < self
|
|
92
|
+
|
|
93
|
+
PRESENTER = Presenter::Document::Operation::Delete
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# Return request
|
|
98
|
+
#
|
|
99
|
+
# @return [Request]
|
|
100
|
+
#
|
|
101
|
+
# @api private
|
|
102
|
+
#
|
|
103
|
+
def request
|
|
104
|
+
Request.delete(context_path)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Esearch
|
|
2
|
+
class Command
|
|
3
|
+
# Command to check existance of objects
|
|
4
|
+
class Exist < self
|
|
5
|
+
|
|
6
|
+
EXPECTED_STATI = [ 200, 404 ].freeze
|
|
7
|
+
|
|
8
|
+
# Return result
|
|
9
|
+
#
|
|
10
|
+
# @return [true]
|
|
11
|
+
# if object exists
|
|
12
|
+
#
|
|
13
|
+
# @return [false]
|
|
14
|
+
# otherwise
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
#
|
|
18
|
+
def result
|
|
19
|
+
assert_success
|
|
20
|
+
response.status == 200
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Return request
|
|
26
|
+
#
|
|
27
|
+
# @return [Request]
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
#
|
|
31
|
+
def request
|
|
32
|
+
Request.head(context_path)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Esearch
|
|
2
|
+
class Command
|
|
3
|
+
# Base class for commands on index
|
|
4
|
+
class Index < self
|
|
5
|
+
|
|
6
|
+
# Create index comand
|
|
7
|
+
class Create < self
|
|
8
|
+
include Concord.new(:context, :settings)
|
|
9
|
+
|
|
10
|
+
EXPECT_STATUS = [ 201 ].freeze
|
|
11
|
+
PRESENTER = Presenter::Index::Create
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# Return request
|
|
16
|
+
#
|
|
17
|
+
# @return [Request]
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
#
|
|
21
|
+
def request
|
|
22
|
+
Request.put(context_path, settings)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Delete index comand
|
|
28
|
+
class Delete < self
|
|
29
|
+
|
|
30
|
+
PRESENTER = Presenter::Index::Delete
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Return request
|
|
35
|
+
#
|
|
36
|
+
# @return [Request]
|
|
37
|
+
#
|
|
38
|
+
# @api private
|
|
39
|
+
#
|
|
40
|
+
def request
|
|
41
|
+
Request.delete(context_path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Refresh index command
|
|
47
|
+
class Refresh < self
|
|
48
|
+
|
|
49
|
+
PRESENTER = Presenter::Index::Refresh
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Return request
|
|
54
|
+
#
|
|
55
|
+
# @return [Request]
|
|
56
|
+
#
|
|
57
|
+
# @api private
|
|
58
|
+
#
|
|
59
|
+
def request
|
|
60
|
+
Request.post(context_path.join('_refresh'))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|