attributor 2.6.1 → 3.0
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.
- 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
|