hoodoo 1.12.4 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,6 +11,52 @@ module Hoodoo
11
11
  #
12
12
  attr_accessor :properties
13
13
 
14
+ # Initialize an Array instance with the appropriate name and options.
15
+ #
16
+ # +name+:: The JSON key.
17
+ # +options+:: A +Hash+ of options, e.g. <tt>:required => true,
18
+ # :type => :enum, :field_from => [ 1, 2, 3, 4 ]</tt>. If
19
+ # a +:type+ field is present, the Array contains atomic
20
+ # types of the given kind. Otherwise, either pass a
21
+ # block with inner schema DSL calls describing complex
22
+ # array entry schema, or nothing for no array content
23
+ # validation. If a block _and_ +:type+ option are
24
+ # passed, the block is used and option ignored.
25
+ #
26
+ def initialize( name, options = {} )
27
+ super( name, options )
28
+
29
+ if options.has_key?( :type )
30
+
31
+ # Defining a property via "#property" adds it to the @properties
32
+ # array, but handling of simple Types in array validation and
33
+ # rendering is too different from complex types to use the same
34
+ # code flow; we need the property to be independently used, so
35
+ # extract it into its own instance variable and delete the item
36
+ # from @properties.
37
+ #
38
+ value_klass = type_option_to_class( options[ :type ] )
39
+ random_name = Hoodoo::UUID.generate()
40
+ @value_property = property( random_name,
41
+ value_klass,
42
+ extract_field_prefix_options_from( options ) )
43
+
44
+ @properties.delete( random_name )
45
+
46
+ # This is approaching a blunt hack. Without it, validation errors
47
+ # will result in e.g. "fields[1].cd2f0a15ec8e4bd6ab1964b25b044e69"
48
+ # in error messages. By using nil, the validation code's JSON path
49
+ # array to string code doesn't include the item, giving the
50
+ # desired result. In addition, the base class Field#render code
51
+ # has an important check for non-nil but empty and bails out, but
52
+ # allows the nil name case to render simple types as expected. A
53
+ # delicate / fragile balance of nil-vs-empty arises.
54
+ #
55
+ @value_property.name = nil
56
+
57
+ end
58
+ end
59
+
14
60
  # Check if data is a valid Array and return a Hoodoo::Errors instance.
15
61
  #
16
62
  def validate( data, path = '' )
@@ -18,8 +64,12 @@ module Hoodoo
18
64
  return errors if errors.has_errors? || ( ! @required && data.nil? )
19
65
 
20
66
  if data.is_a?( ::Array )
21
- # No array entry schema? No array entry validation, then.
22
- unless @properties.nil?
67
+
68
+ # A block which defined properties for this instance takes
69
+ # precedence; then check for a ":type" option via "@@value_property"
70
+ # stored in the constructor; then give up and do no validation.
71
+ #
72
+ if @properties.nil? == false && @properties.empty? == false
23
73
  data.each_with_index do | item, index |
24
74
  @properties.each do | name, property |
25
75
  rdata = ( item.is_a?( ::Hash ) && item.has_key?( name ) ) ? item[ name ] : nil
@@ -27,7 +77,13 @@ module Hoodoo
27
77
  errors.merge!( property.validate( rdata, indexed_path ) )
28
78
  end
29
79
  end
80
+ elsif @value_property.nil? == false
81
+ data.each_with_index do | item, index |
82
+ indexed_path = "#{ full_path( path ) }[#{ index }]"
83
+ errors.merge!( @value_property.validate( item, indexed_path ) )
84
+ end
30
85
  end
86
+
31
87
  else
32
88
  errors.add_error(
33
89
  'generic.invalid_array',
@@ -66,11 +122,7 @@ module Hoodoo
66
122
 
67
123
  # ...then look at rendering the input entries of 'data' into 'array'.
68
124
 
69
- if @properties.nil?
70
- # Must modify existing instance of 'array', so use 'push()'
71
- array.push( *data )
72
-
73
- else
125
+ if @properties.nil? == false && @properties.empty? == false
74
126
  data.each do | item |
75
127
 
76
128
  # We have properties defined so array values (in "item") must be
@@ -98,6 +150,21 @@ module Hoodoo
98
150
  # Must modify existing instance of 'array', so use 'push()'
99
151
  array.push( rendered )
100
152
  end
153
+
154
+ elsif @value_property.nil? == false
155
+ data.each do | item |
156
+ subtarget = {}
157
+ @value_property.render( item, subtarget )
158
+ rendered = subtarget.empty? ? nil : read_at_path( subtarget, path ).values.first
159
+
160
+ # Must modify existing instance of 'array', so use 'push()'
161
+ array.push( rendered )
162
+ end
163
+
164
+ else
165
+ # Must modify existing instance of 'array', so use 'push()'
166
+ array.push( *data )
167
+
101
168
  end
102
169
  end
103
170
 
@@ -71,7 +71,7 @@ module Hoodoo
71
71
  # Returns the full path array that was used (a clone of +@path+).
72
72
  #
73
73
  def render( data, target )
74
- return if @name.empty?
74
+ return if @name.nil? == false && @name.empty?
75
75
 
76
76
  root = target
77
77
  path = @path.clone
@@ -57,23 +57,30 @@ module Hoodoo
57
57
  raise "Can't use \#keys and then \#key in the same hash definition - use one or the other"
58
58
  end
59
59
 
60
+ @specific = true
61
+ value_klass = block_given? ?
62
+ Hoodoo::Presenters::Object :
63
+ type_option_to_class( options.delete( :type ) )
64
+
60
65
  # If we're defining specific keys and some of those keys have fields
61
- # with defaults, we need to merge those up to provide a whole-key
66
+ # with defaults, we need to merge those up to provide a whole-Hash
62
67
  # equivalent default. If someone renders an empty hash they expect a
63
68
  # specific key with some internal defaults to be rendered; doing this
64
69
  # amalgamation up to key level is the easiest way to handle that.
65
-
66
- if options.has_key?( :default )
67
- @has_default = true
68
-
69
- @default ||= {}
70
- @default.merge!( Hoodoo::Utilities.stringify( options[ :default ] ) )
70
+ #
71
+ if options.has_key?( :default ) || options.has_key?( :field_default )
72
+ @has_default = true
73
+ @default ||= {}
74
+ @default[ name ] = options[ :default ] || options[ :field_default ]
71
75
  end
72
76
 
73
- @specific = true
74
-
75
- klass = block_given? ? Hoodoo::Presenters::Object : Hoodoo::Presenters::Field
76
- prop = property( name, klass, options, &block )
77
+ prop = property( name,
78
+ value_klass,
79
+ Hoodoo::Utilities.deep_merge_into(
80
+ options,
81
+ extract_field_prefix_options_from( options )
82
+ ),
83
+ &block )
77
84
 
78
85
  if prop && prop.respond_to?( :is_internationalised? ) && prop.is_internationalised?
79
86
  internationalised()
@@ -148,12 +155,20 @@ module Hoodoo
148
155
  end
149
156
 
150
157
  @specific = false
158
+ key_klass = options.has_key?( :length ) ?
159
+ Hoodoo::Presenters::String :
160
+ Hoodoo::Presenters::Text
161
+
162
+ property( 'keys', key_klass, options )
151
163
 
152
- klass = options.has_key?( :length ) ? Hoodoo::Presenters::String : Hoodoo::Presenters::Text
153
- property('keys', klass, options)
164
+ value_klass = block_given? ?
165
+ Hoodoo::Presenters::Object :
166
+ type_option_to_class( options[ :type ] )
154
167
 
155
- klass = block_given? ? Hoodoo::Presenters::Object : Hoodoo::Presenters::Field
156
- prop = property( 'values', klass, {}, &block )
168
+ prop = property( 'values',
169
+ value_klass,
170
+ extract_field_prefix_options_from( options ),
171
+ &block )
157
172
 
158
173
  if prop && prop.respond_to?( :is_internationalised? ) && prop.is_internationalised?
159
174
  internationalised()
@@ -237,7 +252,7 @@ module Hoodoo
237
252
  keys_property.rename( key )
238
253
  values_property.rename( key )
239
254
 
240
- errors.merge!( keys_property.validate( key, local_path ) )
255
+ errors.merge!( keys_property.validate( key, local_path ) )
241
256
  errors.merge!( values_property.validate( value, local_path ) )
242
257
  end
243
258
 
@@ -300,6 +300,7 @@ module Hoodoo
300
300
  # 8601 subset date and time, else +false+.
301
301
  #
302
302
  def self.valid_iso8601_subset_datetime?( str )
303
+ return false unless str.is_a?( String ) || str.is_a?( Symbol )
303
304
 
304
305
  # Relies on Ruby evaluation behaviour and operator precedence - "'foo'
305
306
  # && true" => true, but "true && 'foo'" => 'foo'. Don't use "and" here!
@@ -324,6 +325,7 @@ module Hoodoo
324
325
  # subset date, else +false+.
325
326
  #
326
327
  def self.valid_iso8601_subset_date?( str )
328
+ return false unless str.is_a?( String ) || str.is_a?( Symbol )
327
329
 
328
330
  # Same reliance as 'valid_iso8601_subset_datetime'?.
329
331
 
@@ -12,6 +12,6 @@ module Hoodoo
12
12
  # The Hoodoo gem version. If this changes, ensure that the date in
13
13
  # "hoodoo.gemspec" is correct and run "bundle install" (or "update").
14
14
  #
15
- VERSION = '1.12.4'
15
+ VERSION = '1.13.0'
16
16
 
17
17
  end
@@ -108,4 +108,33 @@ describe Hoodoo::Presenters::BaseDSL do
108
108
  }.not_to raise_error
109
109
  end
110
110
  end
111
+
112
+ describe '#type_option_to_class' do
113
+ it 'should raise an error for unrecognised types' do
114
+ expect {
115
+ klass = Hoodoo::Presenters::Object.new
116
+ klass.send( :type_option_to_class, :foo )
117
+ }.to raise_error(RuntimeError, "Unsupported 'type' option value of 'foo' in Hoodoo::Presenters::BaseDSL")
118
+ end
119
+ end
120
+
121
+ describe '#extract_field_prefix_options_from' do
122
+ it 'should extract options' do
123
+ data = {
124
+ :default => :foo,
125
+ :field_one => :one,
126
+ 'field_two' => :two,
127
+ 'field_three-three three' => { :three => 3 }
128
+ }
129
+
130
+ klass = Hoodoo::Presenters::Object.new
131
+ result = klass.send( :extract_field_prefix_options_from, data )
132
+
133
+ expect( result ).to( eql( {
134
+ :one => :one,
135
+ :two => :two,
136
+ :'three-three three' => { :three => 3 }
137
+ } ) )
138
+ end
139
+ end
111
140
  end
@@ -29,6 +29,8 @@ describe Hoodoo::Presenters::Array do
29
29
  end
30
30
  end
31
31
 
32
+ ############################################################################
33
+
32
34
  describe '#validate' do
33
35
  it 'should return [] when valid array' do
34
36
  expect(@inst.validate([]).errors).to eq([])
@@ -37,12 +39,12 @@ describe Hoodoo::Presenters::Array do
37
39
  it 'should return correct error when data is not a array' do
38
40
  errors = @inst.validate('asckn')
39
41
 
40
- err = [ {'code'=>"generic.invalid_array", 'message'=>"Field `one` is an invalid array", 'reference'=>"one"}]
42
+ err = [ {'code'=>'generic.invalid_array', 'message'=>'Field `one` is an invalid array', 'reference'=>'one'}]
41
43
  expect(errors.errors).to eq(err)
42
44
  end
43
45
 
44
46
  it 'should return correct error with non array types' do
45
- err = [ {'code'=>"generic.invalid_array", 'message'=>"Field `one` is an invalid array", 'reference'=>"one"}]
47
+ err = [ {'code'=>'generic.invalid_array', 'message'=>'Field `one` is an invalid array', 'reference'=>'one'}]
46
48
 
47
49
  expect(@inst.validate('asckn').errors).to eq(err)
48
50
  expect(@inst.validate(34534).errors).to eq(err)
@@ -58,14 +60,14 @@ describe Hoodoo::Presenters::Array do
58
60
  it 'should return error when required and absent' do
59
61
  @inst.required = true
60
62
  expect(@inst.validate(nil).errors).to eq([
61
- {'code'=>"generic.required_field_missing", 'message'=>"Field `one` is required", 'reference'=>"one"}
63
+ {'code'=>'generic.required_field_missing', 'message'=>'Field `one` is required', 'reference'=>'one'}
62
64
  ])
63
65
  end
64
66
 
65
67
  it 'should return correct error with path' do
66
68
  errors = @inst.validate('scdacs','ordinary')
67
69
  expect(errors.errors).to eq([
68
- {'code'=>"generic.invalid_array", 'message'=>"Field `ordinary.one` is an invalid array", 'reference'=>"ordinary.one"}
70
+ {'code'=>'generic.invalid_array', 'message'=>'Field `ordinary.one` is an invalid array', 'reference'=>'ordinary.one'}
69
71
  ])
70
72
  end
71
73
 
@@ -75,7 +77,7 @@ describe Hoodoo::Presenters::Array do
75
77
 
76
78
  errors = TestPresenterArray.validate(data)
77
79
  expect(errors.errors).to eq([
78
- {'code'=>"generic.required_field_missing", 'message'=>"Field `an_array` is required", 'reference'=>"an_array"},
80
+ {'code'=>'generic.required_field_missing', 'message'=>'Field `an_array` is required', 'reference'=>'an_array'},
79
81
  ])
80
82
 
81
83
  end
@@ -104,8 +106,8 @@ describe Hoodoo::Presenters::Array do
104
106
 
105
107
  errors = TestPresenterArray.validate(data)
106
108
  expect(errors.errors).to eq([
107
- {'code'=>"generic.invalid_integer", 'message'=>"Field `an_array[1].an_integer` is an invalid integer", 'reference'=>"an_array[1].an_integer"},
108
- {'code'=>"generic.invalid_datetime", 'message'=>"Field `an_array[2].a_datetime` is an invalid ISO8601 datetime", 'reference'=>"an_array[2].a_datetime"},
109
+ {'code'=>'generic.invalid_integer', 'message'=>'Field `an_array[1].an_integer` is an invalid integer', 'reference'=>'an_array[1].an_integer'},
110
+ {'code'=>'generic.invalid_datetime', 'message'=>'Field `an_array[2].a_datetime` is an invalid ISO8601 datetime', 'reference'=>'an_array[2].a_datetime'},
109
111
  ])
110
112
  end
111
113
 
@@ -125,6 +127,8 @@ describe Hoodoo::Presenters::Array do
125
127
  end
126
128
  end
127
129
 
130
+ ############################################################################
131
+
128
132
  describe '#render' do
129
133
  it 'renders correctly with whole-array default (1)' do
130
134
  data = nil
@@ -186,7 +190,7 @@ describe Hoodoo::Presenters::Array do
186
190
  }
187
191
  ],
188
192
  'a_default_array' => [ { 'an_integer' => 42 }, { 'some_array_text' => 'hello' } ],
189
- 'an_enum' => "one"
193
+ 'an_enum' => 'one'
190
194
  })
191
195
  end
192
196
 
@@ -218,7 +222,7 @@ describe Hoodoo::Presenters::Array do
218
222
  { 'an_integer' => 23 },
219
223
  { 'an_integer' => 42, 'a_datetime' => time },
220
224
  ],
221
- 'an_enum' => "one"
225
+ 'an_enum' => 'one'
222
226
  })
223
227
  end
224
228
 
@@ -245,5 +249,238 @@ describe Hoodoo::Presenters::Array do
245
249
  })
246
250
  end
247
251
  end
252
+ class TestPresenterTypedArray < Hoodoo::Presenters::Base
253
+ schema do
254
+ array :array, :type => :array
255
+ array :boolean, :type => :boolean
256
+ array :date, :type => :date
257
+ array :date_time, :type => :date_time
258
+ array :decimal, :type => :decimal, :field_precision => 2
259
+ array :enum, :type => :enum, :field_from => [ :one, :two, :three ]
260
+ array :float, :type => :float
261
+ array :integer, :type => :integer
262
+ array :string, :type => :string, :field_length => 4
263
+ array :tags, :type => :tags
264
+ array :text, :type => :text
265
+ array :uuid, :type => :uuid
266
+ array :field
267
+ end
268
+ end
269
+
270
+ ############################################################################
271
+
272
+ ARRAY_DATA = {
273
+ 'array' => { :valid => [ [ 2, 3, 4 ] ], :invalid => [ 4, { :one => 1 } ] },
274
+ 'boolean' => { :valid => [ true ], :invalid => [ 4.51, 'false' ] },
275
+ 'date' => { :valid => [ Date.today.iso8601 ], :invalid => [ Date.today, '23rd January 2041' ] },
276
+ 'date_time' => { :valid => [ DateTime.now.iso8601 ], :invalid => [ DateTime.now, '2017-01-27 12:00' ] },
277
+ 'decimal' => { :valid => [ BigDecimal.new(4.51, 2) ], :invalid => [ 4.51, '4.51' ] },
278
+ 'enum' => { :valid => [ 'one' ], :invalid => [ 'One', 1 ] },
279
+ 'float' => { :valid => [ 4.51 ], :invalid => [ BigDecimal.new(4.51, 2), '4.51' ] },
280
+ 'integer' => { :valid => [ 4 ], :invalid => [ '4' ] },
281
+ 'string' => { :valid => [ 'four' ], :invalid => [ 'toolong', 4, true ] },
282
+ 'tags' => { :valid => [ 'tag_a,tag_b,tag_c' ], :invalid => [ 4, true ] },
283
+ 'text' => { :valid => [ 'hello world' ], :invalid => [ 4, true ] },
284
+ 'uuid' => { :valid => [ Hoodoo::UUID.generate() ], :invalid => [ '123456', 4, true ] },
285
+ 'field' => { :valid => [ 4, '4', { :one => 1 } ], :invalid => [ ] }
286
+ }
287
+
288
+ ARRAY_DATA.each do | field, values |
289
+ context '#render' do
290
+ values[ :valid ].each_with_index do | value, index |
291
+ it "renders correctly for '#{ field }' (#{ index + 1 })" do
292
+ data = { field => [ value, value, value ] }
293
+ expect( TestPresenterTypedArray.render( data ) ).to eq( data )
294
+ end
295
+ end
296
+ end
297
+
298
+ context '#validate' do
299
+ values[ :valid ].each_with_index do | value, index |
300
+ it "accepts a valid value for '#{ field }' (#{ index + 1 })" do
301
+ data = { field => [ value, value, value ] }
302
+ expect( TestPresenterTypedArray.validate( data ).errors.size ).to eql( 0 )
303
+ end
304
+ end
305
+
306
+ values[ :invalid ].each_with_index do | value, index |
307
+ it "rejects an invalid value for '#{ field }' (#{ index + 1 })" do
308
+ data = { field => [ value, value, value ] }
309
+ expect( TestPresenterTypedArray.validate( data ).errors.size ).to eql( 3 )
310
+ end
311
+ end
312
+ end
313
+ end
314
+
315
+ ############################################################################
316
+
317
+ context 'RDoc examples' do
318
+ context 'VeryWealthy' do
319
+ class TestHypotheticalCurrency < Hoodoo::Presenters::Base
320
+ schema do
321
+ string :currency_code, :length => 16
322
+ integer :precision
323
+ end
324
+ end
325
+
326
+ class TestVeryWealthy < Hoodoo::Presenters::Base
327
+ schema do
328
+ array :currencies, :required => true do
329
+ type TestHypotheticalCurrency
330
+ string :notes, :required => true, :length => 32
331
+ end
332
+ end
333
+ end
334
+
335
+ let( :valid_data ) do
336
+ {
337
+ 'currencies' => [
338
+ {
339
+ 'currency_code' => 'X_HOODOO',
340
+ 'precision' => 2,
341
+ 'notes' => 'A short note'
342
+ }
343
+ ]
344
+ }
345
+ end
346
+
347
+ context '#validate' do
348
+ it 'enforces a required array' do
349
+ data = {}
350
+
351
+ errors = TestVeryWealthy.validate( data ).errors
352
+
353
+ expect( errors.size ).to( eql( 1 ) )
354
+ expect( errors[ 0 ][ 'code' ] ).to( eql( 'generic.required_field_missing' ) )
355
+ expect( errors[ 0 ][ 'reference' ] ).to( eql( 'currencies' ) )
356
+ end
357
+
358
+ it 'enforces a required array entry field' do
359
+ data = {
360
+ 'currencies' => [ {} ]
361
+ }
362
+
363
+ errors = TestVeryWealthy.validate( data ).errors
364
+
365
+ expect( errors.size ).to( eql( 1 ) )
366
+ expect( errors[ 0 ][ 'code' ] ).to( eql( 'generic.required_field_missing' ) )
367
+ expect( errors[ 0 ][ 'reference' ] ).to( eql( 'currencies[0].notes' ) )
368
+ end
369
+
370
+ it 'enforces an array entry field length' do
371
+ data = {
372
+ 'currencies' => [
373
+ {
374
+ 'notes' => 'This note is too long for the 32-character limit'
375
+ }
376
+ ]
377
+ }
378
+
379
+ errors = TestVeryWealthy.validate( data ).errors
380
+
381
+ expect( errors.size ).to( eql( 1 ) )
382
+ expect( errors[ 0 ][ 'code' ] ).to( eql( 'generic.invalid_string' ) )
383
+ expect( errors[ 0 ][ 'reference' ] ).to( eql( 'currencies[0].notes' ) )
384
+ end
385
+
386
+ it 'is happy with valid data' do
387
+ expect( TestVeryWealthy.validate( valid_data() ).errors.size ).to( eql( 0 ) )
388
+ end
389
+ end
390
+
391
+ context '#render' do
392
+ it 'renders valid data' do
393
+ expect( TestVeryWealthy.render( valid_data() ) ).to( eql( valid_data() ) )
394
+ end
395
+ end
396
+ end
397
+
398
+ context 'UUIDCollection' do
399
+ class TestUUIDCollection < Hoodoo::Presenters::Base
400
+ schema do
401
+ array :uuids, :type => :uuid
402
+ end
403
+ end
404
+
405
+ let( :valid_data ) do
406
+ {
407
+ 'uuids' => [
408
+ Hoodoo::UUID.generate(),
409
+ Hoodoo::UUID.generate(),
410
+ Hoodoo::UUID.generate()
411
+ ]
412
+ }
413
+ end
414
+
415
+ context '#validate' do
416
+ it 'validates entries' do
417
+ data = {
418
+ 'uuids' => [
419
+ Hoodoo::UUID.generate(),
420
+ 'not a UUID'
421
+ ]
422
+ }
423
+
424
+ errors = TestUUIDCollection.validate( data ).errors
425
+
426
+ expect( errors.size ).to( eql( 1 ) )
427
+ expect( errors[ 0 ][ 'code' ] ).to( eql( 'generic.invalid_uuid' ) )
428
+ expect( errors[ 0 ][ 'reference' ] ).to( eql( 'uuids[1]' ) )
429
+ end
430
+ end
431
+
432
+ context '#render' do
433
+ it 'renders valid data' do
434
+ expect( TestUUIDCollection.render( valid_data() ) ).to( eql( valid_data() ) )
435
+ end
436
+ end
437
+ end
438
+
439
+ context 'DecimalCollection' do
440
+ class TestDecimalCollection < Hoodoo::Presenters::Base
441
+ schema do
442
+ array :numbers, :type => :decimal, :field_precision => 2
443
+ end
444
+ end
445
+
446
+ let( :valid_data ) do
447
+ {
448
+ 'numbers' => [
449
+ BigDecimal.new( '42.55111' ), # Precision is FYI data generators, not the renderer :-/
450
+ BigDecimal.new( '42.4' ),
451
+ BigDecimal.new( '42' )
452
+ ]
453
+ }
454
+ end
455
+
456
+ context '#validate' do
457
+ it 'validates entries' do
458
+ data = {
459
+ 'numbers' => [
460
+ BigDecimal.new( '42.21' ),
461
+ 'not a decimal'
462
+ ]
463
+ }
464
+
465
+ errors = TestDecimalCollection.validate( data ).errors
466
+
467
+ expect( errors.size ).to( eql( 1 ) )
468
+ expect( errors[ 0 ][ 'code' ] ).to( eql( 'generic.invalid_decimal' ) )
469
+ expect( errors[ 0 ][ 'reference' ] ).to( eql( 'numbers[1]' ) )
470
+ end
471
+ end
472
+
473
+ context '#render' do
474
+ it 'renders valid data' do
475
+
476
+ # Precision is FYI data generators, not the renderer so high
477
+ # precision BigDecimals are returned as-is in rendering :-/
478
+ #
479
+ expect( TestDecimalCollection.render( valid_data() ) ).to( eql( valid_data() ) )
480
+
481
+ end
482
+ end
483
+ end
484
+ end
248
485
 
249
486
  end