elasticsearch-persistence 5.0.2 → 6.1.1
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/.rspec +2 -0
- data/Gemfile +9 -0
- data/README.md +206 -338
- data/Rakefile +15 -12
- data/elasticsearch-persistence.gemspec +6 -7
- data/examples/notes/application.rb +3 -4
- data/lib/elasticsearch/persistence.rb +2 -110
- data/lib/elasticsearch/persistence/repository.rb +212 -53
- data/lib/elasticsearch/persistence/repository/dsl.rb +94 -0
- data/lib/elasticsearch/persistence/repository/find.rb +27 -10
- data/lib/elasticsearch/persistence/repository/response/results.rb +21 -8
- data/lib/elasticsearch/persistence/repository/search.rb +30 -18
- data/lib/elasticsearch/persistence/repository/serialize.rb +65 -7
- data/lib/elasticsearch/persistence/repository/store.rb +38 -44
- data/lib/elasticsearch/persistence/version.rb +1 -1
- data/spec/repository/find_spec.rb +179 -0
- data/spec/repository/response/results_spec.rb +128 -0
- data/spec/repository/search_spec.rb +181 -0
- data/spec/repository/serialize_spec.rb +53 -0
- data/spec/repository/store_spec.rb +327 -0
- data/spec/repository_spec.rb +723 -0
- data/spec/spec_helper.rb +32 -0
- metadata +26 -104
- data/examples/music/album.rb +0 -54
- data/examples/music/artist.rb +0 -70
- data/examples/music/artists/_form.html.erb +0 -8
- data/examples/music/artists/artists_controller.rb +0 -67
- data/examples/music/artists/artists_controller_test.rb +0 -53
- data/examples/music/artists/index.html.erb +0 -60
- data/examples/music/artists/show.html.erb +0 -54
- data/examples/music/assets/application.css +0 -257
- data/examples/music/assets/autocomplete.css +0 -48
- data/examples/music/assets/blank_artist.png +0 -0
- data/examples/music/assets/blank_cover.png +0 -0
- data/examples/music/assets/form.css +0 -113
- data/examples/music/index_manager.rb +0 -73
- data/examples/music/search/index.html.erb +0 -95
- data/examples/music/search/search_controller.rb +0 -41
- data/examples/music/search/search_controller_test.rb +0 -12
- data/examples/music/search/search_helper.rb +0 -15
- data/examples/music/suggester.rb +0 -69
- data/examples/music/template.rb +0 -430
- data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css +0 -7
- data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js +0 -6
- data/examples/music/vendor/assets/stylesheets/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- data/lib/elasticsearch/persistence/client.rb +0 -51
- data/lib/elasticsearch/persistence/model.rb +0 -135
- data/lib/elasticsearch/persistence/model/base.rb +0 -87
- data/lib/elasticsearch/persistence/model/errors.rb +0 -8
- data/lib/elasticsearch/persistence/model/find.rb +0 -180
- data/lib/elasticsearch/persistence/model/rails.rb +0 -47
- data/lib/elasticsearch/persistence/model/store.rb +0 -254
- data/lib/elasticsearch/persistence/model/utils.rb +0 -0
- data/lib/elasticsearch/persistence/repository/class.rb +0 -71
- data/lib/elasticsearch/persistence/repository/naming.rb +0 -115
- data/lib/rails/generators/elasticsearch/model/model_generator.rb +0 -21
- data/lib/rails/generators/elasticsearch/model/templates/model.rb.tt +0 -9
- data/lib/rails/generators/elasticsearch_generator.rb +0 -2
- data/test/integration/model/model_basic_test.rb +0 -233
- data/test/integration/repository/custom_class_test.rb +0 -85
- data/test/integration/repository/customized_class_test.rb +0 -82
- data/test/integration/repository/default_class_test.rb +0 -116
- data/test/integration/repository/virtus_model_test.rb +0 -118
- data/test/test_helper.rb +0 -55
- data/test/unit/model_base_test.rb +0 -72
- data/test/unit/model_find_test.rb +0 -153
- data/test/unit/model_gateway_test.rb +0 -101
- data/test/unit/model_rails_test.rb +0 -112
- data/test/unit/model_store_test.rb +0 -576
- data/test/unit/persistence_test.rb +0 -32
- data/test/unit/repository_class_test.rb +0 -51
- data/test/unit/repository_client_test.rb +0 -32
- data/test/unit/repository_find_test.rb +0 -388
- data/test/unit/repository_indexing_test.rb +0 -37
- data/test/unit/repository_module_test.rb +0 -146
- data/test/unit/repository_naming_test.rb +0 -146
- data/test/unit/repository_response_results_test.rb +0 -98
- data/test/unit/repository_search_test.rb +0 -117
- data/test/unit/repository_serialize_test.rb +0 -57
- data/test/unit/repository_store_test.rb +0 -303
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Elasticsearch::Persistence::Repository::Serialize do
|
4
|
+
|
5
|
+
let(:repository) do
|
6
|
+
DEFAULT_REPOSITORY
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#serialize' do
|
10
|
+
|
11
|
+
before do
|
12
|
+
class MyDocument
|
13
|
+
def to_hash
|
14
|
+
{ a: 1 }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'calls #to_hash on the object' do
|
20
|
+
expect(repository.serialize(MyDocument.new)).to eq(a: 1)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#deserialize' do
|
25
|
+
|
26
|
+
context 'when klass is defined on the Repository' do
|
27
|
+
|
28
|
+
let(:repository) do
|
29
|
+
require 'set'
|
30
|
+
MyTestRepository.new(klass: Set)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'instantiates an object of the klass' do
|
34
|
+
expect(repository.deserialize('_source' => { a: 1 })).to be_a(Set)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'uses the source field to instantiate the object' do
|
38
|
+
expect(repository.deserialize('_source' => { a: 1 })).to eq(Set.new({ a: 1}))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'when klass is not defined on the Repository' do
|
43
|
+
|
44
|
+
it 'returns the raw Hash' do
|
45
|
+
expect(repository.deserialize('_source' => { a: 1 })).to be_a(Hash)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'uses the source field to instantiate the object' do
|
49
|
+
expect(repository.deserialize('_source' => { a: 1 })).to eq(a: 1)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,327 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Elasticsearch::Persistence::Repository::Store do
|
4
|
+
|
5
|
+
let(:repository) do
|
6
|
+
DEFAULT_REPOSITORY
|
7
|
+
end
|
8
|
+
|
9
|
+
after do
|
10
|
+
begin; repository.delete_index!; rescue; end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#save' do
|
14
|
+
|
15
|
+
let(:document) do
|
16
|
+
{ a: 1 }
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:response) do
|
20
|
+
repository.save(document)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'saves the document' do
|
24
|
+
expect(repository.find(response['_id'])).to eq('a' => 1)
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'when the repository defines a custom serialize method' do
|
28
|
+
|
29
|
+
before do
|
30
|
+
class OtherNoteRepository
|
31
|
+
include Elasticsearch::Persistence::Repository
|
32
|
+
def serialize(document)
|
33
|
+
{ b: 1 }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
after do
|
39
|
+
if defined?(OtherNoteRepository)
|
40
|
+
Object.send(:remove_const, OtherNoteRepository.name)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
let(:repository) do
|
45
|
+
OtherNoteRepository.new(client: DEFAULT_CLIENT)
|
46
|
+
end
|
47
|
+
|
48
|
+
let(:response) do
|
49
|
+
repository.save(document)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'saves the document' do
|
53
|
+
expect(repository.find(response['_id'])).to eq('b' => 1)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when options are provided' do
|
58
|
+
|
59
|
+
let!(:response) do
|
60
|
+
repository.save(document, type: 'other_note')
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'saves the document using the options' do
|
64
|
+
expect {
|
65
|
+
repository.find(response['_id'])
|
66
|
+
}.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound)
|
67
|
+
expect(repository.find(response['_id'], type: 'other_note')).to eq('a' => 1)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#update' do
|
73
|
+
|
74
|
+
before(:all) do
|
75
|
+
class Note
|
76
|
+
def to_hash
|
77
|
+
{ text: 'testing', views: 0 }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
after(:all) do
|
83
|
+
if defined?(Note)
|
84
|
+
Object.send(:remove_const, :Note)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'when the document exists' do
|
89
|
+
|
90
|
+
let!(:id) do
|
91
|
+
repository.save(Note.new)['_id']
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'when an id is provided' do
|
95
|
+
|
96
|
+
context 'when a doc is specified in the options' do
|
97
|
+
|
98
|
+
before do
|
99
|
+
repository.update(id, doc: { text: 'testing_2' })
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'updates using the doc parameter' do
|
103
|
+
expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'when a script is specified in the options' do
|
108
|
+
|
109
|
+
before do
|
110
|
+
repository.update(id, script: { inline: 'ctx._source.views += 1' })
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'updates using the script parameter' do
|
114
|
+
expect(repository.find(id)).to eq('text' => 'testing', 'views' => 1)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'when params are specified in the options' do
|
119
|
+
|
120
|
+
before do
|
121
|
+
repository.update(id, script: { inline: 'ctx._source.views += params.count',
|
122
|
+
params: { count: 2 } })
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'updates using the script parameter' do
|
126
|
+
expect(repository.find(id)).to eq('text' => 'testing', 'views' => 2)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context 'when upsert is specified in the options' do
|
131
|
+
|
132
|
+
before do
|
133
|
+
repository.update(id, script: { inline: 'ctx._source.views += 1' },
|
134
|
+
upsert: { text: 'testing_2' })
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'executes the script' do
|
138
|
+
expect(repository.find(id)).to eq('text' => 'testing', 'views' => 1)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'when doc_as_upsert is specified in the options' do
|
143
|
+
|
144
|
+
before do
|
145
|
+
repository.update(id, doc: { text: 'testing_2' },
|
146
|
+
doc_as_upsert: true)
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'performs an upsert' do
|
150
|
+
expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'when a document is provided as the query criteria' do
|
156
|
+
|
157
|
+
context 'when no options are provided' do
|
158
|
+
|
159
|
+
before do
|
160
|
+
repository.update(id: id, text: 'testing_2')
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'updates using the id and the document as the doc parameter' do
|
164
|
+
expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context 'when options are provided' do
|
169
|
+
|
170
|
+
context 'when a doc is specified in the options' do
|
171
|
+
|
172
|
+
before do
|
173
|
+
repository.update({ id: id, text: 'testing' }, doc: { text: 'testing_2' })
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'updates using the id and the doc in the options' do
|
177
|
+
expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
context 'when a script is specified in the options' do
|
182
|
+
|
183
|
+
before do
|
184
|
+
repository.update({ id: id, text: 'testing' },
|
185
|
+
script: { inline: 'ctx._source.views += 1' })
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'updates using the id and script from the options' do
|
189
|
+
expect(repository.find(id)).to eq('text' => 'testing', 'views' => 1)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context 'when params are specified in the options' do
|
194
|
+
|
195
|
+
before do
|
196
|
+
repository.update({ id: id, text: 'testing' },
|
197
|
+
script: { inline: 'ctx._source.views += params.count',
|
198
|
+
params: { count: 2 } })
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'updates using the id and script and params from the options' do
|
202
|
+
expect(repository.find(id)).to eq('text' => 'testing', 'views' => 2)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
context 'when upsert is specified in the options' do
|
207
|
+
|
208
|
+
before do
|
209
|
+
repository.update({ id: id, text: 'testing_2' },
|
210
|
+
doc_as_upsert: true)
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'updates using the id and script and params from the options' do
|
214
|
+
expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
context 'when the document does not exist' do
|
222
|
+
|
223
|
+
context 'when an id is provided 'do
|
224
|
+
|
225
|
+
it 'raises an exception' do
|
226
|
+
expect {
|
227
|
+
repository.update(1, doc: { text: 'testing_2' })
|
228
|
+
}.to raise_exception(Elasticsearch::Transport::Transport::Errors::NotFound)
|
229
|
+
end
|
230
|
+
|
231
|
+
context 'when upsert is provided' do
|
232
|
+
|
233
|
+
before do
|
234
|
+
repository.update(1, doc: { text: 'testing' }, doc_as_upsert: true)
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'upserts the document' do
|
238
|
+
expect(repository.find(1)).to eq('text' => 'testing')
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
context 'when a document is provided' do
|
244
|
+
|
245
|
+
it 'raises an exception' do
|
246
|
+
expect {
|
247
|
+
repository.update(id: 1, text: 'testing_2')
|
248
|
+
}.to raise_exception(Elasticsearch::Transport::Transport::Errors::NotFound)
|
249
|
+
end
|
250
|
+
|
251
|
+
context 'when upsert is provided' do
|
252
|
+
|
253
|
+
before do
|
254
|
+
repository.update({ id: 1, text: 'testing' }, doc_as_upsert: true)
|
255
|
+
end
|
256
|
+
|
257
|
+
it 'upserts the document' do
|
258
|
+
expect(repository.find(1)).to eq('text' => 'testing')
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
describe '#delete' do
|
266
|
+
|
267
|
+
before(:all) do
|
268
|
+
class Note
|
269
|
+
def to_hash
|
270
|
+
{ text: 'testing', views: 0 }
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
after(:all) do
|
276
|
+
if defined?(Note)
|
277
|
+
Object.send(:remove_const, :Note)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
context 'when the document exists' do
|
282
|
+
|
283
|
+
let!(:id) do
|
284
|
+
repository.save(Note.new)['_id']
|
285
|
+
end
|
286
|
+
|
287
|
+
context 'an id is provided' do
|
288
|
+
|
289
|
+
before do
|
290
|
+
repository.delete(id)
|
291
|
+
end
|
292
|
+
|
293
|
+
it 'deletes the document using the id' do
|
294
|
+
expect {
|
295
|
+
repository.find(id)
|
296
|
+
}.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
context 'when a document is provided' do
|
301
|
+
|
302
|
+
before do
|
303
|
+
repository.delete(id: id, text: 'testing')
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'deletes the document using the document' do
|
307
|
+
expect {
|
308
|
+
repository.find(id)
|
309
|
+
}.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
context 'when the document does not exist' do
|
315
|
+
|
316
|
+
before do
|
317
|
+
repository.create_index!
|
318
|
+
end
|
319
|
+
|
320
|
+
it 'raises an exception' do
|
321
|
+
expect {
|
322
|
+
repository.delete(1)
|
323
|
+
}.to raise_exception(Elasticsearch::Transport::Transport::Errors::NotFound)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
@@ -0,0 +1,723 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Elasticsearch::Persistence::Repository do
|
4
|
+
|
5
|
+
describe '#create' do
|
6
|
+
|
7
|
+
before(:all) do
|
8
|
+
class RepositoryWithoutDSL
|
9
|
+
include Elasticsearch::Persistence::Repository
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:all) do
|
14
|
+
if defined?(RepositoryWithoutDSL)
|
15
|
+
Object.send(:remove_const, RepositoryWithoutDSL.name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'creates a repository object' do
|
20
|
+
expect(RepositoryWithoutDSL.create).to be_a(RepositoryWithoutDSL)
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when options are provided' do
|
24
|
+
|
25
|
+
let(:repository) do
|
26
|
+
RepositoryWithoutDSL.create(document_type: 'note')
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'sets the options on the instance' do
|
30
|
+
expect(repository.document_type).to eq('note')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'when a block is passed' do
|
35
|
+
|
36
|
+
let(:repository) do
|
37
|
+
RepositoryWithoutDSL.create(document_type: 'note') do
|
38
|
+
mapping dynamic: 'strict' do
|
39
|
+
indexes :foo
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'executes the block on the instance' do
|
45
|
+
expect(repository.mapping.to_hash).to eq(note: { dynamic: 'strict', properties: { foo: { type: 'text' } } })
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'when options are provided in the args and set in the block' do
|
49
|
+
|
50
|
+
let(:repository) do
|
51
|
+
RepositoryWithoutDSL.create(mapping: double('mapping', to_hash: {}), document_type: 'note') do
|
52
|
+
mapping dynamic: 'strict' do
|
53
|
+
indexes :foo
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'uses the options from the args' do
|
59
|
+
expect(repository.mapping.to_hash).to eq({})
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe '#initialize' do
|
66
|
+
|
67
|
+
before(:all) do
|
68
|
+
class RepositoryWithoutDSL
|
69
|
+
include Elasticsearch::Persistence::Repository
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
after(:all) do
|
74
|
+
if defined?(RepositoryWithoutDSL)
|
75
|
+
Object.send(:remove_const, RepositoryWithoutDSL.name)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
after do
|
80
|
+
begin; repository.delete_index!; rescue; end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'when options are not provided' do
|
84
|
+
|
85
|
+
let(:repository) do
|
86
|
+
RepositoryWithoutDSL.new
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'sets a default client' do
|
90
|
+
expect(repository.client).to be_a(Elasticsearch::Transport::Client)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'sets a default document type' do
|
94
|
+
expect(repository.document_type).to eq('_doc')
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'sets a default index name' do
|
98
|
+
expect(repository.index_name).to eq('repository')
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'does not set a klass' do
|
102
|
+
expect(repository.klass).to be_nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'when options are provided' do
|
107
|
+
|
108
|
+
let(:client) do
|
109
|
+
Elasticsearch::Client.new
|
110
|
+
end
|
111
|
+
|
112
|
+
let(:repository) do
|
113
|
+
RepositoryWithoutDSL.new(client: client, document_type: 'user', index_name: 'users', klass: Array)
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'sets the client' do
|
117
|
+
expect(repository.client).to be(client)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'sets document type' do
|
121
|
+
expect(repository.document_type).to eq('user')
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'sets index name' do
|
125
|
+
expect(repository.index_name).to eq('users')
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'sets the klass' do
|
129
|
+
expect(repository.klass).to eq(Array)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context 'when the DSL module is included' do
|
135
|
+
|
136
|
+
before(:all) do
|
137
|
+
class RepositoryWithDSL
|
138
|
+
include Elasticsearch::Persistence::Repository
|
139
|
+
include Elasticsearch::Persistence::Repository::DSL
|
140
|
+
|
141
|
+
document_type 'note'
|
142
|
+
index_name 'notes_repo'
|
143
|
+
klass Hash
|
144
|
+
client DEFAULT_CLIENT
|
145
|
+
|
146
|
+
settings number_of_shards: 1, number_of_replicas: 0 do
|
147
|
+
mapping dynamic: 'strict' do
|
148
|
+
indexes :foo do
|
149
|
+
indexes :bar
|
150
|
+
end
|
151
|
+
indexes :baz
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
after(:all) do
|
158
|
+
if defined?(RepositoryWithDSL)
|
159
|
+
Object.send(:remove_const, RepositoryWithDSL.name)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
after do
|
164
|
+
begin; repository.delete_index; rescue; end
|
165
|
+
end
|
166
|
+
|
167
|
+
context '#client' do
|
168
|
+
|
169
|
+
it 'allows the value to be set only once on the class' do
|
170
|
+
RepositoryWithDSL.client(double('client', class: 'other_client'))
|
171
|
+
expect(RepositoryWithDSL.client).to be(DEFAULT_CLIENT)
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'sets the value at the class level' do
|
175
|
+
expect(RepositoryWithDSL.client).to be(DEFAULT_CLIENT)
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'sets the value as the default at the instance level' do
|
179
|
+
expect(RepositoryWithDSL.new.client).to be(DEFAULT_CLIENT)
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'allows the value to be overridden with options on the instance' do
|
183
|
+
expect(RepositoryWithDSL.new(client: double('client', instance: 'other')).client.instance).to eq('other')
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context '#klass' do
|
188
|
+
|
189
|
+
it 'allows the value to be set only once on the class' do
|
190
|
+
RepositoryWithDSL.klass(Array)
|
191
|
+
expect(RepositoryWithDSL.klass).to eq(Hash)
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'sets the value at the class level' do
|
195
|
+
expect(RepositoryWithDSL.klass).to eq(Hash)
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'sets the value as the default at the instance level' do
|
199
|
+
expect(RepositoryWithDSL.new.klass).to eq(Hash)
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'allows the value to be overridden with options on the instance' do
|
203
|
+
expect(RepositoryWithDSL.new(klass: Array).klass).to eq(Array)
|
204
|
+
end
|
205
|
+
|
206
|
+
context 'when nil is passed to the method' do
|
207
|
+
|
208
|
+
before do
|
209
|
+
RepositoryWithDSL.klass(nil)
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'allows the value to be set only once' do
|
213
|
+
expect(RepositoryWithDSL.klass).to eq(Hash)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
context '#document_type' do
|
219
|
+
|
220
|
+
it 'allows the value to be set only once on the class' do
|
221
|
+
RepositoryWithDSL.document_type('other_note')
|
222
|
+
expect(RepositoryWithDSL.document_type).to eq('note')
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'sets the value at the class level' do
|
226
|
+
expect(RepositoryWithDSL.document_type).to eq('note')
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'sets the value as the default at the instance level' do
|
230
|
+
expect(RepositoryWithDSL.new.document_type).to eq('note')
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'allows the value to be overridden with options on the instance' do
|
234
|
+
expect(RepositoryWithDSL.new(document_type: 'other_note').document_type).to eq('other_note')
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
context '#index_name' do
|
239
|
+
|
240
|
+
it 'allows the value to be set only once on the class' do
|
241
|
+
RepositoryWithDSL.index_name('other_name')
|
242
|
+
expect(RepositoryWithDSL.index_name).to eq('notes_repo')
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'sets the value at the class level' do
|
246
|
+
expect(RepositoryWithDSL.index_name).to eq('notes_repo')
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'sets the value as the default at the instance level' do
|
250
|
+
expect(RepositoryWithDSL.new.index_name).to eq('notes_repo')
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'allows the value to be overridden with options on the instance' do
|
254
|
+
expect(RepositoryWithDSL.new(index_name: 'other_notes_repo').index_name).to eq('other_notes_repo')
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
describe '#create_index!' do
|
259
|
+
|
260
|
+
context 'when the method is called on an instance' do
|
261
|
+
|
262
|
+
let(:repository) do
|
263
|
+
RepositoryWithDSL.new
|
264
|
+
end
|
265
|
+
|
266
|
+
before do
|
267
|
+
begin; repository.delete_index!; rescue; end
|
268
|
+
repository.create_index!
|
269
|
+
end
|
270
|
+
|
271
|
+
it 'creates the index' do
|
272
|
+
expect(repository.index_exists?).to be(true)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
context 'when the method is called on the class' do
|
277
|
+
|
278
|
+
it 'raises a NotImplementedError' do
|
279
|
+
expect {
|
280
|
+
RepositoryWithDSL.create_index!
|
281
|
+
}.to raise_exception(NotImplementedError)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
describe '#delete_index!' do
|
287
|
+
|
288
|
+
context 'when the method is called on an instance' do
|
289
|
+
|
290
|
+
let(:repository) do
|
291
|
+
RepositoryWithDSL.new
|
292
|
+
end
|
293
|
+
|
294
|
+
before do
|
295
|
+
repository.create_index!
|
296
|
+
begin; repository.delete_index!; rescue; end
|
297
|
+
end
|
298
|
+
|
299
|
+
it 'deletes the index' do
|
300
|
+
expect(repository.index_exists?).to be(false)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
context 'when the method is called on the class' do
|
305
|
+
|
306
|
+
it 'raises a NotImplementedError' do
|
307
|
+
expect {
|
308
|
+
RepositoryWithDSL.delete_index!
|
309
|
+
}.to raise_exception(NotImplementedError)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe '#refresh_index!' do
|
315
|
+
|
316
|
+
context 'when the method is called on an instance' do
|
317
|
+
|
318
|
+
let(:repository) do
|
319
|
+
RepositoryWithDSL.new
|
320
|
+
end
|
321
|
+
|
322
|
+
before do
|
323
|
+
repository.create_index!
|
324
|
+
end
|
325
|
+
|
326
|
+
it 'refreshes the index' do
|
327
|
+
expect(repository.refresh_index!['_shards']).to be_a(Hash)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
context 'when the method is called on the class' do
|
332
|
+
|
333
|
+
it 'raises a NotImplementedError' do
|
334
|
+
expect {
|
335
|
+
RepositoryWithDSL.refresh_index!
|
336
|
+
}.to raise_exception(NotImplementedError)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
describe '#index_exists?' do
|
342
|
+
|
343
|
+
context 'when the method is called on an instance' do
|
344
|
+
|
345
|
+
let(:repository) do
|
346
|
+
RepositoryWithDSL.new
|
347
|
+
end
|
348
|
+
|
349
|
+
before do
|
350
|
+
repository.create_index!
|
351
|
+
end
|
352
|
+
|
353
|
+
it 'determines if the index exists' do
|
354
|
+
expect(repository.index_exists?).to be(true)
|
355
|
+
end
|
356
|
+
|
357
|
+
context 'when arguments are passed in' do
|
358
|
+
|
359
|
+
it 'passes the arguments to the request' do
|
360
|
+
expect(repository.index_exists?(index: 'other')).to be(false)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
context 'when the method is called on the class' do
|
366
|
+
|
367
|
+
it 'raises a NotImplementedError' do
|
368
|
+
expect {
|
369
|
+
RepositoryWithDSL.index_exists?
|
370
|
+
}.to raise_exception(NotImplementedError)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
describe '#mapping' do
|
376
|
+
|
377
|
+
let(:expected_mapping) do
|
378
|
+
{ note: { dynamic: 'strict',
|
379
|
+
properties: { foo: { type: 'object',
|
380
|
+
properties: { bar: { type: 'text' } } },
|
381
|
+
baz: { type: 'text' } }
|
382
|
+
}
|
383
|
+
}
|
384
|
+
end
|
385
|
+
|
386
|
+
it 'sets the value at the class level' do
|
387
|
+
expect(RepositoryWithDSL.mapping.to_hash).to eq(expected_mapping)
|
388
|
+
end
|
389
|
+
|
390
|
+
it 'sets the value as the default at the instance level' do
|
391
|
+
expect(RepositoryWithDSL.new.mapping.to_hash).to eq(expected_mapping)
|
392
|
+
end
|
393
|
+
|
394
|
+
it 'allows the value to be overridden with options on the instance' do
|
395
|
+
expect(RepositoryWithDSL.new(mapping: double('mapping', to_hash: { note: {} })).mapping.to_hash).to eq(note: {})
|
396
|
+
end
|
397
|
+
|
398
|
+
context 'when the instance has a different document type' do
|
399
|
+
|
400
|
+
let(:expected_mapping) do
|
401
|
+
{ other_note: { dynamic: 'strict',
|
402
|
+
properties: { foo: { type: 'object',
|
403
|
+
properties: { bar: { type: 'text' } } },
|
404
|
+
baz: { type: 'text' } }
|
405
|
+
}
|
406
|
+
}
|
407
|
+
end
|
408
|
+
|
409
|
+
it 'updates the mapping to use the document type' do
|
410
|
+
expect(RepositoryWithDSL.new(document_type: 'other_note').mapping.to_hash).to eq(expected_mapping)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
describe '#settings' do
|
416
|
+
|
417
|
+
it 'sets the value at the class level' do
|
418
|
+
expect(RepositoryWithDSL.settings.to_hash).to eq(number_of_shards: 1, number_of_replicas: 0)
|
419
|
+
end
|
420
|
+
|
421
|
+
it 'sets the value as the default at the instance level' do
|
422
|
+
expect(RepositoryWithDSL.new.settings.to_hash).to eq(number_of_shards: 1, number_of_replicas: 0)
|
423
|
+
end
|
424
|
+
|
425
|
+
it 'allows the value to be overridden with options on the instance' do
|
426
|
+
expect(RepositoryWithDSL.new(settings: { number_of_shards: 3 }).settings.to_hash).to eq({number_of_shards: 3})
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
context 'when the DSL module is not included' do
|
432
|
+
|
433
|
+
before(:all) do
|
434
|
+
class RepositoryWithoutDSL
|
435
|
+
include Elasticsearch::Persistence::Repository
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
after(:all) do
|
440
|
+
if defined?(RepositoryWithoutDSL)
|
441
|
+
Object.send(:remove_const, RepositoryWithoutDSL.name)
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
context '#client' do
|
446
|
+
|
447
|
+
it 'does not define the method at the class level' do
|
448
|
+
expect {
|
449
|
+
RepositoryWithoutDSL.client
|
450
|
+
}.to raise_exception(NoMethodError)
|
451
|
+
end
|
452
|
+
|
453
|
+
it 'sets a default on the instance' do
|
454
|
+
expect(RepositoryWithoutDSL.new.client).to be_a(Elasticsearch::Transport::Client)
|
455
|
+
end
|
456
|
+
|
457
|
+
it 'allows the value to be overridden with options on the instance' do
|
458
|
+
expect(RepositoryWithoutDSL.new(client: double('client', object_id: 123)).client.object_id).to eq(123)
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
context '#klass' do
|
463
|
+
|
464
|
+
it 'does not define the method at the class level' do
|
465
|
+
expect {
|
466
|
+
RepositoryWithoutDSL.klass
|
467
|
+
}.to raise_exception(NoMethodError)
|
468
|
+
end
|
469
|
+
|
470
|
+
it 'does not set a default on an instance' do
|
471
|
+
expect(RepositoryWithoutDSL.new.klass).to be_nil
|
472
|
+
end
|
473
|
+
|
474
|
+
it 'allows the value to be overridden with options on the instance' do
|
475
|
+
expect(RepositoryWithoutDSL.new(klass: Array).klass).to eq(Array)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
context '#document_type' do
|
480
|
+
|
481
|
+
it 'does not define the method at the class level' do
|
482
|
+
expect {
|
483
|
+
RepositoryWithoutDSL.document_type
|
484
|
+
}.to raise_exception(NoMethodError)
|
485
|
+
end
|
486
|
+
|
487
|
+
it 'sets a default on the instance' do
|
488
|
+
expect(RepositoryWithoutDSL.new.document_type).to eq('_doc')
|
489
|
+
end
|
490
|
+
|
491
|
+
it 'allows the value to be overridden with options on the instance' do
|
492
|
+
expect(RepositoryWithoutDSL.new(document_type: 'notes').document_type).to eq('notes')
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
context '#index_name' do
|
497
|
+
|
498
|
+
it 'does not define the method at the class level' do
|
499
|
+
expect {
|
500
|
+
RepositoryWithoutDSL.index_name
|
501
|
+
}.to raise_exception(NoMethodError)
|
502
|
+
end
|
503
|
+
|
504
|
+
it 'sets a default on the instance' do
|
505
|
+
expect(RepositoryWithoutDSL.new.index_name).to eq('repository')
|
506
|
+
end
|
507
|
+
|
508
|
+
it 'allows the value to be overridden with options on the instance' do
|
509
|
+
expect(RepositoryWithoutDSL.new(index_name: 'notes_repository').index_name).to eq('notes_repository')
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
describe '#create_index!' do
|
514
|
+
|
515
|
+
let(:repository) do
|
516
|
+
RepositoryWithoutDSL.new(client: DEFAULT_CLIENT)
|
517
|
+
end
|
518
|
+
|
519
|
+
after do
|
520
|
+
begin; repository.delete_index!; rescue; end
|
521
|
+
end
|
522
|
+
|
523
|
+
it 'does not define the method at the class level' do
|
524
|
+
expect {
|
525
|
+
RepositoryWithoutDSL.create_index!
|
526
|
+
}.to raise_exception(NoMethodError)
|
527
|
+
end
|
528
|
+
|
529
|
+
it 'creates an index' do
|
530
|
+
repository.create_index!
|
531
|
+
expect(repository.index_exists?).to eq(true)
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
describe '#delete_index!' do
|
536
|
+
|
537
|
+
let(:repository) do
|
538
|
+
RepositoryWithoutDSL.new(client: DEFAULT_CLIENT)
|
539
|
+
end
|
540
|
+
|
541
|
+
it 'does not define the method at the class level' do
|
542
|
+
expect {
|
543
|
+
RepositoryWithoutDSL.delete_index!
|
544
|
+
}.to raise_exception(NoMethodError)
|
545
|
+
end
|
546
|
+
|
547
|
+
it 'deletes an index' do
|
548
|
+
repository.create_index!
|
549
|
+
repository.delete_index!
|
550
|
+
expect(repository.index_exists?).to eq(false)
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
describe '#refresh_index!' do
|
555
|
+
|
556
|
+
let(:repository) do
|
557
|
+
RepositoryWithoutDSL.new(client: DEFAULT_CLIENT)
|
558
|
+
end
|
559
|
+
|
560
|
+
after do
|
561
|
+
begin; repository.delete_index!; rescue; end
|
562
|
+
end
|
563
|
+
|
564
|
+
it 'does not define the method at the class level' do
|
565
|
+
expect {
|
566
|
+
RepositoryWithoutDSL.refresh_index!
|
567
|
+
}.to raise_exception(NoMethodError)
|
568
|
+
end
|
569
|
+
|
570
|
+
it 'refreshes an index' do
|
571
|
+
repository.create_index!
|
572
|
+
expect(repository.refresh_index!['_shards']).to be_a(Hash)
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
describe '#index_exists?' do
|
577
|
+
|
578
|
+
let(:repository) do
|
579
|
+
RepositoryWithoutDSL.new(client: DEFAULT_CLIENT)
|
580
|
+
end
|
581
|
+
|
582
|
+
after do
|
583
|
+
begin; repository.delete_index!; rescue; end
|
584
|
+
end
|
585
|
+
|
586
|
+
it 'does not define the method at the class level' do
|
587
|
+
expect {
|
588
|
+
RepositoryWithoutDSL.index_exists?
|
589
|
+
}.to raise_exception(NoMethodError)
|
590
|
+
end
|
591
|
+
|
592
|
+
it 'returns whether the index exists' do
|
593
|
+
repository.create_index!
|
594
|
+
expect(repository.index_exists?).to be(true)
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
describe '#mapping' do
|
599
|
+
|
600
|
+
it 'does not define the method at the class level' do
|
601
|
+
expect {
|
602
|
+
RepositoryWithoutDSL.mapping
|
603
|
+
}.to raise_exception(NoMethodError)
|
604
|
+
end
|
605
|
+
|
606
|
+
it 'sets a default on an instance' do
|
607
|
+
expect(RepositoryWithoutDSL.new.mapping.to_hash).to eq(_doc: { properties: {} })
|
608
|
+
end
|
609
|
+
|
610
|
+
it 'allows the mapping to be set as an option' do
|
611
|
+
expect(RepositoryWithoutDSL.new(mapping: double('mapping', to_hash: { note: {} })).mapping.to_hash).to eq(note: {})
|
612
|
+
end
|
613
|
+
|
614
|
+
context 'when a block is passed to the create method' do
|
615
|
+
|
616
|
+
let(:expected_mapping) do
|
617
|
+
{ note: { dynamic: 'strict',
|
618
|
+
properties: { foo: { type: 'object',
|
619
|
+
properties: { bar: { type: 'text' } } },
|
620
|
+
baz: { type: 'text' } }
|
621
|
+
}
|
622
|
+
}
|
623
|
+
end
|
624
|
+
|
625
|
+
let(:repository) do
|
626
|
+
RepositoryWithoutDSL.create(document_type: 'note') do
|
627
|
+
mapping dynamic: 'strict' do
|
628
|
+
indexes :foo do
|
629
|
+
indexes :bar
|
630
|
+
end
|
631
|
+
indexes :baz
|
632
|
+
end
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
it 'allows the mapping to be set in the block' do
|
637
|
+
expect(repository.mapping.to_hash).to eq(expected_mapping)
|
638
|
+
end
|
639
|
+
|
640
|
+
context 'when the mapping is set in the options' do
|
641
|
+
|
642
|
+
let(:repository) do
|
643
|
+
RepositoryWithoutDSL.create(mapping: double('mapping', to_hash: { note: {} })) do
|
644
|
+
mapping dynamic: 'strict' do
|
645
|
+
indexes :foo do
|
646
|
+
indexes :bar
|
647
|
+
end
|
648
|
+
indexes :baz
|
649
|
+
end
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
it 'uses the mapping from the options' do
|
654
|
+
expect(repository.mapping.to_hash).to eq(note: {})
|
655
|
+
end
|
656
|
+
end
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
660
|
+
describe '#settings' do
|
661
|
+
|
662
|
+
it 'does not define the method at the class level' do
|
663
|
+
expect {
|
664
|
+
RepositoryWithoutDSL.settings
|
665
|
+
}.to raise_exception(NoMethodError)
|
666
|
+
end
|
667
|
+
|
668
|
+
it 'sets a default on an instance' do
|
669
|
+
expect(RepositoryWithoutDSL.new.settings.to_hash).to eq({})
|
670
|
+
end
|
671
|
+
|
672
|
+
it 'allows the settings to be set as an option' do
|
673
|
+
expect(RepositoryWithoutDSL.new(settings: double('settings', to_hash: {})).settings.to_hash).to eq({})
|
674
|
+
end
|
675
|
+
|
676
|
+
context 'when a block is passed to the #create method' do
|
677
|
+
|
678
|
+
let(:repository) do
|
679
|
+
RepositoryWithoutDSL.create(document_type: 'note') do
|
680
|
+
settings number_of_shards: 1, number_of_replicas: 0
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
it 'allows the settings to be set with a block' do
|
685
|
+
expect(repository.settings.to_hash).to eq(number_of_shards: 1, number_of_replicas: 0)
|
686
|
+
end
|
687
|
+
|
688
|
+
context 'when a mapping is set in the block as well' do
|
689
|
+
|
690
|
+
let(:expected_mapping) do
|
691
|
+
{ note: { dynamic: 'strict',
|
692
|
+
properties: { foo: { type: 'object',
|
693
|
+
properties: { bar: { type: 'text' } } },
|
694
|
+
baz: { type: 'text' } }
|
695
|
+
}
|
696
|
+
}
|
697
|
+
end
|
698
|
+
|
699
|
+
let(:repository) do
|
700
|
+
RepositoryWithoutDSL.create(document_type: 'note') do
|
701
|
+
settings number_of_shards: 1, number_of_replicas: 0 do
|
702
|
+
mapping dynamic: 'strict' do
|
703
|
+
indexes :foo do
|
704
|
+
indexes :bar
|
705
|
+
end
|
706
|
+
indexes :baz
|
707
|
+
end
|
708
|
+
end
|
709
|
+
end
|
710
|
+
end
|
711
|
+
|
712
|
+
it 'allows the settings to be set with a block' do
|
713
|
+
expect(repository.settings.to_hash).to eq(number_of_shards: 1, number_of_replicas: 0)
|
714
|
+
end
|
715
|
+
|
716
|
+
it 'allows the mapping to be set with a block' do
|
717
|
+
expect(repository.mappings.to_hash).to eq(expected_mapping)
|
718
|
+
end
|
719
|
+
end
|
720
|
+
end
|
721
|
+
end
|
722
|
+
end
|
723
|
+
end
|