attributor 5.1.0 → 5.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -3
  3. data/CHANGELOG.md +145 -135
  4. data/attributor.gemspec +5 -6
  5. data/lib/attributor.rb +17 -2
  6. data/lib/attributor/attribute.rb +39 -9
  7. data/lib/attributor/dsl_compiler.rb +17 -9
  8. data/lib/attributor/exceptions.rb +5 -0
  9. data/lib/attributor/extras/field_selector.rb +4 -0
  10. data/lib/attributor/families/numeric.rb +19 -6
  11. data/lib/attributor/families/temporal.rb +16 -9
  12. data/lib/attributor/hash_dsl_compiler.rb +6 -6
  13. data/lib/attributor/smart_attribute_selector.rb +149 -0
  14. data/lib/attributor/type.rb +27 -4
  15. data/lib/attributor/types/bigdecimal.rb +7 -2
  16. data/lib/attributor/types/boolean.rb +7 -2
  17. data/lib/attributor/types/class.rb +2 -2
  18. data/lib/attributor/types/collection.rb +22 -5
  19. data/lib/attributor/types/container.rb +3 -3
  20. data/lib/attributor/types/csv.rb +5 -1
  21. data/lib/attributor/types/date.rb +9 -3
  22. data/lib/attributor/types/date_time.rb +8 -2
  23. data/lib/attributor/types/float.rb +4 -3
  24. data/lib/attributor/types/hash.rb +105 -21
  25. data/lib/attributor/types/integer.rb +7 -1
  26. data/lib/attributor/types/model.rb +2 -2
  27. data/lib/attributor/types/object.rb +5 -0
  28. data/lib/attributor/types/polymorphic.rb +3 -2
  29. data/lib/attributor/types/string.rb +20 -1
  30. data/lib/attributor/types/struct.rb +1 -1
  31. data/lib/attributor/types/symbol.rb +5 -0
  32. data/lib/attributor/types/tempfile.rb +4 -0
  33. data/lib/attributor/types/time.rb +7 -3
  34. data/lib/attributor/types/uri.rb +9 -1
  35. data/lib/attributor/version.rb +1 -1
  36. data/spec/attribute_spec.rb +42 -7
  37. data/spec/dsl_compiler_spec.rb +16 -6
  38. data/spec/extras/field_selector/field_selector_spec.rb +9 -0
  39. data/spec/hash_dsl_compiler_spec.rb +2 -2
  40. data/spec/smart_attribute_selector_spec.rb +272 -0
  41. data/spec/support/integers.rb +7 -0
  42. data/spec/type_spec.rb +1 -1
  43. data/spec/types/bigdecimal_spec.rb +8 -0
  44. data/spec/types/boolean_spec.rb +10 -0
  45. data/spec/types/class_spec.rb +0 -1
  46. data/spec/types/collection_spec.rb +16 -0
  47. data/spec/types/date_spec.rb +9 -0
  48. data/spec/types/date_time_spec.rb +9 -0
  49. data/spec/types/float_spec.rb +8 -0
  50. data/spec/types/hash_spec.rb +181 -9
  51. data/spec/types/integer_spec.rb +10 -1
  52. data/spec/types/model_spec.rb +14 -3
  53. data/spec/types/string_spec.rb +10 -0
  54. data/spec/types/temporal_spec.rb +5 -1
  55. data/spec/types/time_spec.rb +9 -0
  56. data/spec/types/uri_spec.rb +9 -0
  57. metadata +24 -34
@@ -0,0 +1,7 @@
1
+ class PositiveIntegerType < Attributor::Integer
2
+
3
+ def self.options
4
+ { min: 0 }
5
+ end
6
+
7
+ end
@@ -68,7 +68,7 @@ describe Attributor::Type do
68
68
  let(:context) { %w(top sub) }
69
69
 
70
70
  it 'raises an exception' do
71
- expect { test_type.load(value, context) }.to raise_error(Attributor::IncompatibleTypeError, /cannot load values of type Fixnum.*while loading top.sub/)
71
+ expect { test_type.load(value, context) }.to raise_error(Attributor::IncompatibleTypeError, /cannot load values of type (Fixnum|Integer).*while loading top.sub/)
72
72
  end
73
73
  end
74
74
  end
@@ -45,4 +45,12 @@ describe Attributor::BigDecimal do
45
45
  end
46
46
  end
47
47
  end
48
+ context '.as_json_schema' do
49
+ subject(:js){ type.as_json_schema }
50
+ it 'adds the right attributes' do
51
+ expect(js.keys).to include(:type, :'x-type_name')
52
+ expect(js[:type]).to eq(:number)
53
+ expect(js[:'x-type_name']).to eq('BigDecimal')
54
+ end
55
+ end
48
56
  end
@@ -7,6 +7,8 @@ describe Attributor::Boolean do
7
7
  expect(type.new.is_a?(Attributor::Dumpable)).not_to be(true)
8
8
  end
9
9
 
10
+ its(:json_schema_type){ should eq(:boolean)}
11
+
10
12
  context '.valid_type?' do
11
13
  context 'for incoming Boolean values' do
12
14
  [false, true].each do |value|
@@ -63,4 +65,12 @@ describe Attributor::Boolean do
63
65
  end
64
66
  end
65
67
  end
68
+ context '.as_json_schema' do
69
+ subject(:js){ type.as_json_schema }
70
+ it 'adds the right attributes' do
71
+ expect(js.keys).to include(:type, :'x-type_name')
72
+ expect(js[:type]).to eq(:boolean)
73
+ expect(js[:'x-type_name']).to eq('Boolean')
74
+ end
75
+ end
66
76
  end
@@ -1,5 +1,4 @@
1
1
  require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
2
- require 'backports'
3
2
 
4
3
  describe Attributor::Class do
5
4
  subject(:type) { Attributor::Class }
@@ -344,4 +344,20 @@ describe Attributor::Collection do
344
344
  end.to_not raise_error
345
345
  end
346
346
  end
347
+
348
+ context '.as_json_schema' do
349
+ let(:member_type) { Attributor::String }
350
+ let(:type) { Attributor::Collection.of(member_type) }
351
+ let(:attribute_options) do
352
+ {}
353
+ end
354
+ subject(:js){ type.as_json_schema(attribute_options: attribute_options) }
355
+
356
+ it 'adds the right attributes' do
357
+ expect(js.keys).to include(:type, :'x-type_name', :items)
358
+ expect(js[:type]).to eq(:array)
359
+ expect(js[:'x-type_name']).to eq('Collection')
360
+ expect(js[:items]).to eq(member_type.as_json_schema)
361
+ end
362
+ end
347
363
  end
@@ -92,4 +92,13 @@ describe Attributor::Date do
92
92
  end
93
93
  end
94
94
  end
95
+ context '.as_json_schema' do
96
+ subject(:js){ type.as_json_schema }
97
+ it 'adds the right attributes' do
98
+ expect(js.keys).to include(:type, :'x-type_name')
99
+ expect(js[:type]).to eq(:string)
100
+ expect(js[:format]).to eq(:'date')
101
+ expect(js[:'x-type_name']).to eq('Date')
102
+ end
103
+ end
95
104
  end
@@ -92,4 +92,13 @@ describe Attributor::DateTime do
92
92
  end
93
93
  end
94
94
  end
95
+ context '.as_json_schema' do
96
+ subject(:js){ type.as_json_schema }
97
+ it 'adds the right attributes' do
98
+ expect(js.keys).to include(:type, :'x-type_name', :format)
99
+ expect(js[:type]).to eq(:string)
100
+ expect(js[:format]).to eq(:'date-time')
101
+ expect(js[:'x-type_name']).to eq('DateTime')
102
+ end
103
+ end
95
104
  end
@@ -76,4 +76,12 @@ describe Attributor::Float do
76
76
  end
77
77
  end
78
78
  end
79
+ context '.as_json_schema' do
80
+ subject(:js){ type.as_json_schema }
81
+ it 'adds the right attributes' do
82
+ expect(js.keys).to include(:type, :'x-type_name')
83
+ expect(js[:type]).to eq(:number)
84
+ expect(js[:'x-type_name']).to eq('Float')
85
+ end
86
+ end
79
87
  end
@@ -7,6 +7,7 @@ describe Attributor::Hash do
7
7
  its(:key_type) { should be(Attributor::Object) }
8
8
  its(:value_type) { should be(Attributor::Object) }
9
9
  its(:dsl_class) { should be(Attributor::HashDSLCompiler) }
10
+ its(:json_schema_type) { should be(:object) }
10
11
 
11
12
  context 'attributes' do
12
13
  context 'with an exception from the definition block' do
@@ -359,7 +360,7 @@ describe Attributor::Hash do
359
360
  context 'with unknown keys in input' do
360
361
  it 'raises an error' do
361
362
  expect do
362
- type.load('other_key' => :value)
363
+ type.load({'other_key' => :value})
363
364
  end.to raise_error(Attributor::AttributorException)
364
365
  end
365
366
  end
@@ -453,7 +454,7 @@ describe Attributor::Hash do
453
454
 
454
455
  context 'for a simple (untyped) hash' do
455
456
  it 'returns the untouched hash value' do
456
- expect(type.dump(value, opts)).to eq(value)
457
+ expect(type.dump(value, **opts)).to eq(value)
457
458
  end
458
459
  end
459
460
 
@@ -475,7 +476,7 @@ describe Attributor::Hash do
475
476
  let(:type) { Attributor::Hash.of(key: String, value: subtype) }
476
477
 
477
478
  it 'returns a hash with the dumped values and keys' do
478
- dumped_value = type.dump(value, opts)
479
+ dumped_value = type.dump(value, **opts)
479
480
  expect(dumped_value).to be_kind_of(::Hash)
480
481
  expect(dumped_value.keys).to match_array %w(id1 id2)
481
482
  expect(dumped_value.values).to have(2).items
@@ -487,7 +488,7 @@ describe Attributor::Hash do
487
488
  let(:value) { { id1: nil, id2: subtype.new(value2) } }
488
489
 
489
490
  it 'correctly returns nil rather than trying to dump their contents' do
490
- dumped_value = type.dump(value, opts)
491
+ dumped_value = type.dump(value, **opts)
491
492
  expect(dumped_value).to be_kind_of(::Hash)
492
493
  expect(dumped_value.keys).to match_array %w(id1 id2)
493
494
  expect(dumped_value['id1']).to be nil
@@ -497,6 +498,22 @@ describe Attributor::Hash do
497
498
  end
498
499
  end
499
500
 
501
+ context '.requirements' do
502
+ let(:type) { Attributor::Hash.construct(block) }
503
+
504
+ context 'forces processing of lazy key initialization' do
505
+ let(:block) do
506
+ proc do
507
+ key 'name', String
508
+ requires 'name'
509
+ end
510
+ end
511
+
512
+ it 'lists the requirements' do
513
+ expect(type.requirements).to_not be_empty
514
+ end
515
+ end
516
+ end
500
517
  context '#validate' do
501
518
  context 'for a key and value typed hash' do
502
519
  let(:key_type) { Integer }
@@ -651,7 +668,7 @@ describe Attributor::Hash do
651
668
  at_least(1).of 'consistency', 'availability', 'partitioning'
652
669
  end
653
670
  # Silly example, just to show that block and inline requires can be combined
654
- requires.at_most(3).of 'consistency', 'availability', 'partitioning'
671
+ requires.at_most(1).of 'consistency', 'availability', 'partitioning'
655
672
  end
656
673
  end
657
674
 
@@ -663,6 +680,37 @@ describe Attributor::Hash do
663
680
  )
664
681
  end
665
682
  end
683
+ context 'using a combo of things to test example gen' do
684
+ let(:block) do
685
+ proc do
686
+ key :req1, String
687
+ key :req2, String
688
+ key :exc3, String
689
+ key :exc4, String
690
+ key :least1, String
691
+ key :least2, String
692
+ key :exact1, String
693
+ key :exact2, String
694
+ key :most1, String
695
+ key :most2, String
696
+
697
+ requires.all :req1, :req2
698
+ requires.exclusive :exc3, :exc4
699
+ requires.at_least(2).of :least1, :least2
700
+ requires.exactly(1).of :exc3, :exact1, :exact2
701
+ requires.at_most(1).of :most1, :most2
702
+ requires.at_least(1).of :exc4, :exc3
703
+ end
704
+ end
705
+ it 'comes up with a reasonably good set' do
706
+ ex = type.example
707
+ expect(ex.keys).to match([:req1, :req2, :exc3, :least1, :least2, :most1])
708
+ end
709
+ it 'it favors picking attributes with data' do
710
+ ex = type.example(nil,most2: "data")
711
+ expect(ex.keys).to match([:req1, :req2, :exc3, :least1, :least2, :most2])
712
+ end
713
+ end
666
714
  end
667
715
  end
668
716
 
@@ -687,6 +735,13 @@ describe Attributor::Hash do
687
735
  expect(description[:name]).to eq('Hash')
688
736
  expect(description[:key]).to eq(type: { name: 'Object', id: 'Attributor-Object', family: 'any' })
689
737
  expect(description[:value]).to eq(type: { name: 'Object', id: 'Attributor-Object', family: 'any' })
738
+ expect(description).to_not have_key(:example)
739
+ end
740
+ context 'when there is a given example' do
741
+ let(:example) { { 'one' => 1, two: 2 } }
742
+ it 'uses it, even though there are not individual keys' do
743
+ expect(description[:example]).to eq(example)
744
+ end
690
745
  end
691
746
  end
692
747
 
@@ -1096,10 +1151,10 @@ describe Attributor::Hash do
1096
1151
  let(:hash_of_strings) { Attributor::Hash.of(key: String) }
1097
1152
  let(:hash_of_symbols) { Attributor::Hash.of(key: Symbol) }
1098
1153
 
1099
- let(:merger) { hash_of_strings.load('a' => 1) }
1100
- let(:good_mergee) { hash_of_strings.load('b' => 2) }
1101
- let(:bad_mergee) { hash_of_symbols.load(c: 3) }
1102
- let(:result) { hash_of_strings.load('a' => 1, 'b' => 2) }
1154
+ let(:merger) { hash_of_strings.load({'a' => 1},nil) }
1155
+ let(:good_mergee) { hash_of_strings.load({'b' => 2},nil) }
1156
+ let(:bad_mergee) { hash_of_symbols.load({c: 3}) }
1157
+ let(:result) { hash_of_strings.load({'a' => 1, 'b' => 2},nil) }
1103
1158
 
1104
1159
  it 'validates that the mergee is of like type' do
1105
1160
  expect { merger.merge(bad_mergee) }.to raise_error(ArgumentError)
@@ -1118,4 +1173,121 @@ describe Attributor::Hash do
1118
1173
 
1119
1174
  context Attributor::InvalidDefinition do
1120
1175
  end
1176
+
1177
+
1178
+ context '.as_json_hash' do
1179
+ let(:example){ nil }
1180
+ subject(:description) { type.as_json_schema(example: example) }
1181
+ its([:type]){ should eq(:object)}
1182
+ its([:'x-type_name']){ should eq('Hash')}
1183
+
1184
+ context 'for hashes with explicit key and value types' do
1185
+ let(:key_type){ String }
1186
+ let(:value_type){ Integer }
1187
+
1188
+ subject(:type) { Attributor::Hash.of(key: key_type, value: value_type) }
1189
+
1190
+ it 'describes the key type correctly' do
1191
+ expect(description.keys).to include( :'x-key_type' )
1192
+ expect(description[:'x-key_type']).to be_kind_of(::Hash)
1193
+ expect(description[:'x-key_type'][:type]).to eq( :string )
1194
+ end
1195
+
1196
+ it 'describes the value type correctly' do
1197
+ expect(description.keys).to include( :'x-value_type' )
1198
+ expect(description[:'x-value_type']).to be_kind_of(::Hash)
1199
+ expect(description[:'x-value_type'][:type]).to eq( :integer )
1200
+ end
1201
+
1202
+ end
1203
+
1204
+
1205
+ context 'for hashes with specific keys defined' do
1206
+ let(:block) do
1207
+ proc do
1208
+ key 'a string', String
1209
+ key '1', Integer, min: 1, max: 20
1210
+ key 'some_date', DateTime
1211
+ key 'defaulted', String, default: 'default value'
1212
+ requires do
1213
+ all.of '1','some_date'
1214
+ exclusive 'some_date', 'defaulted'
1215
+ at_least(1).of 'a string', 'some_date'
1216
+ at_most(2).of 'a string', 'some_date'
1217
+ exactly(1).of 'a string', 'some_date'
1218
+ end
1219
+ end
1220
+ end
1221
+
1222
+ let(:type) { Attributor::Hash.of(key: String).construct(block) }
1223
+
1224
+ it 'describes the basic type options correctly' do
1225
+ expect(description[:type]).to eq(:object)
1226
+ expect(description[:'x-key_type']).to eq( type: :string , 'x-type_name': 'String')
1227
+ expect(description).to_not have_key(:'x-value_type')
1228
+ end
1229
+
1230
+ it 'describes the type attributes correctly' do
1231
+ props = description[:properties]
1232
+
1233
+ expect(props['a string']).to eq(type: :string, 'x-type_name': 'String')
1234
+ expect(props['1']).to eq(type: :integer, 'x-type_name': 'Integer', minimum: 1, maximum: 20)
1235
+ expect(props['some_date']).to eq(type: :string, 'x-type_name': 'DateTime', format: :'date-time')
1236
+ expect(props['defaulted']).to eq(type: :string, 'x-type_name': 'String', default: 'default value')
1237
+ end
1238
+
1239
+ it 'describes the attribute requirements correctly' do
1240
+ reqs = description[:required]
1241
+ expect(reqs).to be_kind_of(Array)
1242
+ expect(reqs).to eq( ['1','some_date'] )
1243
+ end
1244
+
1245
+ it 'describes the extended requirements correctly' do
1246
+ reqs = description[:'x-requirements']
1247
+ expect(reqs).to be_kind_of(Array)
1248
+ expect(reqs.size).to be(5)
1249
+ expect(reqs).to include( type: :all, attributes: ['1','some_date'] )
1250
+ expect(reqs).to include( type: :exclusive, attributes: ['some_date','defaulted'] )
1251
+ expect(reqs).to include( type: :at_least, attributes: ['a string','some_date'], count: 1 )
1252
+ expect(reqs).to include( type: :at_most, attributes: ['a string','some_date'], count: 2 )
1253
+ expect(reqs).to include( type: :exactly, attributes: ['a string','some_date'], count: 1 )
1254
+ end
1255
+
1256
+ context 'merging requires.all with attribute required: true' do
1257
+ let(:block) do
1258
+ proc do
1259
+ key 'required string', String, required: true
1260
+ key '1', Integer
1261
+ key 'some_date', DateTime
1262
+ requires do
1263
+ all.of 'some_date'
1264
+ end
1265
+ end
1266
+ end
1267
+ it 'includes attributes with required: true into :required' do
1268
+ expect(description[:required].size).to eq(2)
1269
+ expect(description[:required]).to include( 'required string','some_date' )
1270
+ end
1271
+
1272
+ it 'includes attributes with required: true into the :all requirements' do
1273
+ req_all = description[:'x-requirements'].select{|r| r[:type] == :all}.first
1274
+ expect(req_all[:attributes]).to include( 'required string','some_date' )
1275
+ end
1276
+ end
1277
+
1278
+
1279
+ context 'with an example' do
1280
+ let(:example){ type.example }
1281
+
1282
+ it 'should have the matching example for each leaf key' do
1283
+ expect(description[:properties].keys).to include(*type.keys.keys)
1284
+ description[:properties].each do |name,sub_description|
1285
+ expect(sub_description).to have_key(:example)
1286
+ val = type.attributes[name].dump(example[name])
1287
+ expect(sub_description[:example]).to eq val
1288
+ end
1289
+ end
1290
+ end
1291
+ end
1292
+ end
1121
1293
  end
@@ -97,7 +97,7 @@ describe Attributor::Integer do
97
97
  it "raises for the invalid range [#{min.inspect}, #{max.inspect}]" do
98
98
  opts = { options: { max: max, min: min } }
99
99
  expect do
100
- type.example(nil, opts)
100
+ type.example(nil, **opts)
101
101
  end.to raise_error(Attributor::AttributorException, "Invalid range: [#{min.inspect}, #{max.inspect}]")
102
102
  end
103
103
  end
@@ -146,4 +146,13 @@ describe Attributor::Integer do
146
146
  end
147
147
  end
148
148
  end
149
+
150
+ context '.as_json_schema' do
151
+ subject(:js){ type.as_json_schema }
152
+ it 'adds the right stuff' do
153
+ expect(js.keys).to include(:type, :'x-type_name')
154
+ expect(js[:type]).to eq(:integer)
155
+ expect(js[:'x-type_name']).to eq('Integer')
156
+ end
157
+ end
149
158
  end
@@ -369,7 +369,7 @@ describe Attributor::Model do
369
369
  end
370
370
 
371
371
  context 'for models using the "requires" DSL' do
372
- subject(:address) { Address.load(state: 'CA') }
372
+ subject(:address) { Address.load({state: 'CA'}) }
373
373
  its(:validate) { should_not be_empty }
374
374
  its(:validate) { should include 'Key name is required for $.' }
375
375
  end
@@ -380,8 +380,8 @@ describe Attributor::Model do
380
380
  end
381
381
 
382
382
  context 'that are both invalid' do
383
- subject(:person) { Person.load(name: 'Joe', title: 'dude', okay: true) }
384
- let(:address) { Address.load(name: '1 Main St', state: 'ME') }
383
+ subject(:person) { Person.load({name: 'Joe', title: 'dude', okay: true}) }
384
+ let(:address) { Address.load({name: '1 Main St', state: 'ME'}) }
385
385
  before do
386
386
  person.address = address
387
387
  address.person = person
@@ -413,6 +413,17 @@ describe Attributor::Model do
413
413
  expect(person.address.person).to be(person)
414
414
  expect(output[:address][:person]).to eq(Attributor::Model::CIRCULAR_REFERENCE_MARKER)
415
415
  end
416
+
417
+ it 'passes kwargs' do
418
+ person.class.attributes.values.each do |attr|
419
+ expect(attr).to receive(:dump).with(
420
+ anything,
421
+ context: anything,
422
+ custom_arg: :custom_value
423
+ )
424
+ end
425
+ person.dump(custom_arg: :custom_value)
426
+ end
416
427
  end
417
428
  end
418
429
 
@@ -64,4 +64,14 @@ describe Attributor::String do
64
64
  end.to raise_error(Attributor::IncompatibleTypeError)
65
65
  end
66
66
  end
67
+
68
+ context '.as_json_schema' do
69
+ subject(:js){ type.as_json_schema(attribute_options: { regexp: /^Foobar$/ }) }
70
+ it 'adds the right attributes' do
71
+ expect(js.keys).to include(:type, :'x-type_name')
72
+ expect(js[:type]).to eq(:string)
73
+ expect(js[:'x-type_name']).to eq('String')
74
+ expect(js[:pattern]).to eq('^Foobar$')
75
+ end
76
+ end
67
77
  end