attributor 5.2.0 → 5.6

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 (53) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +4 -3
  3. data/CHANGELOG.md +146 -140
  4. data/attributor.gemspec +4 -5
  5. data/lib/attributor.rb +16 -2
  6. data/lib/attributor/attribute.rb +42 -9
  7. data/lib/attributor/extras/field_selector.rb +4 -0
  8. data/lib/attributor/families/numeric.rb +19 -6
  9. data/lib/attributor/families/temporal.rb +16 -9
  10. data/lib/attributor/hash_dsl_compiler.rb +6 -6
  11. data/lib/attributor/type.rb +27 -4
  12. data/lib/attributor/types/bigdecimal.rb +7 -2
  13. data/lib/attributor/types/boolean.rb +7 -2
  14. data/lib/attributor/types/class.rb +2 -2
  15. data/lib/attributor/types/collection.rb +24 -5
  16. data/lib/attributor/types/container.rb +3 -3
  17. data/lib/attributor/types/csv.rb +5 -1
  18. data/lib/attributor/types/date.rb +9 -3
  19. data/lib/attributor/types/date_time.rb +8 -2
  20. data/lib/attributor/types/float.rb +4 -3
  21. data/lib/attributor/types/hash.rb +82 -18
  22. data/lib/attributor/types/integer.rb +7 -1
  23. data/lib/attributor/types/model.rb +2 -2
  24. data/lib/attributor/types/object.rb +5 -0
  25. data/lib/attributor/types/polymorphic.rb +3 -2
  26. data/lib/attributor/types/string.rb +20 -1
  27. data/lib/attributor/types/struct.rb +1 -1
  28. data/lib/attributor/types/symbol.rb +5 -0
  29. data/lib/attributor/types/tempfile.rb +4 -0
  30. data/lib/attributor/types/time.rb +7 -3
  31. data/lib/attributor/types/uri.rb +9 -1
  32. data/lib/attributor/version.rb +1 -1
  33. data/spec/attribute_spec.rb +42 -7
  34. data/spec/dsl_compiler_spec.rb +7 -7
  35. data/spec/extras/field_selector/field_selector_spec.rb +9 -0
  36. data/spec/hash_dsl_compiler_spec.rb +2 -2
  37. data/spec/support/integers.rb +7 -0
  38. data/spec/type_spec.rb +1 -1
  39. data/spec/types/bigdecimal_spec.rb +8 -0
  40. data/spec/types/boolean_spec.rb +10 -0
  41. data/spec/types/class_spec.rb +0 -1
  42. data/spec/types/collection_spec.rb +16 -0
  43. data/spec/types/date_spec.rb +9 -0
  44. data/spec/types/date_time_spec.rb +9 -0
  45. data/spec/types/float_spec.rb +8 -0
  46. data/spec/types/hash_spec.rb +127 -9
  47. data/spec/types/integer_spec.rb +10 -1
  48. data/spec/types/model_spec.rb +14 -3
  49. data/spec/types/string_spec.rb +10 -0
  50. data/spec/types/temporal_spec.rb +5 -1
  51. data/spec/types/time_spec.rb +9 -0
  52. data/spec/types/uri_spec.rb +9 -0
  53. metadata +21 -34
@@ -4,7 +4,7 @@ describe Attributor::DSLCompiler do
4
4
  let(:target) { double('model', attributes: {}) }
5
5
 
6
6
  let(:dsl_compiler_options) { {} }
7
- subject(:dsl_compiler) { Attributor::DSLCompiler.new(target, dsl_compiler_options) }
7
+ subject(:dsl_compiler) { Attributor::DSLCompiler.new(target, **dsl_compiler_options) }
8
8
 
9
9
  let(:attribute_name) { :name }
10
10
  let(:type) { Attributor::String }
@@ -35,7 +35,7 @@ describe Attributor::DSLCompiler do
35
35
 
36
36
  it 'creates an attribute given a name, type, and options' do
37
37
  expect(Attributor::Attribute).to receive(:new).with(expected_type, expected_options)
38
- dsl_compiler.attribute(attribute_name, type, attribute_options)
38
+ dsl_compiler.attribute(attribute_name, type, **attribute_options)
39
39
  end
40
40
  end
41
41
 
@@ -60,11 +60,11 @@ describe Attributor::DSLCompiler do
60
60
  end
61
61
 
62
62
  it 'creates an attribute with the inherited type and merged options' do
63
- dsl_compiler.attribute(attribute_name, attribute_options)
63
+ dsl_compiler.attribute(attribute_name, **attribute_options)
64
64
  end
65
65
 
66
66
  it 'accepts explicit nil type' do
67
- dsl_compiler.attribute(attribute_name, nil, attribute_options)
67
+ dsl_compiler.attribute(attribute_name, nil, **attribute_options)
68
68
  end
69
69
 
70
70
  context 'but with the attribute also specifying a reference' do
@@ -73,7 +73,7 @@ describe Attributor::DSLCompiler do
73
73
  let(:expected_options) { attribute_options }
74
74
  it 'attribute reference takes precedence over the compiler one (and merges no options)' do
75
75
  expect(attribute_options[:reference]).to_not eq(dsl_compiler_options[:reference])
76
- dsl_compiler.attribute(attribute_name, attribute_options)
76
+ dsl_compiler.attribute(attribute_name, **attribute_options)
77
77
  end
78
78
  end
79
79
  end
@@ -113,12 +113,12 @@ describe Attributor::DSLCompiler do
113
113
  it 'sets the type of the attribute to Struct' do
114
114
  expect(Attributor::Attribute).to receive(:new)
115
115
  .with(expected_type, description: 'The turkey', reference: Turkey)
116
- dsl_compiler.attribute(attribute_name, attribute_options, &attribute_block)
116
+ dsl_compiler.attribute(attribute_name, **attribute_options, &attribute_block)
117
117
  end
118
118
 
119
119
  it 'passes the correct reference to the created attribute' do
120
120
  expect(Attributor::Attribute).to receive(:new).with(expected_type, expected_options)
121
- dsl_compiler.attribute(attribute_name, type, attribute_options, &attribute_block)
121
+ dsl_compiler.attribute(attribute_name, type, **attribute_options, &attribute_block)
122
122
  end
123
123
  end
124
124
  end
@@ -33,4 +33,13 @@ describe Attributor::FieldSelector do
33
33
  end
34
34
  end
35
35
  end
36
+
37
+ context '.as_json_schema' do
38
+ subject(:js){ type.as_json_schema }
39
+ it 'adds the right attributes' do
40
+ expect(js.keys).to include(:type, :'x-type_name')
41
+ expect(js[:type]).to eq(:string)
42
+ expect(js[:'x-type_name']).to eq('FieldSelector')
43
+ end
44
+ end
36
45
  end
@@ -4,7 +4,7 @@ describe Attributor::HashDSLCompiler do
4
4
  let(:target) { double('model', attributes: {}) }
5
5
 
6
6
  let(:dsl_compiler_options) { {} }
7
- subject(:dsl_compiler) { Attributor::HashDSLCompiler.new(target, dsl_compiler_options) }
7
+ subject(:dsl_compiler) { Attributor::HashDSLCompiler.new(target, **dsl_compiler_options) }
8
8
 
9
9
  it 'returns the requirements DSL attached to the right target' do
10
10
  req_dsl = dsl_compiler._requirements_dsl
@@ -103,7 +103,7 @@ describe Attributor::HashDSLCompiler do
103
103
  end
104
104
 
105
105
  context 'Requirement#validate' do
106
- let(:requirement) { req_class.new(arguments) }
106
+ let(:requirement) { req_class.new(**arguments) }
107
107
  let(:subject) { requirement.validate(value, ['$'], nil) }
108
108
 
109
109
  context 'for :all' do
@@ -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
@@ -706,7 +707,7 @@ describe Attributor::Hash do
706
707
  expect(ex.keys).to match([:req1, :req2, :exc3, :least1, :least2, :most1])
707
708
  end
708
709
  it 'it favors picking attributes with data' do
709
- ex = type.example(nil,{most2: "data"})
710
+ ex = type.example(nil,most2: "data")
710
711
  expect(ex.keys).to match([:req1, :req2, :exc3, :least1, :least2, :most2])
711
712
  end
712
713
  end
@@ -1150,10 +1151,10 @@ describe Attributor::Hash do
1150
1151
  let(:hash_of_strings) { Attributor::Hash.of(key: String) }
1151
1152
  let(:hash_of_symbols) { Attributor::Hash.of(key: Symbol) }
1152
1153
 
1153
- let(:merger) { hash_of_strings.load('a' => 1) }
1154
- let(:good_mergee) { hash_of_strings.load('b' => 2) }
1155
- let(:bad_mergee) { hash_of_symbols.load(c: 3) }
1156
- 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) }
1157
1158
 
1158
1159
  it 'validates that the mergee is of like type' do
1159
1160
  expect { merger.merge(bad_mergee) }.to raise_error(ArgumentError)
@@ -1172,4 +1173,121 @@ describe Attributor::Hash do
1172
1173
 
1173
1174
  context Attributor::InvalidDefinition do
1174
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
1175
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