attributor 5.4 → 6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/lib/attributor/attribute.rb +101 -84
  4. data/lib/attributor/extras/field_selector.rb +4 -0
  5. data/lib/attributor/families/numeric.rb +19 -6
  6. data/lib/attributor/families/temporal.rb +16 -9
  7. data/lib/attributor/hash_dsl_compiler.rb +6 -5
  8. data/lib/attributor/type.rb +26 -3
  9. data/lib/attributor/types/bigdecimal.rb +6 -1
  10. data/lib/attributor/types/boolean.rb +5 -0
  11. data/lib/attributor/types/collection.rb +19 -0
  12. data/lib/attributor/types/csv.rb +4 -0
  13. data/lib/attributor/types/date.rb +7 -1
  14. data/lib/attributor/types/date_time.rb +7 -1
  15. data/lib/attributor/types/float.rb +4 -3
  16. data/lib/attributor/types/hash.rb +86 -23
  17. data/lib/attributor/types/integer.rb +7 -1
  18. data/lib/attributor/types/model.rb +9 -21
  19. data/lib/attributor/types/object.rb +5 -0
  20. data/lib/attributor/types/polymorphic.rb +0 -1
  21. data/lib/attributor/types/string.rb +19 -0
  22. data/lib/attributor/types/symbol.rb +5 -0
  23. data/lib/attributor/types/tempfile.rb +4 -0
  24. data/lib/attributor/types/time.rb +6 -2
  25. data/lib/attributor/types/uri.rb +8 -0
  26. data/lib/attributor/version.rb +1 -1
  27. data/lib/attributor.rb +3 -7
  28. data/spec/attribute_spec.rb +148 -124
  29. data/spec/extras/field_selector/field_selector_spec.rb +9 -0
  30. data/spec/hash_dsl_compiler_spec.rb +5 -5
  31. data/spec/spec_helper.rb +0 -2
  32. data/spec/support/integers.rb +7 -0
  33. data/spec/support/models.rb +7 -7
  34. data/spec/types/bigdecimal_spec.rb +8 -0
  35. data/spec/types/boolean_spec.rb +10 -0
  36. data/spec/types/collection_spec.rb +16 -0
  37. data/spec/types/date_spec.rb +9 -0
  38. data/spec/types/date_time_spec.rb +9 -0
  39. data/spec/types/float_spec.rb +8 -0
  40. data/spec/types/hash_spec.rb +181 -22
  41. data/spec/types/integer_spec.rb +9 -0
  42. data/spec/types/model_spec.rb +7 -1
  43. data/spec/types/string_spec.rb +10 -0
  44. data/spec/types/temporal_spec.rb +5 -1
  45. data/spec/types/time_spec.rb +9 -0
  46. data/spec/types/uri_spec.rb +9 -0
  47. metadata +5 -6
  48. data/lib/attributor/attribute_resolver.rb +0 -111
  49. data/spec/attribute_resolver_spec.rb +0 -237
@@ -11,7 +11,7 @@ describe Attributor::Attribute do
11
11
 
12
12
  context 'initialize' do
13
13
  its(:type) { should be type }
14
- its(:options) { should be attribute_options }
14
+ its(:options) { should eq attribute_options }
15
15
 
16
16
  it 'calls check_options!' do
17
17
  expect_any_instance_of(Attributor::Attribute).to receive(:check_options!)
@@ -36,17 +36,49 @@ describe Attributor::Attribute do
36
36
  it { should eq other_attribute }
37
37
  end
38
38
 
39
+ context 'describe_json_schema' do
40
+ let(:type) { PositiveIntegerType }
41
+
42
+ let(:attribute_options) do
43
+ {
44
+ values: [1,20],
45
+ description: "something",
46
+ example: 20,
47
+ max: 1000,
48
+ default: 1
49
+ }
50
+ end
51
+
52
+ context 'reports all of the possible attributes' do
53
+ let(:js){ subject.as_json_schema(example: 20) }
54
+
55
+ it 'including the attribute-specific ones' do
56
+ expect(js[:enum]).to eq( [1,20])
57
+ expect(js[:description]).to eq( "something")
58
+ expect(js[:default]).to eq(1)
59
+ expect(js[:example]).to eq(20)
60
+ end
61
+
62
+ it 'as well as the type-specific ones' do
63
+ expect(js[:type]).to eq(:integer)
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
39
70
  context 'describe' do
40
- let(:attribute_options) { { required: true, values: ['one'], description: 'something', min: 0 } }
71
+ let(:attribute_options) { {required: true, values: ['one'], description: "something", min: 0} }
41
72
  let(:expected) do
42
- h = { type: { name: 'String', id: type.id, family: type.family } }
43
- common = attribute_options.select { |k, _v| Attributor::Attribute::TOP_LEVEL_OPTIONS.include? k }
73
+ h = {type: {name: 'String', id: type.id, family: type.family}}
74
+ common = attribute_options.select{|k,v| Attributor::Attribute::TOP_LEVEL_OPTIONS.include? k }
44
75
  h.merge!(common)
45
- h[:options] = { min: 0 }
76
+ h[:options] = {min: 0}
46
77
  h
47
78
  end
48
79
 
49
- its(:describe) { should eq expected }
80
+ # It has both the type-included options (min) as well as the attribute options (max)
81
+ its(:describe) { should == expected }
50
82
 
51
83
  context 'with example options' do
52
84
  let(:attribute_options) { { description: 'something', example: 'ex_def' } }
@@ -163,6 +195,49 @@ describe Attributor::Attribute do
163
195
  end.not_to raise_error
164
196
  end
165
197
  end
198
+
199
+ context 'custom_options' do
200
+ let(:option_name) { :foo }
201
+ let(:custom_option_args) { [option_name, String] }
202
+
203
+ around do |example|
204
+ Attributor::Attribute.custom_option *custom_option_args
205
+ example.run
206
+ Attributor::Attribute.custom_options.delete option_name
207
+ end
208
+
209
+ it 'raises ArgumentError if given an existing option' do
210
+ expect {
211
+ Attributor::Attribute.custom_option :default, Object
212
+ }.to raise_error(ArgumentError)
213
+ end
214
+
215
+ it 'accepts custom options' do
216
+ expect do
217
+ Attributor::Attribute.new(Integer, foo: 'unvalidated')
218
+ end.not_to raise_error
219
+ end
220
+
221
+ context 'can validate the custom option value' do
222
+ let(:custom_option_args) { [option_name, String, values: ['valid']] }
223
+ it 'does not raise with a valid option value' do
224
+ expect do
225
+ Attributor::Attribute.new(Integer, foo: 'valid')
226
+ end.not_to raise_error
227
+ end
228
+ it 'raises with an invalid option value' do
229
+ expect do
230
+ Attributor::Attribute.new(Integer, foo: 'invalid')
231
+ end.to raise_error(Attributor::AttributorException)
232
+ end
233
+ end
234
+
235
+ it 'appear in as_json_schema' do
236
+ attribute = Attributor::Attribute.new(Integer, foo: 'valid')
237
+ json_schema = attribute.as_json_schema
238
+ expect(json_schema[:'x-foo']).to eq 'valid'
239
+ end
240
+ end
166
241
  end
167
242
 
168
243
  context 'example' do
@@ -383,10 +458,49 @@ describe Attributor::Attribute do
383
458
  context 'applying attribute options' do
384
459
  context ':required' do
385
460
  let(:attribute_options) { { required: true } }
461
+ context 'has no effect on a bare attribute' do
462
+ let(:value) { 'val' }
463
+ it 'it does not error, as we do not know if the parent attribute key was passed in (done at the Hash level)' do
464
+ expect(attribute.validate(value, context)).to be_empty
465
+ end
466
+ end
467
+ end
468
+ context ':null false (non-nullable)' do
469
+ let(:attribute_options) { { null: false } }
386
470
  context 'with a nil value' do
387
471
  let(:value) { nil }
388
472
  it 'returns an error' do
389
- expect(attribute.validate(value, context).first).to eq 'Attribute context is required'
473
+ expect(attribute.validate(value, context).first).to eq 'Attribute context is not nullable'
474
+ end
475
+ end
476
+ end
477
+ context ':null true (nullable)' do
478
+ let(:attribute_options) { { null: true } }
479
+ context 'with a nil value' do
480
+ let(:value) { nil }
481
+ it 'does not error' do
482
+ expect(attribute.validate(value, context)).to be_empty
483
+ end
484
+ end
485
+ end
486
+ context 'defaults to non-nullable if null not defined' do
487
+ let(:attribute_options) { { } }
488
+ context 'with a nil value' do
489
+ let(:value) { nil }
490
+ it 'returns an error' do
491
+ expect(Attributor::Attribute.default_for_null).to be(false)
492
+ expect(attribute.validate(value, context).first).to eq 'Attribute context is not nullable'
493
+ end
494
+ end
495
+ end
496
+
497
+ context 'default can be overrideable with true' do
498
+ let(:attribute_options) { { } }
499
+ context 'with a nil value' do
500
+ let(:value) { nil }
501
+ it 'suceeds' do
502
+ expect(Attributor::Attribute).to receive(:default_for_null).and_return(true)
503
+ expect(attribute.validate(value, context)).to be_empty
390
504
  end
391
505
  end
392
506
  end
@@ -432,6 +546,13 @@ describe Attributor::Attribute do
432
546
  end
433
547
  end
434
548
 
549
+ context 'with a nil value' do
550
+ let(:value) { nil }
551
+ it 'returns no errors' do
552
+ expect(errors).to be_empty
553
+ end
554
+ end
555
+
435
556
  context 'with a value of a value different than the native_type' do
436
557
  let(:value) { 1 }
437
558
 
@@ -441,58 +562,6 @@ describe Attributor::Attribute do
441
562
  end
442
563
  end
443
564
  end
444
-
445
- context '#validate_missing_value' do
446
- let(:key) { '$.instance.ssh_key.name' }
447
- let(:value) { /\w+/.gen }
448
-
449
- let(:attribute_options) { { required_if: key } }
450
-
451
- let(:ssh_key) { double('ssh_key', name: value) }
452
- let(:instance) { double('instance', ssh_key: ssh_key) }
453
-
454
- before { Attributor::AttributeResolver.current.register('instance', instance) }
455
-
456
- let(:attribute_context) { ['$', 'params', 'key_material'] }
457
- subject(:errors) { attribute.validate_missing_value(attribute_context) }
458
-
459
- context 'for a simple dependency without a predicate' do
460
- context 'that is satisfied' do
461
- it { should_not be_empty }
462
- end
463
-
464
- context 'that is missing' do
465
- let(:value) { nil }
466
- it { should be_empty }
467
- end
468
- end
469
-
470
- context 'with a dependency that has a predicate' do
471
- let(:value) { 'default_ssh_key_name' }
472
- # subject(:errors) { attribute.validate_missing_value('') }
473
-
474
- context 'where the target attribute exists, and matches the predicate' do
475
- let(:attribute_options) { { required_if: { key => /default/ } } }
476
-
477
- it { should_not be_empty }
478
-
479
- its(:first) { should match(/Attribute #{Regexp.quote(Attributor.humanize_context(attribute_context))} is required when #{Regexp.quote(key)} matches/) }
480
- end
481
-
482
- context 'where the target attribute exists, but does not match the predicate' do
483
- let(:attribute_options) { { required_if: { key => /other/ } } }
484
-
485
- it { should be_empty }
486
- end
487
-
488
- context 'where the target attribute does not exist' do
489
- let(:attribute_options) { { required_if: { key => /default/ } } }
490
- let(:ssh_key) { double('ssh_key', name: nil) }
491
-
492
- it { should be_empty }
493
- end
494
- end
495
- end
496
565
  end
497
566
 
498
567
  context 'for an attribute for a subclass of Model' do
@@ -561,71 +630,6 @@ describe Attributor::Attribute do
561
630
  end
562
631
  end
563
632
  end
564
-
565
- context '#validate_missing_value' do
566
- let(:type) { Duck }
567
- let(:attribute_name) { nil }
568
- let(:attribute) { Duck.attributes[attribute_name] }
569
-
570
- let(:attribute_context) { ['$', 'duck', attribute_name.to_s] }
571
- subject(:errors) { attribute.validate_missing_value(attribute_context) }
572
-
573
- before do
574
- Attributor::AttributeResolver.current.register('duck', duck)
575
- end
576
-
577
- context 'for a dependency with no predicate' do
578
- let(:attribute_name) { :email }
579
-
580
- let(:duck) do
581
- d = Duck.new
582
- d.age = 1
583
- d.name = 'Donald'
584
- d
585
- end
586
-
587
- context 'where the target attribute exists, and matches the predicate' do
588
- it { should_not be_empty }
589
- its(:first) { should eq 'Attribute $.duck.email is required when name (for $.duck) is present.' }
590
- end
591
- context 'where the target attribute does not exist' do
592
- before do
593
- duck.name = nil
594
- end
595
- it { should be_empty }
596
- end
597
- end
598
-
599
- context 'for a dependency with a predicate' do
600
- let(:attribute_name) { :age }
601
-
602
- let(:duck) do
603
- d = Duck.new
604
- d.name = 'Daffy'
605
- d.email = 'daffy@darkwing.uoregon.edu' # he's a duck,get it?
606
- d
607
- end
608
-
609
- context 'where the target attribute exists, and matches the predicate' do
610
- it { should_not be_empty }
611
- its(:first) { should match(/Attribute #{Regexp.quote('$.duck.age')} is required when name #{Regexp.quote('(for $.duck)')} matches/) }
612
- end
613
-
614
- context 'where the target attribute exists, and does not match the predicate' do
615
- before do
616
- duck.name = 'Donald'
617
- end
618
- it { should be_empty }
619
- end
620
-
621
- context 'where the target attribute does not exist' do
622
- before do
623
- duck.name = nil
624
- end
625
- it { should be_empty }
626
- end
627
- end
628
- end
629
633
  end
630
634
  end
631
635
 
@@ -682,4 +686,24 @@ describe Attributor::Attribute do
682
686
  end
683
687
  end
684
688
  end
689
+
690
+ context '.nullable_attribute?' do
691
+ subject { described_class.nullable_attribute?(options) }
692
+ context 'with null: true option' do
693
+ let(:options) { { null: true } }
694
+ it { should be_truthy }
695
+ end
696
+ context 'with null: false option' do
697
+ let(:options) { { null: false } }
698
+ it { should be_falsey }
699
+ end
700
+ context 'defaults to false without any null option' do
701
+ let(:options) { { } }
702
+ it { should be_falsey }
703
+ end
704
+ context 'defaults to false if null: nil' do
705
+ let(:options) { { null: nil } }
706
+ it { should be_falsey }
707
+ end
708
+ end
685
709
  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
@@ -109,31 +109,31 @@ describe Attributor::HashDSLCompiler do
109
109
  context 'for :all' do
110
110
  let(:arguments) { { all: [:one, :two, :three] } }
111
111
  let(:value) { [:one] }
112
- let(:validation_error) { ['Key two is required for $.', 'Key three is required for $.'] }
112
+ let(:validation_error) { ["Attribute $.key(:two) is required.", "Attribute $.key(:three) is required."] }
113
113
  it { expect(subject).to include(*validation_error) }
114
114
  end
115
115
  context 'for :exactly' do
116
116
  let(:requirement) { req_class.new(exactly: 1).of(:one, :two) }
117
117
  let(:value) { [:one, :two] }
118
- let(:validation_error) { 'Exactly 1 of the following keys [:one, :two] are required for $. Found 2 instead: [:one, :two]' }
118
+ let(:validation_error) { 'Exactly 1 of the following attributes [:one, :two] are required for $. Found 2 instead: [:one, :two]' }
119
119
  it { expect(subject).to include(validation_error) }
120
120
  end
121
121
  context 'for :at_least' do
122
122
  let(:requirement) { req_class.new(at_least: 2).of(:one, :two, :three) }
123
123
  let(:value) { [:one] }
124
- let(:validation_error) { 'At least 2 keys out of [:one, :two, :three] are required to be passed in for $. Found [:one]' }
124
+ let(:validation_error) { 'At least 2 attributes out of [:one, :two, :three] are required to be passed in for $. Found [:one]' }
125
125
  it { expect(subject).to include(validation_error) }
126
126
  end
127
127
  context 'for :at_most' do
128
128
  let(:requirement) { req_class.new(at_most: 1).of(:one, :two, :three) }
129
129
  let(:value) { [:one, :two] }
130
- let(:validation_error) { 'At most 1 keys out of [:one, :two, :three] can be passed in for $. Found [:one, :two]' }
130
+ let(:validation_error) { 'At most 1 attributes out of [:one, :two, :three] can be passed in for $. Found [:one, :two]' }
131
131
  it { expect(subject).to include(validation_error) }
132
132
  end
133
133
  context 'for :exclusive' do
134
134
  let(:arguments) { { exclusive: [:one, :two] } }
135
135
  let(:value) { [:one, :two] }
136
- let(:validation_error) { 'keys [:one, :two] are mutually exclusive for $.' }
136
+ let(:validation_error) { 'Attributes [:one, :two] are mutually exclusive for $.' }
137
137
  it { expect(subject).to include(validation_error) }
138
138
  end
139
139
  end
data/spec/spec_helper.rb CHANGED
@@ -23,9 +23,7 @@ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
23
23
 
24
24
  RSpec.configure do |config|
25
25
  config.around(:each) do |example|
26
- Attributor::AttributeResolver.current = Attributor::AttributeResolver.new
27
26
  example.run
28
- Attributor::AttributeResolver.current = nil
29
27
  end
30
28
  end
31
29
 
@@ -0,0 +1,7 @@
1
+ class PositiveIntegerType < Attributor::Integer
2
+
3
+ def self.options
4
+ { min: 0 }
5
+ end
6
+
7
+ end
@@ -11,9 +11,9 @@ end
11
11
 
12
12
  class Duck < Attributor::Model
13
13
  attributes do
14
- attribute :age, Attributor::Integer, required_if: { 'name' => 'Daffy' }
14
+ attribute :age, Attributor::Integer
15
15
  attribute :name, Attributor::String
16
- attribute :email, Attributor::String, required_if: 'name'
16
+ attribute :email, Attributor::String
17
17
  attribute :angry, Attributor::Boolean, default: true, example: /true|false/, description: 'Angry bird?'
18
18
  attribute :weight, Attributor::Float, example: /\d{1,2}\.\d/, description: 'The weight of the duck'
19
19
  attribute :type, Attributor::Symbol, values: [:duck]
@@ -50,15 +50,15 @@ class Cormorant < Attributor::Model
50
50
  end
51
51
 
52
52
  # This will be a collection of arbitrary Ruby Objects
53
- attribute :fish, Attributor::Collection, description: 'All kinds of fish for feeding the babies'
53
+ attribute :all_the_fish, Attributor::Collection, description: 'All kinds of fish for feeding the babies'
54
54
 
55
55
  # This will be a collection of Cormorants (note, this relationship is circular)
56
- attribute :neighbors, Attributor::Collection.of(Cormorant), description: 'Neighbor cormorants'
56
+ attribute :neighbors, Attributor::Collection.of(Cormorant), member_options: {null: false}, description: 'Neighbor cormorants', null: false
57
57
 
58
58
  # This will be a collection of instances of an anonymous Struct class, each having two well-defined attributes
59
59
 
60
60
  attribute :babies, Attributor::Collection.of(Attributor::Struct), description: 'All the babies', member_options: { identity: :name } do
61
- attribute :name, Attributor::String, example: /[:name]/, description: 'The name of the baby cormorant'
61
+ attribute :name, Attributor::String, example: /[:name]/, description: 'The name of the baby cormorant', required: true
62
62
  attribute :months, Attributor::Integer, default: 0, min: 0, description: 'The age in months of the baby cormorant'
63
63
  attribute :weight, Attributor::Float, example: /\d{1,2}\.\d{3}/, description: 'The weight in kg of the baby cormorant'
64
64
  end
@@ -76,8 +76,8 @@ end
76
76
 
77
77
  class Address < Attributor::Model
78
78
  attributes do
79
- attribute :name, String, example: /\w+/
80
- attribute :state, String, values: %w(OR CA)
79
+ attribute :name, String, example: /\w+/, null: true
80
+ attribute :state, String, values: %w(OR CA), null: false
81
81
  attribute :person, Person, example: proc { |address, context| Person.example(context, address: address) }
82
82
  requires :name
83
83
  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
@@ -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