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.
@@ -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