attributor 2.6.1 → 3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 => 'name'} do
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(::Array)
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
- type.load(value).should == value
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 == expected_result
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 == expected_value
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
- let(:collection_members) { [1, 2, 'three'] }
248
- let(:expected_errors) { ["error 1", "error 2", "error 3"]}
249
-
250
- before do
251
- collection_members.zip(expected_errors).each do |member, expected_error|
252
- type.member_attribute.should_receive(:validate).
253
- with(member,an_instance_of(Array)). # we don't care about the exact context here
254
- and_return([expected_error])
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
- it 'validates members' do
259
- type.validate(collection_members).should =~ expected_errors
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 Array" do
280
+ it "returns an instance of the type" do
266
281
  value = type.example
267
- value.should be_a(::Array)
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
@@ -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 eq(array)
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(Array)
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
@@ -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 be(example_hash)
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
- subject(:description) { type.describe }
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
- keys = description[:keys]
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
- keys['a string'].should eq(type: {name: 'String', id: 'Attributor-String', family: 'string'} )
517
- keys['1'].should eq(type: {name: 'Integer', id: 'Attributor-Integer', family: 'numeric'}, options: {min: 1, max: 20} )
518
- keys['some_date'].should eq(type: {name: 'DateTime', id: 'Attributor-DateTime', family: 'temporal'})
519
- keys['defaulted'].should eq(type: {name: 'String', id: 'Attributor-String', family: 'string'}, default: 'default value')
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