attributor 2.6.1 → 3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -1
- data/CHANGELOG.md +38 -26
- data/lib/attributor.rb +8 -7
- data/lib/attributor/attribute.rb +41 -25
- data/lib/attributor/dsl_compiler.rb +7 -1
- data/lib/attributor/type.rb +10 -8
- data/lib/attributor/types/collection.rb +49 -10
- data/lib/attributor/types/csv.rb +8 -1
- data/lib/attributor/types/hash.rb +45 -11
- data/lib/attributor/types/model.rb +13 -10
- data/lib/attributor/types/string.rb +4 -4
- data/lib/attributor/types/uri.rb +57 -0
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_spec.rb +136 -64
- data/spec/support/models.rb +7 -2
- data/spec/type_spec.rb +13 -3
- data/spec/types/collection_spec.rb +66 -19
- data/spec/types/csv_spec.rb +14 -3
- data/spec/types/hash_spec.rb +134 -10
- data/spec/types/ids_spec.rb +1 -1
- data/spec/types/model_spec.rb +78 -18
- data/spec/types/string_spec.rb +6 -8
- data/spec/types/uri_spec.rb +12 -0
- metadata +6 -3
data/spec/support/models.rb
CHANGED
@@ -40,11 +40,16 @@ class Turducken < Attributor::Model
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
-
|
44
43
|
# http://en.wikipedia.org/wiki/Cormorant
|
45
44
|
|
46
45
|
class Cormorant < Attributor::Model
|
47
46
|
attributes do
|
47
|
+
attribute :name, String, :description => "Name of the Cormorant", :example => /[:name:]/
|
48
|
+
attribute :timestamps do
|
49
|
+
attribute :born_at, DateTime
|
50
|
+
attribute :died_at, DateTime, example: Proc.new {|timestamps| timestamps.born_at + 10}
|
51
|
+
end
|
52
|
+
|
48
53
|
# This will be a collection of arbitrary Ruby Objects
|
49
54
|
attribute :fish, Attributor::Collection, :description => "All kinds of fish for feeding the babies"
|
50
55
|
|
@@ -53,7 +58,7 @@ class Cormorant < Attributor::Model
|
|
53
58
|
|
54
59
|
# This will be a collection of instances of an anonymous Struct class, each having two well-defined attributes
|
55
60
|
|
56
|
-
attribute :babies, Attributor::Collection.of(Attributor::Struct), :description => "All the babies", :member_options => {:identity =>
|
61
|
+
attribute :babies, Attributor::Collection.of(Attributor::Struct), :description => "All the babies", :member_options => {:identity => :name} do
|
57
62
|
attribute :name, Attributor::String, :example => /[:name]/, :description => "The name of the baby cormorant"
|
58
63
|
attribute :months, Attributor::Integer, :default => 0, :min => 0, :description => "The age in months of the baby cormorant"
|
59
64
|
attribute :weight, Attributor::Float, :example => /\d{1,2}\.\d{3}/, :description => "The weight in kg of the baby cormorant"
|
data/spec/type_spec.rb
CHANGED
@@ -3,7 +3,7 @@ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
|
|
3
3
|
|
4
4
|
describe Attributor::Type do
|
5
5
|
|
6
|
-
subject(:test_type) do
|
6
|
+
subject(:test_type) do
|
7
7
|
Class.new do
|
8
8
|
include Attributor::Type
|
9
9
|
def self.native_type
|
@@ -42,7 +42,7 @@ describe Attributor::Type do
|
|
42
42
|
test_type.load(value).should be(value)
|
43
43
|
end
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
context "when given a value that is of native_type" do
|
47
47
|
let(:value) { "one" }
|
48
48
|
it 'returns the value' do
|
@@ -53,7 +53,7 @@ describe Attributor::Type do
|
|
53
53
|
context "when given a value that is not of native_type" do
|
54
54
|
let(:value) { 1 }
|
55
55
|
let(:context) { ['top','sub'] }
|
56
|
-
|
56
|
+
|
57
57
|
it 'raises an exception' do
|
58
58
|
expect { test_type.load(value,context) }.to raise_error( Attributor::IncompatibleTypeError, /cannot load values of type Fixnum.*while loading top.sub/)
|
59
59
|
end
|
@@ -149,6 +149,7 @@ describe Attributor::Type do
|
|
149
149
|
end
|
150
150
|
|
151
151
|
context 'describe' do
|
152
|
+
let(:example){ "Foo" }
|
152
153
|
subject(:description) { test_type.describe }
|
153
154
|
it 'outputs the type name' do
|
154
155
|
description[:name].should eq(test_type.name)
|
@@ -156,6 +157,15 @@ describe Attributor::Type do
|
|
156
157
|
it 'outputs the type id' do
|
157
158
|
description[:id].should eq(test_type.name)
|
158
159
|
end
|
160
|
+
|
161
|
+
context 'with an example' do
|
162
|
+
subject(:description) { test_type.describe(example: example) }
|
163
|
+
it 'includes it in the :example key' do
|
164
|
+
description.should have_key(:example)
|
165
|
+
description[:example].should be(example)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
159
169
|
end
|
160
170
|
|
161
171
|
end
|
@@ -59,7 +59,7 @@ describe Attributor::Collection do
|
|
59
59
|
|
60
60
|
context '.native_type' do
|
61
61
|
it "returns Array" do
|
62
|
-
type.native_type.should be(
|
62
|
+
type.native_type.should be(type)
|
63
63
|
end
|
64
64
|
end
|
65
65
|
|
@@ -79,7 +79,6 @@ describe Attributor::Collection do
|
|
79
79
|
|
80
80
|
context 'for invalid JSON strings' do
|
81
81
|
[
|
82
|
-
'{}',
|
83
82
|
'foobar',
|
84
83
|
'2',
|
85
84
|
'',
|
@@ -107,15 +106,21 @@ describe Attributor::Collection do
|
|
107
106
|
end
|
108
107
|
|
109
108
|
context 'with unspecified element type' do
|
109
|
+
context 'for nil values' do
|
110
|
+
it 'returns nil' do
|
111
|
+
type.load(nil).should be nil
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
110
115
|
context 'for valid values' do
|
111
116
|
[
|
112
|
-
nil,
|
113
117
|
[],
|
114
118
|
[1,2,3],
|
115
119
|
[Object.new, [1,2], nil, true]
|
116
120
|
].each do |value|
|
117
121
|
it "returns value when incoming value is #{value.inspect}" do
|
118
|
-
|
122
|
+
|
123
|
+
type.load(value).should =~ value
|
119
124
|
end
|
120
125
|
end
|
121
126
|
end
|
@@ -141,7 +146,7 @@ describe Attributor::Collection do
|
|
141
146
|
}.each do |member_type, value|
|
142
147
|
it "returns loaded value when member_type is #{member_type} and value is #{value.inspect}" do
|
143
148
|
expected_result = value.map {|v| member_type.load(v)}
|
144
|
-
type.of(member_type).load(value).should
|
149
|
+
type.of(member_type).load(value).should =~ expected_result
|
145
150
|
end
|
146
151
|
end
|
147
152
|
end
|
@@ -221,7 +226,7 @@ describe Attributor::Collection do
|
|
221
226
|
].each do |value|
|
222
227
|
it "returns value when incoming value is #{value.inspect}" do
|
223
228
|
expected_value = value.map {|v| simple_struct.load(v.clone)}
|
224
|
-
type.of(simple_struct).load(value).should
|
229
|
+
type.of(simple_struct).load(value).should =~ expected_value
|
225
230
|
end
|
226
231
|
end
|
227
232
|
end
|
@@ -244,27 +249,37 @@ describe Attributor::Collection do
|
|
244
249
|
end
|
245
250
|
|
246
251
|
context '.validate' do
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
252
|
+
context 'compatible type values' do
|
253
|
+
let(:collection_members) { [1, 2, 'three'] }
|
254
|
+
let(:expected_errors) { ["error 1", "error 2", "error 3"]}
|
255
|
+
|
256
|
+
before do
|
257
|
+
collection_members.zip(expected_errors).each do |member, expected_error|
|
258
|
+
type.member_attribute.should_receive(:validate).
|
259
|
+
with(member,an_instance_of(Array)). # we don't care about the exact context here
|
260
|
+
and_return([expected_error])
|
261
|
+
end
|
255
262
|
end
|
256
|
-
end
|
257
263
|
|
258
|
-
|
259
|
-
|
264
|
+
it 'validates members' do
|
265
|
+
type.validate(collection_members).should =~ expected_errors
|
266
|
+
end
|
267
|
+
end
|
268
|
+
context 'invalid incoming types' do
|
269
|
+
subject(:type) { Attributor::Collection.of(Integer) }
|
270
|
+
it 'raise an exception' do
|
271
|
+
expect {
|
272
|
+
type.validate('invalid_value')
|
273
|
+
}.to raise_error(Attributor::IncompatibleTypeError, /cannot load values of type String/)
|
274
|
+
end
|
260
275
|
end
|
261
276
|
end
|
262
277
|
|
263
278
|
|
264
279
|
context '.example' do
|
265
|
-
it "returns an
|
280
|
+
it "returns an instance of the type" do
|
266
281
|
value = type.example
|
267
|
-
value.should be_a(
|
282
|
+
value.should be_a(type)
|
268
283
|
end
|
269
284
|
|
270
285
|
[
|
@@ -277,6 +292,7 @@ describe Attributor::Collection do
|
|
277
292
|
].each do |member_type|
|
278
293
|
it "returns an Array of native types of #{member_type}" do
|
279
294
|
value = Attributor::Collection.of(member_type).example
|
295
|
+
value.should_not be_empty
|
280
296
|
value.all? { |element| member_type.valid_type?(element) }.should be_true
|
281
297
|
end
|
282
298
|
end
|
@@ -300,4 +316,35 @@ describe Attributor::Collection do
|
|
300
316
|
end
|
301
317
|
|
302
318
|
end
|
319
|
+
|
320
|
+
context '.describe' do
|
321
|
+
let(:type){ Attributor::Collection.of(Attributor::String)}
|
322
|
+
let(:example){ nil }
|
323
|
+
subject(:described){ type.describe(example: example)}
|
324
|
+
it 'includes the member_attribute' do
|
325
|
+
described.should have_key(:member_attribute)
|
326
|
+
described[:member_attribute].should_not have_key(:example)
|
327
|
+
end
|
328
|
+
|
329
|
+
context 'with an example' do
|
330
|
+
let(:example){ type.example }
|
331
|
+
it 'includes the member_attribute with an example from the first member' do
|
332
|
+
described.should have_key(:member_attribute)
|
333
|
+
described[:member_attribute].should have_key(:example)
|
334
|
+
described[:member_attribute].should eq( type.member_attribute.describe(example: example.first ) )
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
context 'dumping' do
|
340
|
+
let(:type) { Attributor::Collection.of(Cormorant) }
|
341
|
+
|
342
|
+
subject(:example) { type.example }
|
343
|
+
it 'dumps' do
|
344
|
+
expect {
|
345
|
+
example.dump
|
346
|
+
}.to_not raise_error
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
303
350
|
end
|
data/spec/types/csv_spec.rb
CHANGED
@@ -9,7 +9,7 @@ describe Attributor::CSV do
|
|
9
9
|
let!(:value) { array.join(',') }
|
10
10
|
|
11
11
|
it 'parses the value and returns an array with the right types' do
|
12
|
-
csv.load(value).should
|
12
|
+
csv.load(value).should =~ array
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
@@ -22,7 +22,7 @@ describe Attributor::CSV do
|
|
22
22
|
end
|
23
23
|
|
24
24
|
it 'generates a comma-separated list of Integer values' do
|
25
|
-
loaded_example.should be_a(
|
25
|
+
loaded_example.should be_a(csv)
|
26
26
|
loaded_example.size.should be > 1
|
27
27
|
loaded_example.each { |e| e.should be_a(Integer) }
|
28
28
|
end
|
@@ -43,10 +43,21 @@ describe Attributor::CSV do
|
|
43
43
|
it 'dumps non-Integer values also' do
|
44
44
|
csv.dump(str_vals).should eq(str_vals.join(','))
|
45
45
|
end
|
46
|
-
|
46
|
+
|
47
47
|
it 'dumps nil values as nil' do
|
48
48
|
csv.dump(nil).should eq(nil)
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
|
+
context '.describe' do
|
53
|
+
let(:example){ csv.example }
|
54
|
+
subject(:described){ csv.describe(example: example)}
|
55
|
+
it 'adds a string example if an example is passed' do
|
56
|
+
described.should have_key(:example)
|
57
|
+
described[:example].should eq(csv.dump(example))
|
58
|
+
end
|
59
|
+
it 'ensures no member_attribute key exists from underlying Collection' do
|
60
|
+
described.should_not have_key(:member_attribute)
|
61
|
+
end
|
62
|
+
end
|
52
63
|
end
|
data/spec/types/hash_spec.rb
CHANGED
@@ -9,6 +9,43 @@ describe Attributor::Hash do
|
|
9
9
|
its(:key_type) { should be(Attributor::Object) }
|
10
10
|
its(:value_type) { should be(Attributor::Object) }
|
11
11
|
|
12
|
+
context 'attributes' do
|
13
|
+
context 'with an exception from the definition block' do
|
14
|
+
subject(:broken_model) do
|
15
|
+
Class.new(Attributor::Model) do
|
16
|
+
attributes do
|
17
|
+
raise 'sorry :('
|
18
|
+
attribute :name, String
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'throws original exception upon first run' do
|
24
|
+
lambda {
|
25
|
+
broken_model.attributes
|
26
|
+
}.should raise_error(RuntimeError, 'sorry :(')
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'throws InvalidDefinition for subsequent access' do
|
30
|
+
broken_model.attributes rescue nil
|
31
|
+
|
32
|
+
lambda {
|
33
|
+
broken_model.attributes
|
34
|
+
}.should raise_error(Attributor::InvalidDefinition)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'throws for any attempts at using of an instance of it' do
|
38
|
+
broken_model.attributes rescue nil
|
39
|
+
|
40
|
+
instance = broken_model.new
|
41
|
+
lambda {
|
42
|
+
instance.name
|
43
|
+
}.should raise_error(Attributor::InvalidDefinition)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
12
49
|
context 'default options' do
|
13
50
|
subject(:options) { type.options }
|
14
51
|
it 'has allow_extra false' do
|
@@ -478,14 +515,15 @@ describe Attributor::Hash do
|
|
478
515
|
let(:example_hash) { {:key => "value"} }
|
479
516
|
let(:options) { { example: proc { example_hash } } }
|
480
517
|
it 'uses the hash' do
|
481
|
-
attribute.example.should
|
518
|
+
attribute.example.should eq(example_hash)
|
482
519
|
end
|
483
520
|
end
|
484
521
|
|
485
522
|
end
|
486
523
|
|
487
524
|
context '.describe' do
|
488
|
-
|
525
|
+
let(:example){ nil }
|
526
|
+
subject(:description) { type.describe(example: example) }
|
489
527
|
context 'for hashes with key and value types' do
|
490
528
|
it 'describes the type correctly' do
|
491
529
|
description[:name].should eq('Hash')
|
@@ -511,12 +549,25 @@ describe Attributor::Hash do
|
|
511
549
|
description[:key].should eq(type:{name: 'String', id: 'Attributor-String', family: 'string'})
|
512
550
|
description.should_not have_key(:value)
|
513
551
|
|
514
|
-
|
552
|
+
attrs = description[:attributes]
|
553
|
+
|
554
|
+
attrs['a string'].should eq(type: {name: 'String', id: 'Attributor-String', family: 'string'} )
|
555
|
+
attrs['1'].should eq(type: {name: 'Integer', id: 'Attributor-Integer', family: 'numeric'}, options: {min: 1, max: 20} )
|
556
|
+
attrs['some_date'].should eq(type: {name: 'DateTime', id: 'Attributor-DateTime', family: 'temporal'})
|
557
|
+
attrs['defaulted'].should eq(type: {name: 'String', id: 'Attributor-String', family: 'string'}, default: 'default value')
|
558
|
+
end
|
559
|
+
|
560
|
+
context 'with an example' do
|
561
|
+
let(:example){ type.example }
|
515
562
|
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
563
|
+
it 'should have the matching example for each leaf key' do
|
564
|
+
description[:attributes].keys.should =~ type.keys.keys
|
565
|
+
description[:attributes].each do |name,sub_description|
|
566
|
+
sub_description.should have_key(:example)
|
567
|
+
val = type.attributes[name].dump( example[name] ).to_s
|
568
|
+
sub_description[:example].should eq( val )
|
569
|
+
end
|
570
|
+
end
|
520
571
|
end
|
521
572
|
end
|
522
573
|
end
|
@@ -613,6 +664,79 @@ describe Attributor::Hash do
|
|
613
664
|
end
|
614
665
|
end
|
615
666
|
|
667
|
+
context '.from_hash' do
|
668
|
+
|
669
|
+
context 'without allowing extra keys' do
|
670
|
+
let(:type) do
|
671
|
+
Class.new(Attributor::Hash) do
|
672
|
+
self.value_type = String
|
673
|
+
|
674
|
+
keys do
|
675
|
+
key :one, String
|
676
|
+
key :two, String, default: 'two'
|
677
|
+
end
|
678
|
+
end
|
679
|
+
end
|
680
|
+
subject(:input){ {} }
|
681
|
+
subject(:output) { type.load(input) }
|
682
|
+
|
683
|
+
its(:class){ should be(type) }
|
684
|
+
|
685
|
+
let(:load_context) { "$.some_root" }
|
686
|
+
it 'complains about the extra, with the right context' do
|
687
|
+
expect{
|
688
|
+
type.load( {one: 'one', three: 3} , load_context )
|
689
|
+
}.to raise_error(Attributor::AttributorException,/Unknown key received: :three while loading \$\.some_root.key\(:three\)/)
|
690
|
+
end
|
691
|
+
context 'properly sets them (and loads them) in the created instance' do
|
692
|
+
let(:input){ {one: 'one', two: 2 } }
|
693
|
+
|
694
|
+
its(:keys){ should eq([:one, :two])}
|
695
|
+
its([:one]){ should eq('one') }
|
696
|
+
its([:two]){ should eq('2') } #loaded as a string
|
697
|
+
end
|
698
|
+
context 'properly sets the default values when not passed in' do
|
699
|
+
let(:input){ {one: 'one'} }
|
700
|
+
|
701
|
+
its([:one]){ should eq('one') }
|
702
|
+
its([:two]){ should eq('two') }
|
703
|
+
end
|
704
|
+
end
|
705
|
+
|
706
|
+
context ' allowing extra keys' do
|
707
|
+
context 'at the top level' do
|
708
|
+
let(:type) do
|
709
|
+
Class.new(Attributor::Hash) do
|
710
|
+
keys allow_extra: true do
|
711
|
+
key :one, String
|
712
|
+
end
|
713
|
+
end
|
714
|
+
end
|
715
|
+
let(:input){ {one: 'one', three: 'tres' } }
|
716
|
+
subject(:output) { type.load(input) }
|
717
|
+
|
718
|
+
its(:keys){ should eq([:one,:three])}
|
719
|
+
end
|
720
|
+
context 'inside an :other subkey' do
|
721
|
+
let(:type) do
|
722
|
+
Class.new(Attributor::Hash) do
|
723
|
+
keys allow_extra: true do
|
724
|
+
key :one, String
|
725
|
+
extra :other
|
726
|
+
end
|
727
|
+
end
|
728
|
+
end
|
729
|
+
let(:input){ {one: 'one', three: 'tres' } }
|
730
|
+
subject(:output) { type.load(input) }
|
731
|
+
|
732
|
+
its(:keys){ should =~ [:one,:other] }
|
733
|
+
it 'has the key inside the :other hash' do
|
734
|
+
expect(output[:other]).to eq({three: 'tres'})
|
735
|
+
end
|
736
|
+
end
|
737
|
+
end
|
738
|
+
end
|
739
|
+
|
616
740
|
context 'case_insensitive_load option' do
|
617
741
|
let(:case_insensitive) { true }
|
618
742
|
let(:type) { Attributor::Hash.of(key: String).construct(block, case_insensitive_load: case_insensitive) }
|
@@ -634,17 +758,17 @@ describe Attributor::Hash do
|
|
634
758
|
output['CamelCase'].should eq(3)
|
635
759
|
end
|
636
760
|
it 'has loaded the (internal) insensitive_map upon building the definition' do
|
637
|
-
type.definition
|
761
|
+
type.definition
|
638
762
|
type.insensitive_map.should be_kind_of(::Hash)
|
639
763
|
type.insensitive_map.keys.should =~ ["downcase","upcase","camelcase"]
|
640
764
|
end
|
641
765
|
end
|
642
|
-
|
766
|
+
|
643
767
|
context 'when not defined' do
|
644
768
|
let(:case_insensitive) { false }
|
645
769
|
|
646
770
|
it 'skips the loading of the (internal) insensitive_map' do
|
647
|
-
type.definition
|
771
|
+
type.definition
|
648
772
|
type.insensitive_map.should be_nil
|
649
773
|
end
|
650
774
|
end
|