rdstation-ruby-client 2.1.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +121 -1
  3. data/README.md +106 -22
  4. data/Rakefile +4 -0
  5. data/lib/rdstation-ruby-client.rb +6 -1
  6. data/lib/rdstation.rb +19 -0
  7. data/lib/rdstation/api_response.rb +1 -2
  8. data/lib/rdstation/authentication.rb +8 -3
  9. data/lib/rdstation/{authorization_header.rb → authorization.rb} +11 -8
  10. data/lib/rdstation/builder/field.rb +70 -0
  11. data/lib/rdstation/client.rb +17 -7
  12. data/lib/rdstation/contacts.rb +22 -13
  13. data/lib/rdstation/error.rb +3 -0
  14. data/lib/rdstation/error/format.rb +29 -3
  15. data/lib/rdstation/error/formatter.rb +69 -8
  16. data/lib/rdstation/error_handler.rb +6 -1
  17. data/lib/rdstation/error_handler/invalid_refresh_token.rb +24 -0
  18. data/lib/rdstation/error_handler/unauthorized.rb +2 -0
  19. data/lib/rdstation/events.rb +7 -12
  20. data/lib/rdstation/fields.rb +35 -6
  21. data/lib/rdstation/retryable_request.rb +35 -0
  22. data/lib/rdstation/version.rb +1 -1
  23. data/lib/rdstation/webhooks.rb +25 -13
  24. data/rdstation-ruby-client.gemspec +2 -1
  25. data/spec/lib/rdstation/api_response_spec.rb +34 -0
  26. data/spec/lib/rdstation/authentication_spec.rb +105 -2
  27. data/spec/lib/rdstation/{authorization_header_spec.rb → authorization_spec.rb} +3 -3
  28. data/spec/lib/rdstation/builder/field_spec.rb +69 -0
  29. data/spec/lib/rdstation/client_spec.rb +6 -6
  30. data/spec/lib/rdstation/contacts_spec.rb +23 -3
  31. data/spec/lib/rdstation/error/format_spec.rb +63 -0
  32. data/spec/lib/rdstation/error/formatter_spec.rb +113 -0
  33. data/spec/lib/rdstation/error_handler/invalid_refresh_token_spec.rb +53 -0
  34. data/spec/lib/rdstation/error_handler_spec.rb +23 -0
  35. data/spec/lib/rdstation/events_spec.rb +8 -3
  36. data/spec/lib/rdstation/fields_spec.rb +6 -1
  37. data/spec/lib/rdstation/retryable_request_spec.rb +142 -0
  38. data/spec/lib/rdstation/webhooks_spec.rb +26 -1
  39. data/spec/lib/rdstation_spec.rb +18 -0
  40. metadata +36 -8
@@ -4,27 +4,27 @@ RSpec.describe RDStation::Client do
4
4
  context "when access_token is given" do
5
5
  let(:access_token) { 'access_token' }
6
6
  let(:client) { described_class.new(access_token: access_token) }
7
- let(:mock_authorization_header) { double(RDStation::AuthorizationHeader) }
7
+ let(:mock_authorization) { double(RDStation::Authorization) }
8
8
 
9
- before { allow(RDStation::AuthorizationHeader).to receive(:new).and_return mock_authorization_header }
9
+ before { allow(RDStation::Authorization).to receive(:new).and_return mock_authorization }
10
10
 
11
11
  it 'returns Contacts endpoint' do
12
- expect(RDStation::Contacts).to receive(:new).with({ authorization_header: mock_authorization_header }).and_call_original
12
+ expect(RDStation::Contacts).to receive(:new).with({ authorization: mock_authorization }).and_call_original
13
13
  expect(client.contacts).to be_instance_of RDStation::Contacts
14
14
  end
15
15
 
16
16
  it 'returns Events endpoint' do
17
- expect(RDStation::Events).to receive(:new).with({ authorization_header: mock_authorization_header }).and_call_original
17
+ expect(RDStation::Events).to receive(:new).with({ authorization: mock_authorization }).and_call_original
18
18
  expect(client.events).to be_instance_of RDStation::Events
19
19
  end
20
20
 
21
21
  it 'returns Fields endpoint' do
22
- expect(RDStation::Fields).to receive(:new).with({ authorization_header: mock_authorization_header }).and_call_original
22
+ expect(RDStation::Fields).to receive(:new).with({ authorization: mock_authorization }).and_call_original
23
23
  expect(client.fields).to be_instance_of RDStation::Fields
24
24
  end
25
25
 
26
26
  it 'returns Webhooks endpoint' do
27
- expect(RDStation::Webhooks).to receive(:new).with({ authorization_header: mock_authorization_header }).and_call_original
27
+ expect(RDStation::Webhooks).to receive(:new).with({ authorization: mock_authorization }).and_call_original
28
28
  expect(client.webhooks).to be_instance_of RDStation::Webhooks
29
29
  end
30
30
  end
@@ -16,13 +16,13 @@ RSpec.describe RDStation::Contacts do
16
16
  let(:expired_access_token) { 'expired_access_token' }
17
17
 
18
18
  let(:contact_with_valid_token) do
19
- described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: valid_access_token))
19
+ described_class.new(authorization: RDStation::Authorization.new(access_token: valid_access_token))
20
20
  end
21
21
  let(:contact_with_expired_token) do
22
- described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: expired_access_token))
22
+ described_class.new(authorization: RDStation::Authorization.new(access_token: expired_access_token))
23
23
  end
24
24
  let(:contact_with_invalid_token) do
25
- described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: invalid_access_token))
25
+ described_class.new(authorization: RDStation::Authorization.new(access_token: invalid_access_token))
26
26
  end
27
27
 
28
28
 
@@ -109,6 +109,11 @@ RSpec.describe RDStation::Contacts do
109
109
  end
110
110
 
111
111
  describe '#by_uuid' do
112
+ it 'calls retryable_request' do
113
+ expect(contact_with_valid_token).to receive(:retryable_request)
114
+ contact_with_valid_token.by_uuid('valid_uuid')
115
+ end
116
+
112
117
  context 'with a valid auth token' do
113
118
  context 'when the contact exists' do
114
119
  let(:contact) do
@@ -172,6 +177,11 @@ RSpec.describe RDStation::Contacts do
172
177
  end
173
178
 
174
179
  describe '#by_email' do
180
+ it 'calls retryable_request' do
181
+ expect(contact_with_valid_token).to receive(:retryable_request)
182
+ contact_with_valid_token.by_email('x@xpto.com')
183
+ end
184
+
175
185
  context 'with a valid auth token' do
176
186
  context 'when the contact exists' do
177
187
  let(:contact) do
@@ -235,6 +245,11 @@ RSpec.describe RDStation::Contacts do
235
245
  end
236
246
 
237
247
  describe '#update' do
248
+ it 'calls retryable_request' do
249
+ expect(contact_with_valid_token).to receive(:retryable_request)
250
+ contact_with_valid_token.update('valid_uuid', {})
251
+ end
252
+
238
253
  context 'with a valid access_token' do
239
254
  let(:valid_access_token) { 'valid_access_token' }
240
255
  let(:headers) do
@@ -322,6 +337,11 @@ RSpec.describe RDStation::Contacts do
322
337
  end
323
338
 
324
339
  describe '#upsert' do
340
+ it 'calls retryable_request' do
341
+ expect(contact_with_valid_token).to receive(:retryable_request)
342
+ contact_with_valid_token.upsert('email', 'valid@email.com', {})
343
+ end
344
+
325
345
  context 'with a valid access_token' do
326
346
  let(:valid_access_token) { 'valid_access_token' }
327
347
 
@@ -52,5 +52,68 @@ RSpec.describe RDStation::Error::Format do
52
52
  expect(result).to eq(RDStation::Error::Format::ARRAY_OF_HASHES)
53
53
  end
54
54
  end
55
+
56
+ context 'when receives a mixed type of errors' do
57
+ let(:errors) do
58
+ {
59
+ 'label': {
60
+ 'pt-BR': [
61
+ {
62
+ 'error_type': 'CANNOT_BE_BLANK',
63
+ 'error_message': 'cannot be blank'
64
+ }
65
+ ]
66
+ },
67
+ 'api_identifier': [
68
+ {
69
+ 'error_type': 'CANNOT_BE_BLANK',
70
+ 'error_message': 'cannot be blank'
71
+ }
72
+ ]
73
+ }
74
+ end
75
+
76
+ it 'returns the HASH_OF_MULTIPLE_TYPES format' do
77
+ result = error_format.format
78
+ expect(result).to eq(RDStation::Error::Format::HASH_OF_MULTIPLE_TYPES)
79
+ end
80
+ end
81
+
82
+ context 'when receives a hash of hashes errors' do
83
+ let(:errors) do
84
+ {
85
+ label: {
86
+ 'pt-BR': [
87
+ {
88
+ 'error_type': 'CANNOT_BE_BLANK',
89
+ 'error_message': 'cannot be blank'
90
+ }
91
+ ]
92
+ }
93
+ }
94
+ end
95
+
96
+ it 'returns the HASH_OF_MULTILINGUAL format' do
97
+ result = error_format.format
98
+ expect(result).to eq(RDStation::Error::Format::HASH_OF_HASHES)
99
+ end
100
+ end
101
+
102
+ context 'when receives a single hash with error' do
103
+ let(:errors) do
104
+ {
105
+ 'error' => "'lead_limiter' rate limit exceeded for 86400 second(s) period for key ...",
106
+ 'max' => 24,
107
+ 'usage' => 55,
108
+ 'remaining_time' => 20745,
109
+ }
110
+ end
111
+
112
+ it 'returns the SINGLE_HASH format' do
113
+ result = error_format.format
114
+ expect(result).to eq(RDStation::Error::Format::SINGLE_HASH)
115
+ end
116
+
117
+ end
55
118
  end
56
119
  end
@@ -132,5 +132,118 @@ RSpec.describe RDStation::Error::Formatter do
132
132
  expect(result).to eq(expected_result)
133
133
  end
134
134
  end
135
+
136
+ context 'when receives a hash of multiple type errors' do
137
+ let(:error_format) { instance_double(RDStation::Error::Format, format: RDStation::Error::Format::HASH_OF_MULTIPLE_TYPES) }
138
+
139
+ let(:error_response) do
140
+ {
141
+ 'errors' => {
142
+ 'label' => {
143
+ 'pt-BR' => [
144
+ {
145
+ 'error_type' => 'CANNOT_BE_BLANK',
146
+ 'error_message' => 'cannot be blank'
147
+ }
148
+ ]
149
+ },
150
+ 'api_identifier' => [
151
+ {
152
+ 'error_type' => 'CANNOT_BE_BLANK',
153
+ 'error_message' => 'cannot be blank'
154
+ }
155
+ ]
156
+ }
157
+ }
158
+ end
159
+
160
+ let(:error_formatter) { described_class.new(error_response) }
161
+
162
+ let(:expected_result) do
163
+ [
164
+ {
165
+ 'error_type' => 'CANNOT_BE_BLANK',
166
+ 'error_message' => 'cannot be blank',
167
+ 'path' => 'body.label.pt-BR'
168
+ },
169
+ {
170
+ 'error_type' => 'CANNOT_BE_BLANK',
171
+ 'error_message' => 'cannot be blank',
172
+ 'path' => 'body.api_identifier'
173
+ }
174
+ ]
175
+ end
176
+
177
+ it 'returns an array of errors' do
178
+ result = error_formatter.to_array
179
+ expect(result).to eq(expected_result)
180
+ end
181
+ end
182
+
183
+ context 'when receives a hash of hashes type errors' do
184
+ let(:error_format) { instance_double(RDStation::Error::Format, format: RDStation::Error::Format::HASH_OF_HASHES) }
185
+
186
+ let(:error_response) do
187
+ {
188
+ 'errors' => {
189
+ 'label' => {
190
+ 'pt-BR' => [
191
+ {
192
+ 'error_type' => 'CANNOT_BE_BLANK',
193
+ 'error_message' => 'cannot be blank'
194
+ }
195
+ ]
196
+ }
197
+ }
198
+ }
199
+ end
200
+
201
+ let(:error_formatter) { described_class.new(error_response) }
202
+
203
+ let(:expected_result) do
204
+ [
205
+ {
206
+ 'error_type' => 'CANNOT_BE_BLANK',
207
+ 'error_message' => 'cannot be blank',
208
+ 'path' => 'body.label.pt-BR'
209
+ }
210
+ ]
211
+ end
212
+
213
+ it 'returns an array of errors' do
214
+ result = error_formatter.to_array
215
+ expect(result).to eq(expected_result)
216
+ end
217
+ end
218
+
219
+ context 'when receives a single hash of errors' do
220
+ let(:error_format) { instance_double(RDStation::Error::Format, format: RDStation::Error::Format::SINGLE_HASH) }
221
+
222
+ let(:error_response) do
223
+ {
224
+ 'error' => "'lead_limiter' rate limit exceeded for 86400 second(s) period for key",
225
+ 'max' => 24,
226
+ 'usage' => 55,
227
+ 'remaining_time' => 20745
228
+ }
229
+ end
230
+
231
+ let(:error_formatter) { described_class.new(error_response) }
232
+
233
+ let(:expected_result) do
234
+ [
235
+ {
236
+ 'error_type' => 'TOO_MANY_REQUESTS',
237
+ 'error_message' => "'lead_limiter' rate limit exceeded for 86400 second(s) period for key",
238
+ 'details' => { 'max' => 24, 'usage' => 55, 'remaining_time' => 20745 }
239
+ }
240
+ ]
241
+ end
242
+
243
+ it 'returns an array of errors' do
244
+ result = error_formatter.to_array
245
+ expect(result).to eq(expected_result)
246
+ end
247
+ end
135
248
  end
136
249
  end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RDStation::ErrorHandler::InvalidRefreshToken do
4
+ describe '#raise_error' do
5
+ subject(:invalid_refresh_token) { described_class.new(errors) }
6
+
7
+ context 'when the refresh token is invalid or was revoked' do
8
+ let(:errors) do
9
+ [
10
+ {
11
+ 'error_type' => 'INVALID_REFRESH_TOKEN',
12
+ 'error_message' => 'Error Message',
13
+ }
14
+ ]
15
+ end
16
+
17
+ it 'raises an InvalidRefreshToken error' do
18
+ expect do
19
+ invalid_refresh_token.raise_error
20
+ end.to raise_error(RDStation::Error::InvalidRefreshToken, 'Error Message')
21
+ end
22
+ end
23
+
24
+ context 'when none of the errors are invalid refresh token errors' do
25
+ let(:errors) do
26
+ [
27
+ {
28
+ 'error_message' => 'Error Message',
29
+ 'error_type' => 'RANDOM_ERROR_TYPE'
30
+ },
31
+ {
32
+ 'error_message' => 'Another Error Message',
33
+ 'error_type' => 'ANOTHER_RANDOM_ERROR_TYPE'
34
+ }
35
+ ]
36
+ end
37
+
38
+ it 'does not raise an InvalidRefreshToken error' do
39
+ result = invalid_refresh_token.raise_error
40
+ expect(result).to be_nil
41
+ end
42
+ end
43
+
44
+ context 'when there are no errors' do
45
+ let(:errors) { [] }
46
+
47
+ it 'does not raise an InvalidRefreshToken error' do
48
+ result = invalid_refresh_token.raise_error
49
+ expect(result).to be_nil
50
+ end
51
+ end
52
+ end
53
+ end
@@ -151,6 +151,7 @@ RSpec.describe RDStation::ErrorHandler do
151
151
  expect { error_handler.raise_error }.to raise_error(RDStation::Error::ServiceUnavailable, 'Error Message')
152
152
  end
153
153
  end
154
+
154
155
  context 'with 5xx error' do
155
156
  let(:http_status) { 505 }
156
157
 
@@ -158,5 +159,27 @@ RSpec.describe RDStation::ErrorHandler do
158
159
  expect { error_handler.raise_error }.to raise_error(RDStation::Error::ServerError, 'Error Message')
159
160
  end
160
161
  end
162
+
163
+ context "when response body is not JSON-parseable" do
164
+ let(:error_response) do
165
+ OpenStruct.new(
166
+ code: 502,
167
+ headers: { 'error' => 'header' },
168
+ body: '<html><body>HTML error response</body></html>'
169
+ )
170
+ end
171
+
172
+ it 'raises the correct error' do
173
+ expect { error_handler.raise_error }.to raise_error(RDStation::Error::BadGateway, '<html><body>HTML error response</body></html>')
174
+ end
175
+ end
176
+
177
+ context 'with an unknown error' do
178
+ let(:http_status) { 123 }
179
+
180
+ it 'raises a unknown error' do
181
+ expect { error_handler.raise_error }.to raise_error(RDStation::Error::UnknownError, 'Error Message')
182
+ end
183
+ end
161
184
  end
162
185
  end
@@ -6,13 +6,13 @@ RSpec.describe RDStation::Events do
6
6
  let(:expired_access_token) { 'expired_access_token' }
7
7
 
8
8
  let(:event_with_valid_token) do
9
- described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: valid_access_token))
9
+ described_class.new(authorization: RDStation::Authorization.new(access_token: valid_access_token))
10
10
  end
11
11
  let(:event_with_expired_token) do
12
- described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: expired_access_token))
12
+ described_class.new(authorization: RDStation::Authorization.new(access_token: expired_access_token))
13
13
  end
14
14
  let(:event_with_invalid_token) do
15
- described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: invalid_access_token))
15
+ described_class.new(authorization: RDStation::Authorization.new(access_token: invalid_access_token))
16
16
  end
17
17
 
18
18
  let(:events_endpoint) { 'https://api.rd.services/platform/events' }
@@ -108,6 +108,11 @@ RSpec.describe RDStation::Events do
108
108
  }
109
109
  end
110
110
 
111
+ it 'calls retryable_request' do
112
+ expect(event_with_valid_token).to receive(:retryable_request)
113
+ event_with_valid_token.create({})
114
+ end
115
+
111
116
  context 'with a valid auth token' do
112
117
  before do
113
118
  stub_request(:post, events_endpoint)
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  RSpec.describe RDStation::Fields do
4
4
  let(:valid_access_token) { 'valid_access_token' }
5
5
  let(:rdstation_fields_with_valid_token) do
6
- described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: valid_access_token))
6
+ described_class.new(authorization: RDStation::Authorization.new(access_token: valid_access_token))
7
7
  end
8
8
 
9
9
  let(:valid_headers) do
@@ -38,6 +38,11 @@ RSpec.describe RDStation::Fields do
38
38
  }
39
39
  end
40
40
 
41
+ it 'calls retryable_request' do
42
+ expect(rdstation_fields_with_valid_token).to receive(:retryable_request)
43
+ rdstation_fields_with_valid_token.all
44
+ end
45
+
41
46
  context 'with a valid auth token' do
42
47
  before do
43
48
  stub_request(:get, fields_endpoint)
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+
3
+ class DummyClass
4
+ include ::RDStation::RetryableRequest
5
+ end
6
+
7
+ RSpec.describe RDStation::RetryableRequest do
8
+ let(:subject) { DummyClass.new }
9
+ describe '.retryable_request' do
10
+ context 'when authorization has a valid refresh_token and config is provided' do
11
+ let (:access_token) { 'access_token' }
12
+ let (:new_access_token) { 'new_access_token' }
13
+ let (:refresh_token) { 'refresh_token' }
14
+ let (:auth) do
15
+ ::RDStation::Authorization.new(access_token: access_token,
16
+ refresh_token: refresh_token
17
+ )
18
+ end
19
+ context 'original request was successful' do
20
+ it 'yields control to the given block' do
21
+ expect do |block|
22
+ subject.retryable_request(auth, &block)
23
+ end.to yield_with_args(auth)
24
+ end
25
+ end
26
+
27
+ context 'original request raised a retryable exception' do
28
+ let (:auth_new_access_token) do
29
+ ::RDStation::Authorization.new(access_token: new_access_token,
30
+ refresh_token: refresh_token
31
+ )
32
+ end
33
+
34
+ let(:new_credentials) do
35
+ {
36
+ 'access_token' => new_access_token,
37
+ 'expires_in' => 86_400,
38
+ 'refresh_token' => refresh_token
39
+ }
40
+ end
41
+ let(:authentication_client) {instance_double(::RDStation::Authentication) }
42
+
43
+ before do
44
+ RDStation.configure do |config|
45
+ config.client_id = "123"
46
+ config.client_secret = "312"
47
+ config.on_access_token_refresh do
48
+ 'callback code'
49
+ end
50
+ end
51
+ allow(::RDStation::Authentication).to receive(:new)
52
+ .with(no_args)
53
+ .and_return(authentication_client)
54
+ allow(authentication_client).to receive(:update_access_token)
55
+ .with(auth.refresh_token).
56
+ and_return(new_credentials)
57
+ end
58
+
59
+ it 'refreshes the access_token and retries the request' do
60
+ dummy_request = double("dummy_request")
61
+ expect(dummy_request).to receive(:call).twice do |auth|
62
+ expired_token = ::RDStation::Error::ExpiredAccessToken.new({'error_message' => 'x'})
63
+ raise expired_token unless auth.access_token == new_access_token
64
+ end
65
+
66
+ expect(RDStation.configuration.access_token_refresh_callback)
67
+ .to receive(:call)
68
+ .once do |authorization|
69
+ expect(authorization.access_token).to eq new_access_token
70
+ end
71
+
72
+ expect do
73
+ subject.retryable_request(auth) { |yielded_auth| dummy_request.call(yielded_auth) }
74
+ end.not_to raise_error
75
+ end
76
+
77
+ context 'and keeps raising retryable exception event after token refreshed' do
78
+ it 'retries only once' do
79
+ dummy_request = double("dummy_request")
80
+ expect(dummy_request).to receive(:call).twice do |_|
81
+ raise ::RDStation::Error::ExpiredAccessToken.new({'error_message' => 'x'})
82
+ end
83
+
84
+ expect do
85
+ subject.retryable_request(auth) { |yielded_auth| dummy_request.call(yielded_auth) }
86
+ end.to raise_error ::RDStation::Error::ExpiredAccessToken
87
+ end
88
+ end
89
+
90
+ context 'and access token refresh callback is not set' do
91
+ before do
92
+ RDStation.configure do |config|
93
+ config.on_access_token_refresh(&nil)
94
+ end
95
+ end
96
+
97
+ it 'executes the refresh and retry without raising an error' do
98
+ dummy_request = double("dummy_request")
99
+ expect(dummy_request).to receive(:call).twice do |auth|
100
+ expired_token = ::RDStation::Error::ExpiredAccessToken.new({'error_message' => 'x'})
101
+ raise expired_token unless auth.access_token == new_access_token
102
+ end
103
+
104
+ expect do
105
+ subject.retryable_request(auth) { |yielded_auth| dummy_request.call(yielded_auth) }
106
+ end.not_to raise_error
107
+ end
108
+ end
109
+ end
110
+
111
+ context 'original request raised a non retryable exception' do
112
+ it 'raises error' do
113
+ dummy_request = double("dummy_request")
114
+ expect(dummy_request).to receive(:call).once do |_|
115
+ raise RuntimeError.new("a non retryable error")
116
+ end
117
+
118
+ expect do
119
+ subject.retryable_request(auth) { |yielded_auth| dummy_request.call(yielded_auth) }
120
+ end.to raise_error RuntimeError
121
+ end
122
+ end
123
+ end
124
+
125
+ context 'all legacy scenarios' do
126
+ let (:access_token) { 'access_token' }
127
+ let (:auth) { ::RDStation::Authorization.new(access_token: access_token) }
128
+
129
+ it 'implement me' do
130
+ dummy_request = double("dummy_request")
131
+ expect(dummy_request).to receive(:call).once do |_|
132
+ raise ::RDStation::Error::ExpiredAccessToken.new({'error_message' => 'x'})
133
+ end
134
+
135
+ expect do
136
+ subject.retryable_request(auth) { |yielded_auth| dummy_request.call(yielded_auth) }
137
+ end.to raise_error ::RDStation::Error::ExpiredAccessToken
138
+ end
139
+ end
140
+
141
+ end
142
+ end