dry-validation 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -0
  3. data/.travis.yml +3 -2
  4. data/CHANGELOG.md +42 -0
  5. data/Gemfile +8 -1
  6. data/README.md +13 -89
  7. data/config/errors.yml +35 -29
  8. data/dry-validation.gemspec +2 -2
  9. data/examples/basic.rb +3 -7
  10. data/examples/each.rb +3 -8
  11. data/examples/form.rb +3 -6
  12. data/examples/nested.rb +7 -15
  13. data/lib/dry/validation.rb +33 -5
  14. data/lib/dry/validation/error.rb +10 -26
  15. data/lib/dry/validation/error_compiler.rb +69 -99
  16. data/lib/dry/validation/error_compiler/input.rb +148 -0
  17. data/lib/dry/validation/hint_compiler.rb +83 -33
  18. data/lib/dry/validation/input_processor_compiler.rb +98 -0
  19. data/lib/dry/validation/input_processor_compiler/form.rb +46 -0
  20. data/lib/dry/validation/input_processor_compiler/sanitizer.rb +46 -0
  21. data/lib/dry/validation/messages/abstract.rb +30 -10
  22. data/lib/dry/validation/messages/i18n.rb +2 -1
  23. data/lib/dry/validation/messages/namespaced.rb +1 -0
  24. data/lib/dry/validation/messages/yaml.rb +8 -5
  25. data/lib/dry/validation/result.rb +33 -25
  26. data/lib/dry/validation/schema.rb +168 -61
  27. data/lib/dry/validation/schema/attr.rb +5 -27
  28. data/lib/dry/validation/schema/check.rb +24 -0
  29. data/lib/dry/validation/schema/dsl.rb +97 -0
  30. data/lib/dry/validation/schema/form.rb +2 -26
  31. data/lib/dry/validation/schema/key.rb +32 -28
  32. data/lib/dry/validation/schema/rule.rb +88 -32
  33. data/lib/dry/validation/schema/value.rb +77 -27
  34. data/lib/dry/validation/schema_compiler.rb +38 -0
  35. data/lib/dry/validation/version.rb +1 -1
  36. data/spec/fixtures/locales/pl.yml +1 -1
  37. data/spec/integration/attr_spec.rb +122 -0
  38. data/spec/integration/custom_error_messages_spec.rb +9 -11
  39. data/spec/integration/custom_predicates_spec.rb +68 -18
  40. data/spec/integration/error_compiler_spec.rb +259 -65
  41. data/spec/integration/hints_spec.rb +28 -9
  42. data/spec/integration/injecting_rules_spec.rb +11 -12
  43. data/spec/integration/localized_error_messages_spec.rb +16 -16
  44. data/spec/integration/messages/i18n_spec.rb +9 -5
  45. data/spec/integration/optional_keys_spec.rb +9 -11
  46. data/spec/integration/schema/array_schema_spec.rb +23 -0
  47. data/spec/integration/schema/check_rules_spec.rb +39 -31
  48. data/spec/integration/schema/check_with_nth_el_spec.rb +25 -0
  49. data/spec/integration/schema/each_with_set_spec.rb +23 -24
  50. data/spec/integration/schema/form_spec.rb +122 -0
  51. data/spec/integration/schema/inheriting_schema_spec.rb +31 -0
  52. data/spec/integration/schema/input_processor_spec.rb +46 -0
  53. data/spec/integration/schema/macros/confirmation_spec.rb +33 -0
  54. data/spec/integration/schema/macros/maybe_spec.rb +32 -0
  55. data/spec/integration/schema/macros/required_spec.rb +59 -0
  56. data/spec/integration/schema/macros/when_spec.rb +65 -0
  57. data/spec/integration/schema/nested_values_spec.rb +41 -0
  58. data/spec/integration/schema/not_spec.rb +14 -14
  59. data/spec/integration/schema/option_with_default_spec.rb +30 -0
  60. data/spec/integration/schema/reusing_schema_spec.rb +33 -0
  61. data/spec/integration/schema/using_types_spec.rb +29 -0
  62. data/spec/integration/schema/xor_spec.rb +17 -14
  63. data/spec/integration/schema_spec.rb +75 -245
  64. data/spec/shared/rule_compiler.rb +8 -0
  65. data/spec/spec_helper.rb +13 -0
  66. data/spec/unit/hint_compiler_spec.rb +10 -10
  67. data/spec/unit/{input_type_compiler_spec.rb → input_processor_compiler/form_spec.rb} +88 -73
  68. data/spec/unit/schema/key_spec.rb +33 -0
  69. data/spec/unit/schema/rule_spec.rb +7 -6
  70. data/spec/unit/schema/value_spec.rb +187 -54
  71. metadata +53 -31
  72. data/.rubocop.yml +0 -16
  73. data/.rubocop_todo.yml +0 -7
  74. data/lib/dry/validation/input_type_compiler.rb +0 -83
  75. data/lib/dry/validation/schema/definition.rb +0 -74
  76. data/lib/dry/validation/schema/result.rb +0 -68
  77. data/rakelib/rubocop.rake +0 -18
  78. data/spec/integration/rule_groups_spec.rb +0 -94
  79. data/spec/integration/schema/attrs_spec.rb +0 -38
  80. data/spec/integration/schema/default_key_behavior_spec.rb +0 -23
  81. data/spec/integration/schema/grouped_rules_spec.rb +0 -57
  82. data/spec/integration/schema/nested_spec.rb +0 -31
  83. data/spec/integration/schema_form_spec.rb +0 -97
@@ -0,0 +1,30 @@
1
+ RSpec.describe Dry::Validation::Schema, 'defining an option with default value' do
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ configure do
5
+ option :db, -> { DB }
6
+
7
+ def unique?(name, value)
8
+ DB.none? { |item| item[name] == value }
9
+ end
10
+ end
11
+
12
+ key(:email) { filled? & unique?(:email) }
13
+ end
14
+ end
15
+
16
+ before do
17
+ DB = [{ email: 'jane@doe' }]
18
+ end
19
+
20
+ after do
21
+ Object.send(:remove_const, :DB)
22
+ end
23
+
24
+ it 'uses external dependency set by option with a default value' do
25
+ expect(schema.db).to be(DB)
26
+
27
+ expect(schema.(email: 'jade@doe')).to be_success
28
+ expect(schema.(email: 'jane@doe')).to be_failure
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ RSpec.describe 'Reusing schemas' do
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ key(:city).required
5
+
6
+ key(:location).schema(LocationSchema)
7
+ end
8
+ end
9
+
10
+ before do
11
+ LocationSchema = Dry::Validation.Schema do
12
+ configure { config.input_processor = :form }
13
+
14
+ key(:lat).required(:float?)
15
+ key(:lng).required(:float?)
16
+ end
17
+ end
18
+
19
+ after do
20
+ Object.send(:remove_const, :LocationSchema)
21
+ end
22
+
23
+ it 're-uses existing schema' do
24
+ expect(schema.(city: 'NYC', location: { lat: 1.23, lng: 45.6 })).to be_success
25
+
26
+ expect(schema.(city: 'NYC', location: { lat: nil, lng: '45.6' }).messages).to eql(
27
+ location: {
28
+ lat: ['must be filled'],
29
+ lng: ['must be a float']
30
+ }
31
+ )
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ RSpec.describe Dry::Validation::Schema, 'defining schema using dry types' do
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ key(:email).required(Email)
5
+ key(:age).maybe(Age)
6
+ end
7
+ end
8
+
9
+ before do
10
+ Email = Dry::Types['strict.string']
11
+ Age = Dry::Types['strict.int'].constrained(gt: 18)
12
+ end
13
+
14
+ after do
15
+ Object.send(:remove_const, :Email)
16
+ Object.send(:remove_const, :Age)
17
+ end
18
+
19
+ it 'passes when input is valid' do
20
+ expect(schema.(email: 'jane@doe', age: 19)).to be_success
21
+ expect(schema.(email: 'jane@doe', age: nil)).to be_success
22
+ end
23
+
24
+ it 'fails when input is not valid' do
25
+ expect(schema.(email: '', age: 19)).to_not be_success
26
+ expect(schema.(email: 'jane@doe', age: 17)).to_not be_success
27
+ expect(schema.(email: 'jane@doe', age: '19')).to_not be_success
28
+ end
29
+ end
@@ -1,32 +1,35 @@
1
1
  RSpec.describe 'Schema with xor rules' do
2
- subject(:validate) { schema.new }
3
-
4
- let(:schema) do
5
- Class.new(Dry::Validation::Schema) do
6
- def self.messages
7
- Messages.default.merge(
8
- en: { errors: { be_reasonable: 'you cannot eat cake and have cake!' } }
9
- )
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ configure do
5
+ def self.messages
6
+ Messages.default.merge(
7
+ en: { errors: { be_reasonable: 'you cannot eat cake and have cake!' } }
8
+ )
9
+ end
10
10
  end
11
11
 
12
- key(:eat_cake) { |v| v.eql?('yes!') }
13
- key(:have_cake) { |v| v.eql?('yes!') }
12
+ key(:eat_cake).required
13
+
14
+ key(:have_cake).required
14
15
 
15
- rule(:be_reasonable) { rule(:eat_cake) ^ rule(:have_cake) }
16
+ rule(:be_reasonable) do
17
+ value(:eat_cake).eql?('yes!') ^ value(:have_cake).eql?('yes!')
18
+ end
16
19
  end
17
20
  end
18
21
 
19
22
  describe '#messages' do
20
23
  it 'passes when only one option is selected' do
21
- messages = validate.(eat_cake: 'yes!', have_cake: 'no!').messages[:be_reasonable]
24
+ messages = schema.(eat_cake: 'yes!', have_cake: 'no!').messages[:be_reasonable]
22
25
 
23
26
  expect(messages).to be(nil)
24
27
  end
25
28
 
26
29
  it 'fails when both options are selected' do
27
- messages = validate.(eat_cake: 'yes!', have_cake: 'yes!').messages[:be_reasonable]
30
+ messages = schema.(eat_cake: 'yes!', have_cake: 'yes!').messages[:be_reasonable]
28
31
 
29
- expect(messages).to eql([['you cannot eat cake and have cake!'], 'yes!'])
32
+ expect(messages).to eql(['you cannot eat cake and have cake!'])
30
33
  end
31
34
  end
32
35
  end
@@ -1,179 +1,55 @@
1
- RSpec.describe Dry::Validation::Schema do
2
- subject(:validation) { schema.new }
3
-
4
- describe 'defining key-based schema (hash-like)' do
5
- let(:schema) do
6
- Class.new(Dry::Validation::Schema) do
7
- key(:email) { |email| email.filled? }
8
-
9
- key(:age) do |age|
10
- age.none? | (age.int? & age.gt?(18))
11
- end
12
-
13
- key(:address) do |address|
14
- address.hash? do
15
- address.key(:city) do |city|
16
- city.min_size?(3)
17
- end
18
-
19
- address.key(:street) do |street|
20
- street.filled?
21
- end
22
-
23
- address.key(:country) do |country|
24
- country.key(:name, &:filled?)
25
- country.key(:code, &:filled?)
26
- end
27
- end
28
- end
29
-
30
- key(:phone_numbers) do |phone_numbers|
31
- phone_numbers.array? { phone_numbers.each(&:str?) }
32
- end
1
+ RSpec.describe Dry::Validation::Schema, 'defining key-based schema' do
2
+ describe 'with a flat structure' do
3
+ subject(:schema) do
4
+ Dry::Validation.Schema do
5
+ key(:email).required
6
+ key(:age) { none? | (int? & gt?(18)) }
33
7
  end
34
8
  end
35
9
 
36
- let(:input) do
37
- {
38
- email: 'jane@doe.org',
39
- age: 19,
40
- address: { city: 'NYC', street: 'Street 1/2', country: { code: 'US', name: 'USA' } },
41
- phone_numbers: [
42
- '123456', '234567'
43
- ]
44
- }.freeze
10
+ it 'passes when input is valid' do
11
+ expect(schema.(email: 'jane@doe', age: 19)).to be_success
12
+ expect(schema.(email: 'jane@doe', age: nil)).to be_success
45
13
  end
46
14
 
47
- describe '#messages' do
48
- it 'returns compiled error messages' do
49
- expect(validation.(input.merge(email: '')).messages).to match_array([
50
- [:email, [['email must be filled'], '']]
51
- ])
52
- end
15
+ it 'fails when input is not valid' do
16
+ expect(schema.(email: 'jane@doe', age: 17)).to_not be_success
53
17
  end
54
18
 
55
- describe '#call' do
56
- it 'passes when attributes are valid' do
57
- expect(validation.(input)).to be_empty
58
- end
59
-
60
- it 'validates presence of an email and min age value' do
61
- expect(validation.(input.merge(email: '', age: 18))).to match_array([
62
- [:error, [:input, [:age, 18, [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]],
63
- [:error, [:input, [:email, "", [[:val, [:email, [:predicate, [:filled?, []]]]]]]]]
64
- ])
65
- end
66
-
67
- it 'validates presence of the email key and type of age value' do
68
- expect(validation.(name: 'Jane', age: '18', address: input[:address], phone_numbers: input[:phone_numbers])).to match_array([
69
- [:error, [:input, [:age, "18", [[:val, [:age, [:predicate, [:int?, []]]]]]]]],
70
- [:error, [:input, [:email, nil, [[:key, [:email, [:predicate, [:key?, [:email]]]]]]]]]
71
- ])
72
- end
73
-
74
- it 'validates presence of the address and phone_number keys' do
75
- expect(validation.(email: 'jane@doe.org', age: 19)).to match_array([
76
- [:error, [:input, [:address, nil, [[:key, [:address, [:predicate, [:key?, [:address]]]]]]]]],
77
- [:error, [:input, [:phone_numbers, nil, [[:key, [:phone_numbers, [:predicate, [:key?, [:phone_numbers]]]]]]]]]
78
- ])
79
- end
80
-
81
- it 'validates presence of keys under address and min size of the city value' do
82
- expect(validation.(input.merge(address: { city: 'NY' }))).to match_array([
83
- [:error, [
84
- :input, [
85
- :address, {city: "NY"},
86
- [
87
- [:input, [:city, "NY", [[:val, [:city, [:predicate, [:min_size?, [3]]]]]]]],
88
- [:input, [:street, nil, [[:key, [:street, [:predicate, [:key?, [:street]]]]]]]],
89
- [:input, [:country, nil, [[:key, [:country, [:predicate, [:key?, [:country]]]]]]]]
90
- ]
91
- ]
92
- ]]
93
- ])
94
- end
19
+ it 'returns result which quacks like hash' do
20
+ input = { email: 'jane@doe', age: 19 }
21
+ result = schema.(input)
95
22
 
96
- it 'validates address type' do
97
- expect(validation.(input.merge(address: 'totally not a hash'))).to match_array([
98
- [:error, [:input, [:address, "totally not a hash", [[:val, [:address, [:predicate, [:hash?, []]]]]]]]]
99
- ])
100
- end
101
-
102
- it 'validates address code and name values' do
103
- expect(validation.(input.merge(address: input[:address].merge(country: { code: 'US', name: '' })))).to match_array([
104
- [:error, [
105
- :input, [
106
- :address, {city: "NYC", street: "Street 1/2", country: {code: "US", name: ""}},
107
- [
108
- [
109
- :input, [
110
- :country, {code: "US", name: ""}, [
111
- [
112
- :input, [
113
- :name, "", [[:val, [:name, [:predicate, [:filled?, []]]]]]
114
- ]
115
- ]
116
- ]
117
- ]
118
- ]
119
- ]
120
- ]
121
- ]]
122
- ])
123
- end
23
+ expect(result[:email]).to eql('jane@doe')
24
+ expect(Hash[result]).to eql(input)
124
25
 
125
- it 'validates each phone number' do
126
- expect(validation.(input.merge(phone_numbers: ['123', 312]))).to match_array([
127
- [:error, [
128
- :input, [
129
- :phone_numbers, ["123", 312],
130
- [
131
- [
132
- :input, [
133
- :phone_numbers, 312, [
134
- [:val, [:phone_numbers, [:predicate, [:str?, []]]]]
135
- ]
136
- ]
137
- ]
138
- ]
139
- ]
140
- ]]
141
- ])
142
- end
26
+ expect(result.to_a).to eql([[:email, 'jane@doe'], [:age, 19]])
143
27
  end
144
28
  end
145
29
 
146
- describe 'defining attr-based schema (model-like)' do
147
- let(:schema) do
148
- Class.new(Dry::Validation::Schema) do
149
- attr(:email) { |email| email.filled? }
30
+ describe 'with nested structures' do
31
+ subject(:schema) do
32
+ Dry::Validation.Schema do
33
+ key(:email).required
150
34
 
151
- attr(:age) do |age|
152
- age.none? | (age.int? & age.gt?(18))
153
- end
35
+ key(:age).maybe(:int?, gt?: 18)
154
36
 
155
- attr(:address) do |address|
156
- address.attr(:city) do |city|
157
- city.min_size?(3)
158
- end
37
+ key(:address).schema do
38
+ key(:city).required(min_size?: 3)
159
39
 
160
- address.attr(:street) do |street|
161
- street.filled?
162
- end
40
+ key(:street).required
163
41
 
164
- address.attr(:country) do |country|
165
- country.attr(:name, &:filled?)
166
- country.attr(:code, &:filled?)
42
+ key(:country).schema do
43
+ key(:name).required
44
+ key(:code).required
167
45
  end
168
46
  end
169
47
 
170
- attr(:phone_numbers) do |phone_numbers|
171
- phone_numbers.array? { phone_numbers.each(&:str?) }
172
- end
48
+ key(:phone_numbers).each(:str?)
173
49
  end
174
50
  end
175
51
 
176
- let(:input_data) do
52
+ let(:input) do
177
53
  {
178
54
  email: 'jane@doe.org',
179
55
  age: 19,
@@ -184,126 +60,80 @@ RSpec.describe Dry::Validation::Schema do
184
60
  }.freeze
185
61
  end
186
62
 
187
- def input(data = input_data)
188
- struct_from_hash(data)
189
- end
190
-
191
63
  describe '#messages' do
192
64
  it 'returns compiled error messages' do
193
- expect(validation.(input(input_data.merge(email: ''))).messages).to match_array([
194
- [:email, [["email must be filled"], '']]
195
- ])
65
+ expect(schema.(input.merge(email: '')).messages).to eql(
66
+ email: ['must be filled']
67
+ )
196
68
  end
197
69
  end
198
70
 
199
71
  describe '#call' do
200
72
  it 'passes when attributes are valid' do
201
- expect(validation.(input)).to be_empty
73
+ expect(schema.(input)).to be_success
202
74
  end
203
75
 
204
76
  it 'validates presence of an email and min age value' do
205
- expect(validation.(input(input_data.merge(email: '', age: 18)))).to match_array([
206
- [:error, [:input, [:age, 18, [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]],
207
- [:error, [:input, [:email, "", [[:val, [:email, [:predicate, [:filled?, []]]]]]]]]
208
- ])
77
+ expect(schema.(input.merge(email: '', age: 18)).messages).to eql(
78
+ email: ['must be filled'], age: ['must be greater than 18']
79
+ )
209
80
  end
210
81
 
211
- it 'validates presence of the email attr and type of age value' do
212
- input_object = input(input_data.reject { |k, v| k == :email }.merge(age: '18'))
82
+ it 'validates presence of the email key and type of age value' do
83
+ attrs = {
84
+ name: 'Jane',
85
+ age: '18',
86
+ address: input[:address], phone_numbers: input[:phone_numbers]
87
+ }
213
88
 
214
- expect(validation.(input_object)).to match_array([
215
- [:error, [:input, [:age, "18", [[:val, [:age, [:predicate, [:int?, []]]]]]]]],
216
- [:error, [:input, [:email, input_object, [[:attr, [:email, [:predicate, [:attr?, [:email]]]]]]]]]
217
- ])
89
+ expect(schema.(attrs).messages).to eql(
90
+ email: ['is missing'],
91
+ age: ['must be an integer', 'must be greater than 18']
92
+ )
218
93
  end
219
94
 
220
95
  it 'validates presence of the address and phone_number keys' do
221
- input_object = input(email: 'jane@doe.org', age: 19)
96
+ attrs = { email: 'jane@doe.org', age: 19 }
222
97
 
223
- expect(validation.(input_object)).to match_array([
224
- [:error, [
225
- :input, [
226
- :address, input_object,
227
- [
228
- [:attr, [:address, [:predicate, [:attr?, [:address]]]]]
229
- ]
230
- ]
231
- ]],
232
- [:error, [
233
- :input, [
234
- :phone_numbers, input_object,
235
- [
236
- [:attr, [:phone_numbers, [:predicate, [:attr?, [:phone_numbers]]]]]
237
- ]
238
- ]
239
- ]]
240
- ])
98
+ expect(schema.(attrs).messages).to eql(
99
+ address: ['is missing'], phone_numbers: ['is missing']
100
+ )
241
101
  end
242
102
 
243
103
  it 'validates presence of keys under address and min size of the city value' do
244
- address = { city: 'NY' }
245
- input_object = input(input_data.merge(address: address))
246
- address_object = input_object.address.class.from_hash(address)
104
+ attrs = input.merge(address: { city: 'NY' })
247
105
 
248
- expect(validation.(input_object)).to match_array([
249
- [:error, [
250
- :input, [
251
- :address, address_object,
252
- [
253
- [:input, [:city, "NY", [[:val, [:city, [:predicate, [:min_size?, [3]]]]]]]],
254
- [:input, [:street, address_object, [[:attr, [:street, [:predicate, [:attr?, [:street]]]]]]]],
255
- [:input, [:country, address_object, [[:attr, [:country, [:predicate, [:attr?, [:country]]]]]]]]
256
- ]
257
- ]
258
- ]]
259
- ])
106
+ expect(schema.(attrs).messages).to eql(
107
+ address: {
108
+ street: ['is missing'],
109
+ country: ['is missing'],
110
+ city: ['size cannot be less than 3']
111
+ }
112
+ )
260
113
  end
261
114
 
262
- it 'validates address code and name values' do
263
- input_object = input(input_data.merge(address: input_data[:address].merge(country: { code: 'US', name: '' })))
115
+ it 'validates address type' do
116
+ expect(schema.(input.merge(address: 'totally not a hash')).messages).to eql(
117
+ address: ['must be a hash']
118
+ )
119
+ end
264
120
 
265
- country_object = input_object.address.country.class.from_hash(code: "US", name: "")
121
+ it 'validates address code and name values' do
122
+ attrs = input.merge(
123
+ address: input[:address].merge(country: { code: 'US', name: '' })
124
+ )
266
125
 
267
- expect(validation.(input_object)).to match_array([
268
- [:error, [
269
- :input, [
270
- :address, input_object.address.class.from_hash(city: "NYC", street: "Street 1/2", country: country_object),
271
- [
272
- [
273
- :input, [
274
- :country, country_object, [
275
- [
276
- :input, [
277
- :name, "", [[:val, [:name, [:predicate, [:filled?, []]]]]]
278
- ]
279
- ]
280
- ]
281
- ]
282
- ]
283
- ]
284
- ]
285
- ]]
286
- ])
126
+ expect(schema.(attrs).messages).to eql(
127
+ address: { country: { name: ['must be filled'] } }
128
+ )
287
129
  end
288
130
 
289
131
  it 'validates each phone number' do
290
- input_object = input(input_data.merge(phone_numbers: ['123', 312]))
132
+ attrs = input.merge(phone_numbers: ['123', 312])
291
133
 
292
- expect(validation.(input_object)).to match_array([
293
- [:error, [
294
- :input, [
295
- :phone_numbers, ["123", 312],[
296
- [
297
- :input, [
298
- :phone_numbers, 312, [
299
- [:val, [:phone_numbers, [:predicate, [:str?, []]]]]
300
- ]
301
- ]
302
- ]
303
- ]
304
- ]
305
- ]]
306
- ])
134
+ expect(schema.(attrs).messages).to eql(
135
+ phone_numbers: { 1 => ['must be a string'] }
136
+ )
307
137
  end
308
138
  end
309
139
  end