grape 1.2.4 → 1.2.5
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 +4 -4
- data/CHANGELOG.md +18 -4
- data/README.md +144 -4
- data/grape.gemspec +3 -1
- data/lib/grape.rb +94 -66
- data/lib/grape/api.rb +46 -4
- data/lib/grape/api/instance.rb +23 -12
- data/lib/grape/dsl/desc.rb +11 -2
- data/lib/grape/dsl/validations.rb +4 -3
- data/lib/grape/eager_load.rb +18 -0
- data/lib/grape/endpoint.rb +3 -3
- data/lib/grape/error_formatter.rb +1 -1
- data/lib/grape/exceptions/validation_errors.rb +4 -2
- data/lib/grape/formatter.rb +1 -1
- data/lib/grape/middleware/auth/base.rb +2 -4
- data/lib/grape/middleware/base.rb +2 -0
- data/lib/grape/middleware/helpers.rb +10 -0
- data/lib/grape/parser.rb +1 -1
- data/lib/grape/util/base_inheritable.rb +34 -0
- data/lib/grape/util/inheritable_values.rb +5 -25
- data/lib/grape/util/lazy_block.rb +25 -0
- data/lib/grape/util/lazy_value.rb +5 -0
- data/lib/grape/util/reverse_stackable_values.rb +7 -36
- data/lib/grape/util/stackable_values.rb +19 -22
- data/lib/grape/validations/attributes_iterator.rb +5 -3
- data/lib/grape/validations/multiple_attributes_iterator.rb +11 -0
- data/lib/grape/validations/params_scope.rb +12 -12
- data/lib/grape/validations/single_attribute_iterator.rb +13 -0
- data/lib/grape/validations/validator_factory.rb +6 -11
- data/lib/grape/validations/validators/all_or_none.rb +6 -13
- data/lib/grape/validations/validators/at_least_one_of.rb +5 -13
- data/lib/grape/validations/validators/base.rb +11 -10
- data/lib/grape/validations/validators/coerce.rb +4 -0
- data/lib/grape/validations/validators/default.rb +1 -1
- data/lib/grape/validations/validators/exactly_one_of.rb +6 -23
- data/lib/grape/validations/validators/multiple_params_base.rb +14 -10
- data/lib/grape/validations/validators/mutual_exclusion.rb +6 -18
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api/defines_boolean_in_params_spec.rb +37 -0
- data/spec/grape/api_remount_spec.rb +158 -0
- data/spec/grape/api_spec.rb +72 -0
- data/spec/grape/endpoint_spec.rb +1 -1
- data/spec/grape/exceptions/base_spec.rb +4 -0
- data/spec/grape/exceptions/validation_errors_spec.rb +6 -4
- data/spec/grape/integration/rack_spec.rb +22 -6
- data/spec/grape/middleware/base_spec.rb +8 -0
- data/spec/grape/middleware/formatter_spec.rb +11 -1
- data/spec/grape/validations/multiple_attributes_iterator_spec.rb +29 -0
- data/spec/grape/validations/params_scope_spec.rb +13 -0
- data/spec/grape/validations/single_attribute_iterator_spec.rb +33 -0
- data/spec/grape/validations/validators/all_or_none_spec.rb +138 -30
- data/spec/grape/validations/validators/at_least_one_of_spec.rb +173 -29
- data/spec/grape/validations/validators/coerce_spec.rb +6 -2
- data/spec/grape/validations/validators/exactly_one_of_spec.rb +202 -38
- data/spec/grape/validations/validators/mutual_exclusion_spec.rb +184 -27
- data/spec/grape/validations_spec.rb +32 -20
- metadata +103 -115
- data/Appraisals +0 -28
- data/Dangerfile +0 -2
- data/Gemfile +0 -33
- data/Gemfile.lock +0 -231
- data/Guardfile +0 -10
- data/RELEASING.md +0 -111
- data/Rakefile +0 -25
- data/benchmark/simple.rb +0 -27
- data/benchmark/simple_with_type_coercer.rb +0 -22
- data/gemfiles/multi_json.gemfile +0 -35
- data/gemfiles/multi_xml.gemfile +0 -35
- data/gemfiles/rack_1.5.2.gemfile.lock +0 -232
- data/gemfiles/rack_edge.gemfile +0 -35
- data/gemfiles/rails_3.gemfile +0 -36
- data/gemfiles/rails_3.gemfile.lock +0 -288
- data/gemfiles/rails_4.gemfile +0 -35
- data/gemfiles/rails_4.gemfile.lock +0 -280
- data/gemfiles/rails_5.gemfile +0 -35
- data/gemfiles/rails_5.gemfile.lock +0 -312
- data/gemfiles/rails_edge.gemfile +0 -35
- data/pkg/grape-1.2.0.gem +0 -0
- data/pkg/grape-1.2.1.gem +0 -0
- data/pkg/grape-1.2.3.gem +0 -0
@@ -165,6 +165,19 @@ describe Grape::Validations::ParamsScope do
|
|
165
165
|
|
166
166
|
expect(last_response.status).to eq(200)
|
167
167
|
end
|
168
|
+
|
169
|
+
it do
|
170
|
+
subject.params do
|
171
|
+
requires :foo, as: :baz, type: Hash do
|
172
|
+
requires :bar, as: :qux
|
173
|
+
end
|
174
|
+
end
|
175
|
+
subject.get('/nested-renaming') { declared(params).to_json }
|
176
|
+
get '/nested-renaming', foo: { bar: 'any' }
|
177
|
+
|
178
|
+
expect(last_response.status).to eq(200)
|
179
|
+
expect(last_response.body).to eq('{"baz":{"qux":"any"}}')
|
180
|
+
end
|
168
181
|
end
|
169
182
|
|
170
183
|
context 'array without coerce type explicitly given' do
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::Validations::SingleAttributeIterator do
|
4
|
+
describe '#each' do
|
5
|
+
subject(:iterator) { described_class.new(validator, scope, params) }
|
6
|
+
let(:scope) { Grape::Validations::ParamsScope.new(api: Class.new(Grape::API)) }
|
7
|
+
let(:validator) { double(attrs: %i[first second third]) }
|
8
|
+
|
9
|
+
context 'when params is a hash' do
|
10
|
+
let(:params) do
|
11
|
+
{ first: 'string', second: 'string' }
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'yields params and every single attribute from the list' do
|
15
|
+
expect { |b| iterator.each(&b) }
|
16
|
+
.to yield_successive_args([params, :first], [params, :second], [params, :third])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'when params is an array' do
|
21
|
+
let(:params) do
|
22
|
+
[{ first: 'string1', second: 'string1' }, { first: 'string2', second: 'string2' }]
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'yields every single attribute from the list for each of the array elements' do
|
26
|
+
expect { |b| iterator.each(&b) }.to yield_successive_args(
|
27
|
+
[params[0], :first], [params[0], :second], [params[0], :third],
|
28
|
+
[params[1], :first], [params[1], :second], [params[1], :third]
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -2,58 +2,166 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Grape::Validations::AllOrNoneOfValidator do
|
4
4
|
describe '#validate!' do
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
subject(:validate) { post path, params }
|
6
|
+
|
7
|
+
module ValidationsSpec
|
8
|
+
module AllOrNoneOfValidatorSpec
|
9
|
+
class API < Grape::API
|
10
|
+
rescue_from Grape::Exceptions::ValidationErrors do |e|
|
11
|
+
error!(e.errors.transform_keys! { |key| key.join(',') }, 400)
|
12
|
+
end
|
13
|
+
|
14
|
+
params do
|
15
|
+
optional :beer, :wine, type: Boolean
|
16
|
+
all_or_none_of :beer, :wine
|
17
|
+
end
|
18
|
+
post do
|
19
|
+
end
|
20
|
+
|
21
|
+
params do
|
22
|
+
optional :beer, :wine, :other, type: Boolean
|
23
|
+
all_or_none_of :beer, :wine
|
24
|
+
end
|
25
|
+
post 'mixed-params' do
|
26
|
+
end
|
27
|
+
|
28
|
+
params do
|
29
|
+
optional :beer, :wine, type: Boolean
|
30
|
+
all_or_none_of :beer, :wine, message: 'choose all or none'
|
31
|
+
end
|
32
|
+
post '/custom-message' do
|
33
|
+
end
|
34
|
+
|
35
|
+
params do
|
36
|
+
requires :item, type: Hash do
|
37
|
+
optional :beer, :wine, type: Boolean
|
38
|
+
all_or_none_of :beer, :wine
|
39
|
+
end
|
40
|
+
end
|
41
|
+
post '/nested-hash' do
|
42
|
+
end
|
43
|
+
|
44
|
+
params do
|
45
|
+
requires :items, type: Array do
|
46
|
+
optional :beer, :wine, type: Boolean
|
47
|
+
all_or_none_of :beer, :wine
|
48
|
+
end
|
49
|
+
end
|
50
|
+
post '/nested-array' do
|
51
|
+
end
|
10
52
|
|
11
|
-
|
53
|
+
params do
|
54
|
+
requires :items, type: Array do
|
55
|
+
requires :nested_items, type: Array do
|
56
|
+
optional :beer, :wine, type: Boolean
|
57
|
+
all_or_none_of :beer, :wine
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
post '/deeply-nested-array' do
|
62
|
+
end
|
63
|
+
end
|
12
64
|
end
|
13
65
|
end
|
14
|
-
|
15
|
-
|
66
|
+
|
67
|
+
def app
|
68
|
+
ValidationsSpec::AllOrNoneOfValidatorSpec::API
|
69
|
+
end
|
16
70
|
|
17
71
|
context 'when all restricted params are present' do
|
18
|
-
let(:
|
72
|
+
let(:path) { '/' }
|
73
|
+
let(:params) { { beer: true, wine: true } }
|
19
74
|
|
20
|
-
it 'does not
|
21
|
-
|
75
|
+
it 'does not return a validation error' do
|
76
|
+
validate
|
77
|
+
expect(last_response.status).to eq 201
|
22
78
|
end
|
23
79
|
|
24
80
|
context 'mixed with other params' do
|
25
|
-
let(:
|
81
|
+
let(:path) { '/mixed-params' }
|
82
|
+
let(:params) { { beer: true, wine: true, other: true } }
|
26
83
|
|
27
|
-
it 'does not
|
28
|
-
|
84
|
+
it 'does not return a validation error' do
|
85
|
+
validate
|
86
|
+
expect(last_response.status).to eq 201
|
29
87
|
end
|
30
88
|
end
|
31
89
|
end
|
32
90
|
|
33
|
-
context 'when
|
91
|
+
context 'when a subset of restricted params are present' do
|
92
|
+
let(:path) { '/' }
|
93
|
+
let(:params) { { beer: true } }
|
94
|
+
|
95
|
+
it 'returns a validation error' do
|
96
|
+
validate
|
97
|
+
expect(last_response.status).to eq 400
|
98
|
+
expect(JSON.parse(last_response.body)).to eq(
|
99
|
+
'beer,wine' => ['provide all or none of parameters']
|
100
|
+
)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'when custom message is specified' do
|
105
|
+
let(:path) { '/custom-message' }
|
106
|
+
let(:params) { { beer: true } }
|
107
|
+
|
108
|
+
it 'returns a validation error' do
|
109
|
+
validate
|
110
|
+
expect(last_response.status).to eq 400
|
111
|
+
expect(JSON.parse(last_response.body)).to eq(
|
112
|
+
'beer,wine' => ['choose all or none']
|
113
|
+
)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'when no restricted params are present' do
|
118
|
+
let(:path) { '/' }
|
34
119
|
let(:params) { { somethingelse: true } }
|
35
120
|
|
36
|
-
it 'does not
|
37
|
-
|
121
|
+
it 'does not return a validation error' do
|
122
|
+
validate
|
123
|
+
expect(last_response.status).to eq 201
|
38
124
|
end
|
39
125
|
end
|
40
126
|
|
41
|
-
context 'when
|
42
|
-
let(:
|
127
|
+
context 'when restricted params are nested inside required hash' do
|
128
|
+
let(:path) { '/nested-hash' }
|
129
|
+
let(:params) { { item: { beer: true } } }
|
43
130
|
|
44
|
-
it '
|
45
|
-
|
46
|
-
|
47
|
-
|
131
|
+
it 'returns a validation error with full names of the params' do
|
132
|
+
validate
|
133
|
+
expect(last_response.status).to eq 400
|
134
|
+
expect(JSON.parse(last_response.body)).to eq(
|
135
|
+
'item[beer],item[wine]' => ['provide all or none of parameters']
|
136
|
+
)
|
48
137
|
end
|
49
|
-
|
50
|
-
let(:mixed_params) { params.merge!(other: true, andanother: true) }
|
138
|
+
end
|
51
139
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
140
|
+
context 'when mutually exclusive params are nested inside array' do
|
141
|
+
let(:path) { '/nested-array' }
|
142
|
+
let(:params) { { items: [{ beer: true, wine: true }, { wine: true }] } }
|
143
|
+
|
144
|
+
it 'returns a validation error with full names of the params' do
|
145
|
+
validate
|
146
|
+
expect(last_response.status).to eq 400
|
147
|
+
expect(JSON.parse(last_response.body)).to eq(
|
148
|
+
'items[1][beer],items[1][wine]' => ['provide all or none of parameters']
|
149
|
+
)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context 'when mutually exclusive params are deeply nested' do
|
154
|
+
let(:path) { '/deeply-nested-array' }
|
155
|
+
let(:params) { { items: [{ nested_items: [{ beer: true }] }] } }
|
156
|
+
|
157
|
+
it 'returns a validation error with full names of the params' do
|
158
|
+
validate
|
159
|
+
expect(last_response.status).to eq 400
|
160
|
+
expect(JSON.parse(last_response.body)).to eq(
|
161
|
+
'items[0][nested_items][0][beer],items[0][nested_items][0][wine]' => [
|
162
|
+
'provide all or none of parameters'
|
163
|
+
]
|
164
|
+
)
|
57
165
|
end
|
58
166
|
end
|
59
167
|
end
|
@@ -2,65 +2,209 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Grape::Validations::AtLeastOneOfValidator do
|
4
4
|
describe '#validate!' do
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
subject(:validate) { post path, params }
|
6
|
+
|
7
|
+
module ValidationsSpec
|
8
|
+
module AtLeastOneOfValidatorSpec
|
9
|
+
class API < Grape::API
|
10
|
+
rescue_from Grape::Exceptions::ValidationErrors do |e|
|
11
|
+
error!(e.errors.transform_keys! { |key| key.join(',') }, 400)
|
12
|
+
end
|
13
|
+
|
14
|
+
params do
|
15
|
+
optional :beer, :wine, :grapefruit
|
16
|
+
at_least_one_of :beer, :wine, :grapefruit
|
17
|
+
end
|
18
|
+
post do
|
19
|
+
end
|
20
|
+
|
21
|
+
params do
|
22
|
+
optional :beer, :wine, :grapefruit, :other
|
23
|
+
at_least_one_of :beer, :wine, :grapefruit
|
24
|
+
end
|
25
|
+
post 'mixed-params' do
|
26
|
+
end
|
27
|
+
|
28
|
+
params do
|
29
|
+
optional :beer, :wine, :grapefruit
|
30
|
+
at_least_one_of :beer, :wine, :grapefruit, message: 'you should choose something'
|
31
|
+
end
|
32
|
+
post '/custom-message' do
|
33
|
+
end
|
34
|
+
|
35
|
+
params do
|
36
|
+
requires :item, type: Hash do
|
37
|
+
optional :beer, :wine, :grapefruit
|
38
|
+
at_least_one_of :beer, :wine, :grapefruit, message: 'fail'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
post '/nested-hash' do
|
42
|
+
end
|
10
43
|
|
11
|
-
|
44
|
+
params do
|
45
|
+
requires :items, type: Array do
|
46
|
+
optional :beer, :wine, :grapefruit
|
47
|
+
at_least_one_of :beer, :wine, :grapefruit, message: 'fail'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
post '/nested-array' do
|
51
|
+
end
|
52
|
+
|
53
|
+
params do
|
54
|
+
requires :items, type: Array do
|
55
|
+
requires :nested_items, type: Array do
|
56
|
+
optional :beer, :wine, :grapefruit
|
57
|
+
at_least_one_of :beer, :wine, :grapefruit, message: 'fail'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
post '/deeply-nested-array' do
|
62
|
+
end
|
63
|
+
end
|
12
64
|
end
|
13
65
|
end
|
14
|
-
|
15
|
-
|
66
|
+
|
67
|
+
def app
|
68
|
+
ValidationsSpec::AtLeastOneOfValidatorSpec::API
|
69
|
+
end
|
16
70
|
|
17
71
|
context 'when all restricted params are present' do
|
72
|
+
let(:path) { '/' }
|
18
73
|
let(:params) { { beer: true, wine: true, grapefruit: true } }
|
19
74
|
|
20
|
-
it 'does not
|
21
|
-
|
75
|
+
it 'does not return a validation error' do
|
76
|
+
validate
|
77
|
+
expect(last_response.status).to eq 201
|
22
78
|
end
|
23
79
|
|
24
80
|
context 'mixed with other params' do
|
25
|
-
let(:
|
81
|
+
let(:path) { '/mixed-params' }
|
82
|
+
let(:params) { { beer: true, wine: true, grapefruit: true, other: true } }
|
26
83
|
|
27
|
-
it 'does not
|
28
|
-
|
84
|
+
it 'does not return a validation error' do
|
85
|
+
validate
|
86
|
+
expect(last_response.status).to eq 201
|
29
87
|
end
|
30
88
|
end
|
31
89
|
end
|
32
90
|
|
33
91
|
context 'when a subset of restricted params are present' do
|
92
|
+
let(:path) { '/' }
|
34
93
|
let(:params) { { beer: true, grapefruit: true } }
|
35
94
|
|
36
|
-
it 'does not
|
37
|
-
|
95
|
+
it 'does not return a validation error' do
|
96
|
+
validate
|
97
|
+
expect(last_response.status).to eq 201
|
38
98
|
end
|
39
99
|
end
|
40
100
|
|
41
|
-
context 'when
|
42
|
-
let(:
|
101
|
+
context 'when none of the restricted params is selected' do
|
102
|
+
let(:path) { '/' }
|
103
|
+
let(:params) { { other: true } }
|
104
|
+
|
105
|
+
it 'returns a validation error' do
|
106
|
+
validate
|
107
|
+
expect(last_response.status).to eq 400
|
108
|
+
expect(JSON.parse(last_response.body)).to eq(
|
109
|
+
'beer,wine,grapefruit' => ['are missing, at least one parameter must be provided']
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'when custom message is specified' do
|
114
|
+
let(:path) { '/custom-message' }
|
43
115
|
|
44
|
-
|
45
|
-
|
116
|
+
it 'returns a validation error' do
|
117
|
+
validate
|
118
|
+
expect(last_response.status).to eq 400
|
119
|
+
expect(JSON.parse(last_response.body)).to eq(
|
120
|
+
'beer,wine,grapefruit' => ['you should choose something']
|
121
|
+
)
|
122
|
+
end
|
46
123
|
end
|
47
124
|
end
|
48
125
|
|
49
|
-
context 'when
|
50
|
-
let(:
|
126
|
+
context 'when exactly one of the restricted params is selected' do
|
127
|
+
let(:path) { '/' }
|
128
|
+
let(:params) { { beer: true } }
|
51
129
|
|
52
|
-
it '
|
53
|
-
|
54
|
-
|
55
|
-
end.to raise_error(Grape::Exceptions::Validation)
|
130
|
+
it 'does not return a validation error' do
|
131
|
+
validate
|
132
|
+
expect(last_response.status).to eq 201
|
56
133
|
end
|
57
134
|
end
|
58
135
|
|
59
|
-
context 'when
|
60
|
-
let(:
|
136
|
+
context 'when restricted params are nested inside hash' do
|
137
|
+
let(:path) { '/nested-hash' }
|
138
|
+
|
139
|
+
context 'when at least one of them is present' do
|
140
|
+
let(:params) { { item: { beer: true, wine: true } } }
|
141
|
+
|
142
|
+
it 'does not return a validation error' do
|
143
|
+
validate
|
144
|
+
expect(last_response.status).to eq 201
|
145
|
+
end
|
146
|
+
end
|
61
147
|
|
62
|
-
|
63
|
-
|
148
|
+
context 'when none of them are present' do
|
149
|
+
let(:params) { { item: { other: true } } }
|
150
|
+
|
151
|
+
it 'returns a validation error with full names of the params' do
|
152
|
+
validate
|
153
|
+
expect(last_response.status).to eq 400
|
154
|
+
expect(JSON.parse(last_response.body)).to eq(
|
155
|
+
'item[beer],item[wine],item[grapefruit]' => ['fail']
|
156
|
+
)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
context 'when restricted params are nested inside array' do
|
162
|
+
let(:path) { '/nested-array' }
|
163
|
+
|
164
|
+
context 'when at least one of them is present' do
|
165
|
+
let(:params) { { items: [{ beer: true, wine: true }, { grapefruit: true }] } }
|
166
|
+
|
167
|
+
it 'does not return a validation error' do
|
168
|
+
validate
|
169
|
+
expect(last_response.status).to eq 201
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
context 'when none of them are present' do
|
174
|
+
let(:params) { { items: [{ beer: true, other: true }, { other: true }] } }
|
175
|
+
|
176
|
+
it 'returns a validation error with full names of the params' do
|
177
|
+
validate
|
178
|
+
expect(last_response.status).to eq 400
|
179
|
+
expect(JSON.parse(last_response.body)).to eq(
|
180
|
+
'items[1][beer],items[1][wine],items[1][grapefruit]' => ['fail']
|
181
|
+
)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context 'when restricted params are deeply nested' do
|
187
|
+
let(:path) { '/deeply-nested-array' }
|
188
|
+
|
189
|
+
context 'when at least one of them is present' do
|
190
|
+
let(:params) { { items: [{ nested_items: [{ wine: true }] }] } }
|
191
|
+
|
192
|
+
it 'does not return a validation error' do
|
193
|
+
validate
|
194
|
+
expect(last_response.status).to eq 201
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context 'when none of them are present' do
|
199
|
+
let(:params) { { items: [{ nested_items: [{ other: true }] }] } }
|
200
|
+
|
201
|
+
it 'returns a validation error with full names of the params' do
|
202
|
+
validate
|
203
|
+
expect(last_response.status).to eq 400
|
204
|
+
expect(JSON.parse(last_response.body)).to eq(
|
205
|
+
'items[0][nested_items][0][beer],items[0][nested_items][0][wine],items[0][nested_items][0][grapefruit]' => ['fail']
|
206
|
+
)
|
207
|
+
end
|
64
208
|
end
|
65
209
|
end
|
66
210
|
end
|