rom-elasticsearch 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/.rspec +3 -0
- data/.travis.yml +24 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +3 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +22 -0
- data/README.md +40 -0
- data/Rakefile +19 -0
- data/lib/rom-elasticsearch.rb +1 -0
- data/lib/rom/elasticsearch.rb +8 -0
- data/lib/rom/elasticsearch/attribute.rb +30 -0
- data/lib/rom/elasticsearch/commands.rb +49 -0
- data/lib/rom/elasticsearch/dataset.rb +219 -0
- data/lib/rom/elasticsearch/errors.rb +23 -0
- data/lib/rom/elasticsearch/gateway.rb +81 -0
- data/lib/rom/elasticsearch/plugins/relation/query_dsl.rb +57 -0
- data/lib/rom/elasticsearch/query_methods.rb +64 -0
- data/lib/rom/elasticsearch/relation.rb +241 -0
- data/lib/rom/elasticsearch/schema.rb +26 -0
- data/lib/rom/elasticsearch/types.rb +33 -0
- data/lib/rom/elasticsearch/version.rb +5 -0
- data/rom-elasticsearch.gemspec +27 -0
- data/spec/integration/rom/elasticsearch/relation/command_spec.rb +47 -0
- data/spec/shared/setup.rb +16 -0
- data/spec/shared/unit/user_fixtures.rb +15 -0
- data/spec/shared/unit/users.rb +18 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/unit/rom/elasticsearch/dataset/body_spec.rb +13 -0
- data/spec/unit/rom/elasticsearch/dataset/delete_spec.rb +17 -0
- data/spec/unit/rom/elasticsearch/dataset/params_spec.rb +13 -0
- data/spec/unit/rom/elasticsearch/dataset/put_spec.rb +14 -0
- data/spec/unit/rom/elasticsearch/dataset/query_string_spec.rb +12 -0
- data/spec/unit/rom/elasticsearch/dataset/search_spec.rb +20 -0
- data/spec/unit/rom/elasticsearch/gateway_spec.rb +10 -0
- data/spec/unit/rom/elasticsearch/plugins/relation/query_dsl_spec.rb +34 -0
- data/spec/unit/rom/elasticsearch/relation/create_index_spec.rb +75 -0
- data/spec/unit/rom/elasticsearch/relation/dataset_spec.rb +26 -0
- data/spec/unit/rom/elasticsearch/relation/delete_spec.rb +32 -0
- data/spec/unit/rom/elasticsearch/relation/get_spec.rb +22 -0
- data/spec/unit/rom/elasticsearch/relation/map_spec.rb +18 -0
- data/spec/unit/rom/elasticsearch/relation/pluck_spec.rb +18 -0
- data/spec/unit/rom/elasticsearch/relation/query_spec.rb +18 -0
- data/spec/unit/rom/elasticsearch/relation/query_string_spec.rb +18 -0
- data/spec/unit/rom/elasticsearch/relation/search_spec.rb +18 -0
- data/spec/unit/rom/elasticsearch/relation/to_a_spec.rb +28 -0
- metadata +186 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7ea7ae9dbc43c75ca9583e5df195db789ea28430
|
4
|
+
data.tar.gz: 2d1b2523c9acc5c248b1841685588c73943e8990
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 887ffb629635a427b714a6e66cc8f4ba54251efa5b17ec8c70d1f8704f9b79de460b30402c437b38bb0da75b09d860e1ae5048f6feda933d5560b029f85fae9d
|
7
|
+
data.tar.gz: 46b872af00f24af35f1ea18cfbcda7dc5d2b2fedbdeea110c08c142c7199424d80218a7deab7c65e3fdfab90271320a2e6fa25eaf9e219856465a8f76ec9bc60
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
language: ruby
|
2
|
+
sudo: false
|
3
|
+
cache: bundler
|
4
|
+
services:
|
5
|
+
- elasticsearch
|
6
|
+
bundler_args: --without yard guard benchmarks tools
|
7
|
+
before_script:
|
8
|
+
- curl -XPUT http://localhost:9200/rom-test
|
9
|
+
script: "bundle exec rake ci"
|
10
|
+
rvm:
|
11
|
+
- 2.3.4
|
12
|
+
- 2.4.1
|
13
|
+
- jruby-9.1.12.0
|
14
|
+
env:
|
15
|
+
global:
|
16
|
+
- JRUBY_OPTS='--dev -J-Xmx1024M'
|
17
|
+
- COVERAGE='true'
|
18
|
+
notifications:
|
19
|
+
webhooks:
|
20
|
+
urls:
|
21
|
+
- https://webhooks.gitter.im/e/39e1225f489f38b0bd09
|
22
|
+
on_success: change
|
23
|
+
on_failure: always
|
24
|
+
on_start: false
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Issue Guidelines
|
2
|
+
|
3
|
+
## Reporting bugs
|
4
|
+
|
5
|
+
If you found a bug, report an issue and describe what's the expected behavior versus what actually happens. If the bug causes a crash, attach a full backtrace. If possible, a reproduction script showing the problem is highly appreciated.
|
6
|
+
|
7
|
+
## Reporting feature requests
|
8
|
+
|
9
|
+
Report a feature request **only after discussing it first on [discourse.rom-rb.org](https://discourse.rom-rb.org)** where it was accepted. Please provide a concise description of the feature, don't link to a discussion thread, and instead summarize what was discussed.
|
10
|
+
|
11
|
+
## Reporting questions, support requests, ideas, concerns etc.
|
12
|
+
|
13
|
+
**PLEASE DON'T** - use [discourse.rom-rb.org](http://discourse.rom-rb.org) instead.
|
14
|
+
|
15
|
+
# Pull Request Guidelines
|
16
|
+
|
17
|
+
A Pull Request will only be accepted if it addresses a specific issue that was reported previously, or fixes typos, mistakes in documentation etc.
|
18
|
+
|
19
|
+
Other requirements:
|
20
|
+
|
21
|
+
1) Do not open a pull request if you can't provide tests along with it. If you have problems writing tests, ask for help in the related issue.
|
22
|
+
2) Follow the style conventions of the surrounding code. In most cases, this is standard ruby style.
|
23
|
+
3) Add API documentation if it's a new feature
|
24
|
+
4) Update API documentation if it changes an existing feature
|
25
|
+
5) Bonus points for sending a PR to [github.com/rom-rb/rom-rb.org](https://github.com/rom-rb/rom-rb.org) which updates user documentation and guides
|
26
|
+
|
27
|
+
# Asking for help
|
28
|
+
|
29
|
+
If these guidelines aren't helpful, and you're stuck, please post a message on [discourse.rom-rb.org](https://discourse.rom-rb.org).
|
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in rom-elasticsearch.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem 'rom', git: 'https://github.com/rom-rb/rom', branch: 'master' do
|
7
|
+
gem 'rom-core'
|
8
|
+
gem 'rom-mapper'
|
9
|
+
end
|
10
|
+
|
11
|
+
gem 'codeclimate-test-reporter', require: false
|
12
|
+
gem 'simplecov', require: false
|
13
|
+
|
14
|
+
gem 'pry-byebug', platform: :mri
|
15
|
+
gem 'pry', platform: :jruby
|
16
|
+
gem 'elasticsearch-dsl'
|
17
|
+
|
18
|
+
group :tools do
|
19
|
+
gem 'kramdown' # for yard
|
20
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) rom-rb team
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
[gem]: https://rubygems.org/gems/rom-elasticsearch
|
2
|
+
[travis]: https://travis-ci.org/rom-rb/rom-elasticsearch
|
3
|
+
[gemnasium]: https://gemnasium.com/rom-rb/rom-elasticsearch
|
4
|
+
[codeclimate]: https://codeclimate.com/github/rom-rb/rom-elasticsearch
|
5
|
+
[inchpages]: http://inch-ci.org/github/rom-rb/rom-elasticsearch
|
6
|
+
|
7
|
+
# rom-elasticsearch
|
8
|
+
|
9
|
+
[![Gem Version](https://badge.fury.io/rb/rom-elasticsearch.svg)][gem]
|
10
|
+
[![Build Status](https://travis-ci.org/rom-rb/rom-elasticsearch.svg?branch=master)][travis]
|
11
|
+
[![Dependency Status](https://gemnasium.com/rom-rb/rom-elasticsearch.svg)][gemnasium]
|
12
|
+
[![Code Climate](https://codeclimate.com/github/rom-rb/rom-elasticsearch/badges/gpa.svg)][codeclimate]
|
13
|
+
[![Test Coverage](https://codeclimate.com/github/rom-rb/rom-elasticsearch/badges/coverage.svg)][codeclimate]
|
14
|
+
[![Inline docs](http://inch-ci.org/github/rom-rb/rom-elasticsearch.svg?branch=master)][inchpages]
|
15
|
+
|
16
|
+
ElasticSearch support for [rom-rb](https://github.com/rom-rb/rom).
|
17
|
+
|
18
|
+
Resources:
|
19
|
+
|
20
|
+
- [API Documentation](http://api.rom-rb.org/rom-elasticsearch)
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem 'rom-elasticsearch'
|
28
|
+
```
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
$ bundle
|
33
|
+
|
34
|
+
Or install it yourself as:
|
35
|
+
|
36
|
+
$ gem install rom-elasticsearch
|
37
|
+
|
38
|
+
## License
|
39
|
+
|
40
|
+
See `LICENSE` file.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
task default: [:ci]
|
6
|
+
|
7
|
+
desc "Run CI tasks"
|
8
|
+
task ci: [:spec]
|
9
|
+
|
10
|
+
begin
|
11
|
+
require "rubocop/rake_task"
|
12
|
+
|
13
|
+
Rake::Task[:default].enhance [:rubocop]
|
14
|
+
|
15
|
+
RuboCop::RakeTask.new do |task|
|
16
|
+
task.options << "--display-cop-names"
|
17
|
+
end
|
18
|
+
rescue LoadError
|
19
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'rom/elasticsearch'
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rom/attribute'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
module Elasticsearch
|
5
|
+
# ES-specific attribute types for schemas
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
class Attribute < ROM::Attribute
|
9
|
+
INTERNAL_META_KEYS = %i[name source primary_key].freeze
|
10
|
+
|
11
|
+
# Return ES mapping properties
|
12
|
+
#
|
13
|
+
# @return [Hash]
|
14
|
+
#
|
15
|
+
# @api public
|
16
|
+
memoize def properties
|
17
|
+
type.meta.reject { |k, _| INTERNAL_META_KEYS.include?(k) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return if an attribute has any ES mappings
|
21
|
+
#
|
22
|
+
# @return [Bool]
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
def properties?
|
26
|
+
properties.size > 0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rom/commands'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
module Elasticsearch
|
5
|
+
# ElasticSearch relation commands
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
class Commands
|
9
|
+
# Create command
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
class Create < ROM::Commands::Create
|
13
|
+
# @api private
|
14
|
+
def execute(attributes)
|
15
|
+
tuple = input[attributes]
|
16
|
+
|
17
|
+
result =
|
18
|
+
if _id
|
19
|
+
dataset.params(id: tuple.fetch(_id)).put(tuple)
|
20
|
+
else
|
21
|
+
dataset.put(tuple)
|
22
|
+
end
|
23
|
+
[relation.get(result['_id']).one]
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
def dataset
|
30
|
+
relation.dataset
|
31
|
+
end
|
32
|
+
|
33
|
+
def _id
|
34
|
+
relation.schema.primary_key_name
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Delete command
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
class Delete < ROM::Commands::Delete
|
42
|
+
# @api private
|
43
|
+
def execute
|
44
|
+
relation.dataset.delete
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'rom/initializer'
|
2
|
+
|
3
|
+
require 'rom/elasticsearch/query_methods'
|
4
|
+
require 'rom/elasticsearch/errors'
|
5
|
+
|
6
|
+
module ROM
|
7
|
+
module Elasticsearch
|
8
|
+
# Elasticsearch dataset
|
9
|
+
#
|
10
|
+
# Uses an elasticsearch client object provided by the gateway, holds basic
|
11
|
+
# params with information about index name and type, and optional body for
|
12
|
+
# additional queries.
|
13
|
+
#
|
14
|
+
# Dataset object also provide meta information about indices, like custom
|
15
|
+
# settings and mappings.
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
class Dataset
|
19
|
+
extend Initializer
|
20
|
+
|
21
|
+
include QueryMethods
|
22
|
+
|
23
|
+
# Default query options
|
24
|
+
ALL = { query: { match_all: EMPTY_HASH } }.freeze
|
25
|
+
|
26
|
+
# The source key in raw results
|
27
|
+
SOURCE_KEY = '_source'.freeze
|
28
|
+
|
29
|
+
# @!attribute [r] client
|
30
|
+
# @return [::Elasticsearch::Client] configured client from the gateway
|
31
|
+
param :client
|
32
|
+
|
33
|
+
# @!attribute [r] params
|
34
|
+
# @return [Hash] default params
|
35
|
+
option :params, default: -> { EMPTY_HASH }
|
36
|
+
|
37
|
+
# @!attribute [r] client
|
38
|
+
# @return [Hash] default body
|
39
|
+
option :body, default: -> { EMPTY_HASH }
|
40
|
+
|
41
|
+
# Put new data under configured index
|
42
|
+
#
|
43
|
+
# @param [Hash] data
|
44
|
+
#
|
45
|
+
# @return [Hash]
|
46
|
+
#
|
47
|
+
# @api public
|
48
|
+
def put(data)
|
49
|
+
client.index(**params, body: data)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Return index settings
|
53
|
+
#
|
54
|
+
# @return [Hash]
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def settings
|
58
|
+
client.indices.get_settings[index.to_s]['settings']['index']
|
59
|
+
end
|
60
|
+
|
61
|
+
# Return index mappings
|
62
|
+
#
|
63
|
+
# @return [Hash]
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
def mappings
|
67
|
+
client.indices.get_mapping[index.to_s]['mappings'][type.to_s]
|
68
|
+
end
|
69
|
+
|
70
|
+
# Delete everything matching configured params and/or body
|
71
|
+
#
|
72
|
+
# If body is empty it *will delete everything**
|
73
|
+
#
|
74
|
+
# @return [Hash] raw response hash from the client
|
75
|
+
#
|
76
|
+
# @api public
|
77
|
+
def delete
|
78
|
+
if body.empty? && params[:id]
|
79
|
+
client.delete(params)
|
80
|
+
elsif body.empty?
|
81
|
+
client.delete_by_query(params.merge(body: body.merge(ALL)))
|
82
|
+
else
|
83
|
+
client.delete_by_query(params.merge(body: body))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Materialize the dataset
|
88
|
+
#
|
89
|
+
# @return [Array<Hash>]
|
90
|
+
#
|
91
|
+
# @api public
|
92
|
+
def to_a
|
93
|
+
to_enum.to_a
|
94
|
+
end
|
95
|
+
|
96
|
+
# Materialize and iterate over results
|
97
|
+
#
|
98
|
+
# @yieldparam [Hash]
|
99
|
+
#
|
100
|
+
# @raise [SearchError] in case of the client raising an exception
|
101
|
+
#
|
102
|
+
# @api public
|
103
|
+
def each
|
104
|
+
return to_enum unless block_given?
|
105
|
+
view.each do |result|
|
106
|
+
yield(result[SOURCE_KEY])
|
107
|
+
end
|
108
|
+
rescue ::Elasticsearch::Transport::Transport::Error => e
|
109
|
+
raise SearchError.new(e, options)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Map dataset tuples
|
113
|
+
#
|
114
|
+
# @yieldparam [Hash]
|
115
|
+
#
|
116
|
+
# @return [Array]
|
117
|
+
#
|
118
|
+
# @api public
|
119
|
+
def map(&block)
|
120
|
+
to_a.map(&block)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Return configured type from params
|
124
|
+
#
|
125
|
+
# @return [Symbol]
|
126
|
+
#
|
127
|
+
# @api public
|
128
|
+
def type
|
129
|
+
params[:type]
|
130
|
+
end
|
131
|
+
|
132
|
+
# Return configured index name
|
133
|
+
#
|
134
|
+
# @return [Symbol]
|
135
|
+
#
|
136
|
+
# @api public
|
137
|
+
def index
|
138
|
+
params[:index]
|
139
|
+
end
|
140
|
+
|
141
|
+
# Return a new dataset with new body
|
142
|
+
#
|
143
|
+
# @param [Hash] new New body data
|
144
|
+
#
|
145
|
+
# @return [Hash]
|
146
|
+
#
|
147
|
+
# @api public
|
148
|
+
def body(new = nil)
|
149
|
+
if new.nil?
|
150
|
+
@body
|
151
|
+
else
|
152
|
+
with(body: body.merge(new))
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Return a new dataset with new params
|
157
|
+
#
|
158
|
+
# @param [Hash] new New params data
|
159
|
+
#
|
160
|
+
# @return [Hash]
|
161
|
+
#
|
162
|
+
# @api public
|
163
|
+
def params(new = nil)
|
164
|
+
if new.nil?
|
165
|
+
@params
|
166
|
+
else
|
167
|
+
with(params: params.merge(new))
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Refresh index
|
172
|
+
#
|
173
|
+
# @return [Dataset]
|
174
|
+
#
|
175
|
+
# @api public
|
176
|
+
def refresh
|
177
|
+
client.indices.refresh(index: index)
|
178
|
+
self
|
179
|
+
end
|
180
|
+
|
181
|
+
# Create an index
|
182
|
+
#
|
183
|
+
# @param [Hash] opts ES options
|
184
|
+
#
|
185
|
+
# @api public
|
186
|
+
#
|
187
|
+
# @return [Hash]
|
188
|
+
def create_index(opts = EMPTY_HASH)
|
189
|
+
client.indices.create(params.merge(opts))
|
190
|
+
end
|
191
|
+
|
192
|
+
# Delete an index
|
193
|
+
#
|
194
|
+
# @param [Hash] opts ES options
|
195
|
+
#
|
196
|
+
# @api public
|
197
|
+
#
|
198
|
+
# @return [Hash]
|
199
|
+
def delete_index(opts = EMPTY_HASH)
|
200
|
+
client.indices.delete(params.merge(opts))
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
|
205
|
+
# Return results of a query based on configured params and body
|
206
|
+
#
|
207
|
+
# @return [Array<Hash>]
|
208
|
+
#
|
209
|
+
# @api private
|
210
|
+
def view
|
211
|
+
if params[:id]
|
212
|
+
[client.get(params)]
|
213
|
+
else
|
214
|
+
client.search(**params, body: body).fetch('hits').fetch('hits')
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|