attributor 5.0.2 → 5.1.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/.rubocop.yml +30 -0
- data/.travis.yml +6 -4
- data/CHANGELOG.md +6 -1
- data/Gemfile +1 -1
- data/Guardfile +14 -8
- data/Rakefile +4 -5
- data/attributor.gemspec +34 -29
- data/lib/attributor.rb +23 -29
- data/lib/attributor/attribute.rb +108 -127
- data/lib/attributor/attribute_resolver.rb +12 -26
- data/lib/attributor/dsl_compiler.rb +17 -21
- data/lib/attributor/dumpable.rb +1 -2
- data/lib/attributor/example_mixin.rb +5 -8
- data/lib/attributor/exceptions.rb +5 -6
- data/lib/attributor/extensions/randexp.rb +3 -5
- data/lib/attributor/extras/field_selector.rb +4 -4
- data/lib/attributor/extras/field_selector/transformer.rb +6 -7
- data/lib/attributor/families/numeric.rb +0 -2
- data/lib/attributor/families/temporal.rb +1 -4
- data/lib/attributor/hash_dsl_compiler.rb +22 -25
- data/lib/attributor/type.rb +24 -32
- data/lib/attributor/types/bigdecimal.rb +7 -14
- data/lib/attributor/types/boolean.rb +5 -8
- data/lib/attributor/types/class.rb +9 -10
- data/lib/attributor/types/collection.rb +34 -44
- data/lib/attributor/types/container.rb +9 -15
- data/lib/attributor/types/csv.rb +7 -10
- data/lib/attributor/types/date.rb +20 -25
- data/lib/attributor/types/date_time.rb +7 -14
- data/lib/attributor/types/float.rb +4 -6
- data/lib/attributor/types/hash.rb +171 -196
- data/lib/attributor/types/ids.rb +2 -6
- data/lib/attributor/types/integer.rb +12 -17
- data/lib/attributor/types/model.rb +39 -48
- data/lib/attributor/types/object.rb +2 -4
- data/lib/attributor/types/polymorphic.rb +118 -0
- data/lib/attributor/types/regexp.rb +4 -5
- data/lib/attributor/types/string.rb +6 -7
- data/lib/attributor/types/struct.rb +8 -15
- data/lib/attributor/types/symbol.rb +3 -6
- data/lib/attributor/types/tempfile.rb +5 -6
- data/lib/attributor/types/time.rb +11 -11
- data/lib/attributor/types/uri.rb +9 -10
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_resolver_spec.rb +57 -78
- data/spec/attribute_spec.rb +174 -216
- data/spec/attributor_spec.rb +11 -15
- data/spec/dsl_compiler_spec.rb +19 -33
- data/spec/dumpable_spec.rb +6 -7
- data/spec/extras/field_selector/field_selector_spec.rb +1 -1
- data/spec/families_spec.rb +1 -3
- data/spec/hash_dsl_compiler_spec.rb +65 -74
- data/spec/spec_helper.rb +9 -3
- data/spec/support/hashes.rb +2 -3
- data/spec/support/models.rb +30 -36
- data/spec/support/polymorphics.rb +10 -0
- data/spec/type_spec.rb +38 -61
- data/spec/types/bigdecimal_spec.rb +11 -15
- data/spec/types/boolean_spec.rb +12 -39
- data/spec/types/class_spec.rb +10 -11
- data/spec/types/collection_spec.rb +72 -81
- data/spec/types/container_spec.rb +22 -26
- data/spec/types/csv_spec.rb +15 -16
- data/spec/types/date_spec.rb +16 -33
- data/spec/types/date_time_spec.rb +16 -33
- data/spec/types/file_upload_spec.rb +1 -2
- data/spec/types/float_spec.rb +7 -14
- data/spec/types/hash_spec.rb +285 -289
- data/spec/types/ids_spec.rb +5 -7
- data/spec/types/integer_spec.rb +37 -46
- data/spec/types/model_spec.rb +111 -128
- data/spec/types/polymorphic_spec.rb +134 -0
- data/spec/types/regexp_spec.rb +4 -7
- data/spec/types/string_spec.rb +17 -21
- data/spec/types/struct_spec.rb +40 -47
- data/spec/types/tempfile_spec.rb +1 -2
- data/spec/types/temporal_spec.rb +9 -0
- data/spec/types/time_spec.rb +16 -32
- data/spec/types/type_spec.rb +15 -0
- data/spec/types/uri_spec.rb +6 -7
- metadata +77 -25
data/spec/types/ids_spec.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
|
2
2
|
|
3
3
|
describe Attributor::Ids do
|
4
|
-
|
5
4
|
context '.for' do
|
6
|
-
let(:chickens) { 10
|
5
|
+
let(:chickens) { Array.new(10) { Chicken.example } }
|
7
6
|
|
8
7
|
let(:emails) { chickens.collect(&:email) }
|
9
8
|
let(:value) { emails.join(',') }
|
@@ -13,20 +12,19 @@ describe Attributor::Ids do
|
|
13
12
|
its(:member_attribute) { should be(Chicken.attributes[:email]) }
|
14
13
|
|
15
14
|
it 'loads' do
|
16
|
-
ids.load(value).
|
15
|
+
expect(ids.load(value)).to match_array emails
|
17
16
|
end
|
18
17
|
|
19
18
|
it 'generates valid, loadable examples' do
|
20
|
-
ids.validate(ids.load(ids.example)).
|
19
|
+
expect(ids.validate(ids.load(ids.example))).to be_empty
|
21
20
|
end
|
22
|
-
|
23
21
|
end
|
24
22
|
|
25
23
|
context 'attempting to define it as a collection using .of(type)' do
|
26
24
|
it 'raises an error' do
|
27
|
-
expect
|
25
|
+
expect do
|
28
26
|
Attributor::Ids.of(Chicken)
|
29
|
-
|
27
|
+
end.to raise_error(/Defining Ids.of\(type\) is not allowed/)
|
30
28
|
end
|
31
29
|
end
|
32
30
|
end
|
data/spec/types/integer_spec.rb
CHANGED
@@ -1,23 +1,21 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb')
|
2
2
|
|
3
3
|
describe Attributor::Integer do
|
4
|
-
|
5
4
|
subject(:type) { Attributor::Integer }
|
6
5
|
|
7
6
|
it 'it is not Dumpable' do
|
8
|
-
type.new.is_a?(Attributor::Dumpable).
|
7
|
+
expect(type.new.is_a?(Attributor::Dumpable)).not_to be(true)
|
9
8
|
end
|
10
9
|
|
11
10
|
context '.example' do
|
12
|
-
|
13
11
|
context 'when :min and :max are unspecified' do
|
14
12
|
context 'valid cases' do
|
15
13
|
it "returns an Integer in the range [0,#{Attributor::Integer::EXAMPLE_RANGE}]" do
|
16
14
|
20.times do
|
17
15
|
value = type.example
|
18
|
-
value.
|
19
|
-
value.
|
20
|
-
value.
|
16
|
+
expect(value).to be_a(::Integer)
|
17
|
+
expect(value).to be <= Attributor::Integer::EXAMPLE_RANGE
|
18
|
+
expect(value).to be >= 0
|
21
19
|
end
|
22
20
|
end
|
23
21
|
end
|
@@ -25,13 +23,13 @@ describe Attributor::Integer do
|
|
25
23
|
|
26
24
|
context 'when :min is unspecified' do
|
27
25
|
context 'valid cases' do
|
28
|
-
[5,
|
26
|
+
[5, 100_000_000_000_000_000_000, -100_000_000_000_000_000_000].each do |max|
|
29
27
|
it "returns an Integer in the range [,#{max.inspect}]" do
|
30
28
|
20.times do
|
31
|
-
value = type.example(nil, options: {max: max})
|
32
|
-
value.
|
33
|
-
value.
|
34
|
-
value.
|
29
|
+
value = type.example(nil, options: { max: max })
|
30
|
+
expect(value).to be_a(::Integer)
|
31
|
+
expect(value).to be <= max
|
32
|
+
expect(value).to be >= max - Attributor::Integer::EXAMPLE_RANGE
|
35
33
|
end
|
36
34
|
end
|
37
35
|
end
|
@@ -40,10 +38,10 @@ describe Attributor::Integer do
|
|
40
38
|
context 'invalid cases' do
|
41
39
|
['invalid', false].each do |max|
|
42
40
|
it "raises for the invalid range [,#{max.inspect}]" do
|
43
|
-
expect
|
44
|
-
value = type.example(nil, options: {max: max})
|
45
|
-
value.
|
46
|
-
|
41
|
+
expect do
|
42
|
+
value = type.example(nil, options: { max: max })
|
43
|
+
expect(value).to be_a(::Integer)
|
44
|
+
end.to raise_error(Attributor::AttributorException, "Invalid range: [, #{max.inspect}]")
|
47
45
|
end
|
48
46
|
end
|
49
47
|
end
|
@@ -51,13 +49,13 @@ describe Attributor::Integer do
|
|
51
49
|
|
52
50
|
context 'when :max is unspecified' do
|
53
51
|
context 'valid cases' do
|
54
|
-
[1, -
|
52
|
+
[1, -100_000_000_000_000_000_000, 100_000_000_000_000_000_000].each do |min|
|
55
53
|
it "returns an Integer in the range [#{min.inspect},]" do
|
56
54
|
20.times do
|
57
|
-
value = type.example(nil, options: {min: min})
|
58
|
-
value.
|
59
|
-
value.
|
60
|
-
value.
|
55
|
+
value = type.example(nil, options: { min: min })
|
56
|
+
expect(value).to be_a(::Integer)
|
57
|
+
expect(value).to be <= min + Attributor::Integer::EXAMPLE_RANGE
|
58
|
+
expect(value).to be >= min
|
61
59
|
end
|
62
60
|
end
|
63
61
|
end
|
@@ -66,10 +64,10 @@ describe Attributor::Integer do
|
|
66
64
|
context 'invalid cases' do
|
67
65
|
['invalid', false].each do |min|
|
68
66
|
it "raises for the invalid range [#{min.inspect},]" do
|
69
|
-
expect
|
70
|
-
value = type.example(nil, options: {min: min})
|
71
|
-
value.
|
72
|
-
|
67
|
+
expect do
|
68
|
+
value = type.example(nil, options: { min: min })
|
69
|
+
expect(value).to be_a(::Integer)
|
70
|
+
end.to raise_error(Attributor::AttributorException, "Invalid range: [#{min.inspect},]")
|
73
71
|
end
|
74
72
|
end
|
75
73
|
end
|
@@ -78,33 +76,32 @@ describe Attributor::Integer do
|
|
78
76
|
context 'when :min and :max are specified' do
|
79
77
|
context 'valid cases' do
|
80
78
|
[
|
81
|
-
[1,1],
|
82
|
-
[1,5],
|
83
|
-
[-2
|
84
|
-
[-3,2],
|
85
|
-
[-
|
79
|
+
[1, 1],
|
80
|
+
[1, 5],
|
81
|
+
[-2, -2],
|
82
|
+
[-3, 2],
|
83
|
+
[-1_000_000_000_000_000, 1_000_000_000_000_000]
|
86
84
|
].each do |min, max|
|
87
85
|
it "returns an Integer in the range [#{min.inspect},#{max.inspect}]" do
|
88
86
|
20.times do
|
89
|
-
value = type.example(nil, options: {max: max, min: min})
|
90
|
-
value.
|
91
|
-
value.
|
87
|
+
value = type.example(nil, options: { max: max, min: min })
|
88
|
+
expect(value).to be <= max
|
89
|
+
expect(value).to be >= min
|
92
90
|
end
|
93
91
|
end
|
94
92
|
end
|
95
93
|
end
|
96
94
|
|
97
95
|
context 'invalid cases' do
|
98
|
-
[[1
|
96
|
+
[[1, -1], [1, '5'], ['-2', 4], [false, false], [true, true]].each do |min, max|
|
99
97
|
it "raises for the invalid range [#{min.inspect}, #{max.inspect}]" do
|
100
|
-
opts = {options: {max: max, min: min}}
|
101
|
-
expect
|
98
|
+
opts = { options: { max: max, min: min } }
|
99
|
+
expect do
|
102
100
|
type.example(nil, opts)
|
103
|
-
|
101
|
+
end.to raise_error(Attributor::AttributorException, "Invalid range: [#{min.inspect}, #{max.inspect}]")
|
104
102
|
end
|
105
103
|
end
|
106
104
|
end
|
107
|
-
|
108
105
|
end
|
109
106
|
end
|
110
107
|
|
@@ -112,29 +109,26 @@ describe Attributor::Integer do
|
|
112
109
|
let(:value) { nil }
|
113
110
|
|
114
111
|
it 'returns nil for nil' do
|
115
|
-
type.load(nil).
|
112
|
+
expect(type.load(nil)).to be(nil)
|
116
113
|
end
|
117
114
|
|
118
115
|
context 'for incoming integer values' do
|
119
116
|
let(:value) { 1 }
|
120
117
|
|
121
118
|
it 'returns the incoming value' do
|
122
|
-
type.load(value).
|
119
|
+
expect(type.load(value)).to be(value)
|
123
120
|
end
|
124
121
|
end
|
125
122
|
|
126
123
|
context 'for incoming string values' do
|
127
|
-
|
128
|
-
|
129
124
|
context 'that are valid integers' do
|
130
125
|
let(:value) { '1024' }
|
131
126
|
it 'decodes it if the string represents an integer' do
|
132
|
-
type.load(value).
|
127
|
+
expect(type.load(value)).to eq 1024
|
133
128
|
end
|
134
129
|
end
|
135
130
|
|
136
131
|
context 'that are not valid integers' do
|
137
|
-
|
138
132
|
context 'with simple alphanumeric text' do
|
139
133
|
let(:value) { 'not an integer' }
|
140
134
|
|
@@ -149,10 +143,7 @@ describe Attributor::Integer do
|
|
149
143
|
expect { type.load(value) }.to raise_error(/invalid value/)
|
150
144
|
end
|
151
145
|
end
|
152
|
-
|
153
146
|
end
|
154
|
-
|
155
147
|
end
|
156
148
|
end
|
157
149
|
end
|
158
|
-
|
data/spec/types/model_spec.rb
CHANGED
@@ -11,46 +11,52 @@ describe Attributor::Model do
|
|
11
11
|
Class.new(Attributor::Model) do
|
12
12
|
attributes do
|
13
13
|
raise 'sorry :('
|
14
|
-
attribute :name, String
|
15
14
|
end
|
16
15
|
end
|
17
16
|
end
|
18
17
|
|
19
18
|
it 'throws original exception upon first run' do
|
20
|
-
|
19
|
+
expect do
|
21
20
|
broken_model.attributes
|
22
|
-
|
21
|
+
end.to raise_error(RuntimeError, 'sorry :(')
|
23
22
|
end
|
24
23
|
|
25
24
|
it 'throws InvalidDefinition for subsequent access' do
|
26
|
-
|
25
|
+
begin
|
26
|
+
broken_model.attributes
|
27
|
+
rescue
|
28
|
+
nil
|
29
|
+
end
|
27
30
|
|
28
|
-
|
31
|
+
expect do
|
29
32
|
broken_model.attributes
|
30
|
-
|
33
|
+
end.to raise_error(Attributor::InvalidDefinition)
|
31
34
|
end
|
32
35
|
|
33
36
|
it 'throws for any attempts at using of an instance of it' do
|
34
|
-
|
37
|
+
begin
|
38
|
+
broken_model.attributes
|
39
|
+
rescue
|
40
|
+
nil
|
41
|
+
end
|
35
42
|
|
36
43
|
instance = broken_model.new
|
37
|
-
|
44
|
+
expect do
|
38
45
|
instance.name
|
39
|
-
|
46
|
+
end.to raise_error(Attributor::InvalidDefinition)
|
40
47
|
end
|
41
|
-
|
42
48
|
end
|
43
49
|
end
|
44
50
|
|
45
51
|
context 'class methods' do
|
46
|
-
let(:context){
|
52
|
+
let(:context) { %w(root subattr) }
|
47
53
|
|
48
54
|
its(:native_type) { should eq(Chicken) }
|
49
55
|
|
50
56
|
context '.example' do
|
51
57
|
subject(:chicken) { Chicken.example }
|
52
58
|
|
53
|
-
let(:age_opts) { {options: Chicken.attributes[:age].options } }
|
59
|
+
let(:age_opts) { { options: Chicken.attributes[:age].options } }
|
54
60
|
let(:age) { /\d{2}/.gen.to_i }
|
55
61
|
|
56
62
|
context 'for a simple model' do
|
@@ -58,25 +64,25 @@ describe Attributor::Model do
|
|
58
64
|
|
59
65
|
context 'and attribute without :example option' do
|
60
66
|
before do
|
61
|
-
Attributor::Integer.
|
67
|
+
expect(Attributor::Integer).to receive(:example).with(kind_of(Array), age_opts).and_return(age)
|
62
68
|
end
|
63
69
|
|
64
|
-
its(:age) { should
|
70
|
+
its(:age) { should eq age }
|
65
71
|
end
|
66
72
|
|
67
73
|
context 'and attribute with :example options' do
|
68
74
|
before do
|
69
|
-
Attributor::Integer.
|
70
|
-
Attributor::String.
|
75
|
+
expect(Attributor::Integer).not_to receive(:example) # due to lazy-evaluation of examples
|
76
|
+
expect(Attributor::String).not_to receive(:example) # due to the :example option on the attribute
|
71
77
|
end
|
72
|
-
its(:email) { should
|
78
|
+
its(:email) { should match(/\w+@.*\.example\.org/) }
|
73
79
|
end
|
74
80
|
|
75
81
|
context 'with given values' do
|
76
82
|
let(:name) { 'Sir Clucksalot' }
|
77
|
-
subject(:example) { Chicken.example(name: name)}
|
83
|
+
subject(:example) { Chicken.example(name: name) }
|
78
84
|
|
79
|
-
its(:name) {should eq(name) }
|
85
|
+
its(:name) { should eq(name) }
|
80
86
|
end
|
81
87
|
end
|
82
88
|
|
@@ -92,14 +98,13 @@ describe Attributor::Model do
|
|
92
98
|
|
93
99
|
its(:attributes) { should eq(some_chicken.attributes) }
|
94
100
|
end
|
95
|
-
|
96
101
|
end
|
97
102
|
|
98
103
|
context 'with attributes that are also models' do
|
99
104
|
subject(:turducken) { Turducken.example }
|
100
105
|
|
101
106
|
its(:attributes) { should have_key(:chicken) }
|
102
|
-
its(:chicken) { should be_kind_of(Chicken)}
|
107
|
+
its(:chicken) { should be_kind_of(Chicken) }
|
103
108
|
end
|
104
109
|
|
105
110
|
context 'with infinitely-expanding sub-attributes' do
|
@@ -117,20 +122,17 @@ describe Attributor::Model do
|
|
117
122
|
|
118
123
|
it 'terminates example generation at MAX_EXAMPLE_DEPTH' do
|
119
124
|
# call .child on example MAX_EXAMPLE_DEPTH times
|
120
|
-
terminal_child = Attributor::Model::MAX_EXAMPLE_DEPTH.times.inject(example) do |object,
|
125
|
+
terminal_child = Attributor::Model::MAX_EXAMPLE_DEPTH.times.inject(example) do |object, _i|
|
121
126
|
object.child
|
122
127
|
end
|
123
128
|
# after which .child will return nil
|
124
|
-
terminal_child.child.
|
129
|
+
expect(terminal_child.child).to be(nil)
|
125
130
|
# but simple attributes will be generated
|
126
|
-
terminal_child.name.
|
131
|
+
expect(terminal_child.name).not_to be(nil)
|
127
132
|
end
|
128
|
-
|
129
|
-
|
130
133
|
end
|
131
134
|
end
|
132
135
|
|
133
|
-
|
134
136
|
context '.definition' do
|
135
137
|
subject(:definition) { Chicken.definition }
|
136
138
|
|
@@ -141,48 +143,46 @@ describe Attributor::Model do
|
|
141
143
|
end
|
142
144
|
end
|
143
145
|
|
144
|
-
|
145
146
|
context '.load' do
|
146
147
|
let(:age) { 1 }
|
147
|
-
let(:email) {
|
148
|
-
let(:hash) { {:
|
148
|
+
let(:email) { 'cluck@example.org' }
|
149
|
+
let(:hash) { { age: age, email: email } }
|
149
150
|
|
150
151
|
subject(:model) { Chicken.load(hash) }
|
151
152
|
|
152
153
|
context 'with an instance of the model' do
|
153
154
|
it 'returns the instance' do
|
154
|
-
Chicken.load(model).
|
155
|
+
expect(Chicken.load(model)).to be(model)
|
155
156
|
end
|
156
157
|
end
|
157
158
|
|
158
159
|
context 'with a nil value' do
|
159
160
|
it 'returns nil' do
|
160
|
-
Chicken.load(nil).
|
161
|
+
expect(Chicken.load(nil)).to be_nil
|
161
162
|
end
|
162
163
|
|
163
164
|
context 'with recurse: true' do
|
164
165
|
subject(:turducken) { Turducken.load(nil, [], recurse: true) }
|
165
166
|
|
166
167
|
it 'loads with default values' do
|
167
|
-
turducken.name.
|
168
|
-
turducken.chicken.age.
|
168
|
+
expect(turducken.name).to eq('Turkey McDucken')
|
169
|
+
expect(turducken.chicken.age).to be(1)
|
169
170
|
end
|
170
|
-
|
171
171
|
end
|
172
172
|
end
|
173
173
|
|
174
174
|
context 'with a JSON-serialized hash' do
|
175
|
-
let(:context){
|
176
|
-
let(:expected_hash) { {
|
175
|
+
let(:context) { %w(root subattr) }
|
176
|
+
let(:expected_hash) { { 'age' => age, 'email' => email } }
|
177
177
|
let(:json) { hash.to_json }
|
178
178
|
before do
|
179
|
-
Chicken.
|
180
|
-
with(expected_hash,context, recurse: false)
|
181
|
-
JSON.
|
179
|
+
expect(Chicken).to receive(:from_hash)
|
180
|
+
.with(expected_hash, context, recurse: false)
|
181
|
+
expect(JSON).to receive(:parse).with(json).and_call_original
|
182
182
|
end
|
183
183
|
|
184
184
|
it 'deserializes and calls from_hash' do
|
185
|
-
Chicken.load(json,context)
|
185
|
+
Chicken.load(json, context)
|
186
186
|
end
|
187
187
|
end
|
188
188
|
|
@@ -190,36 +190,37 @@ describe Attributor::Model do
|
|
190
190
|
let(:json) { "{'invalid'}" }
|
191
191
|
|
192
192
|
it 'catches the error and reports it correctly' do
|
193
|
-
JSON.
|
194
|
-
expect
|
195
|
-
Chicken.load(json,context)
|
196
|
-
|
193
|
+
expect(JSON).to receive(:parse).with(json).and_call_original
|
194
|
+
expect do
|
195
|
+
Chicken.load(json, context)
|
196
|
+
end.to raise_error(Attributor::DeserializationError,
|
197
|
+
/Error deserializing a String using JSON.*#{context.join('.')}/)
|
197
198
|
end
|
198
199
|
end
|
199
200
|
|
200
|
-
|
201
201
|
context 'with an invalid object type' do
|
202
202
|
it 'raises some sort of error' do
|
203
|
-
expect
|
203
|
+
expect do
|
204
204
|
Chicken.load(Object.new, context)
|
205
|
-
|
205
|
+
end.to raise_error(Attributor::IncompatibleTypeError,
|
206
|
+
/Type Chicken cannot load values of type Object.*#{context.join('.')}/)
|
206
207
|
end
|
207
208
|
end
|
208
209
|
|
209
210
|
context 'with an instance of different model' do
|
210
211
|
it 'raises some sort of error' do
|
211
|
-
expect
|
212
|
+
expect do
|
212
213
|
turducken = Turducken.example
|
213
|
-
|
214
|
-
|
214
|
+
Chicken.load(turducken, context)
|
215
|
+
end.to raise_error(Attributor::AttributorException, /Unknown key received/)
|
215
216
|
end
|
216
217
|
end
|
217
218
|
|
218
|
-
context
|
219
|
+
context 'with a hash' do
|
219
220
|
context 'for a complete set of attributes' do
|
220
221
|
it 'loads the given attributes' do
|
221
|
-
model.age.
|
222
|
-
model.email.
|
222
|
+
expect(model.age).to eq age
|
223
|
+
expect(model.email).to eq email
|
223
224
|
end
|
224
225
|
end
|
225
226
|
|
@@ -227,52 +228,47 @@ describe Attributor::Model do
|
|
227
228
|
let(:hash) { Hash.new }
|
228
229
|
|
229
230
|
it 'sets the defaults' do
|
230
|
-
model.age.
|
231
|
-
model.email.
|
231
|
+
expect(model.age).to eq 1
|
232
|
+
expect(model.email).to be nil
|
232
233
|
end
|
233
234
|
end
|
234
235
|
|
235
236
|
context 'for a superset of attributes' do
|
236
|
-
let(:hash) { {
|
237
|
+
let(:hash) { { 'invalid_attribute' => 'value' } }
|
237
238
|
|
238
239
|
it 'raises an error' do
|
239
|
-
expect
|
240
|
+
expect do
|
240
241
|
Chicken.load(hash, context)
|
241
|
-
|
242
|
-
#raise_error(Attributor::AttributorException, /Unknown attributes.*#{context.join('.')}/)
|
242
|
+
end.to raise_error(Attributor::AttributorException, /Unknown key received/)
|
243
|
+
# raise_error(Attributor::AttributorException, /Unknown attributes.*#{context.join('.')}/)
|
243
244
|
end
|
244
245
|
end
|
245
246
|
|
246
247
|
context 'loading with default values' do
|
247
248
|
let(:reference) { Post }
|
248
|
-
let(:options) { {reference: reference} }
|
249
|
+
let(:options) { { reference: reference } }
|
249
250
|
|
250
251
|
let(:attribute_definition) do
|
251
252
|
proc do
|
252
253
|
attribute :title
|
253
|
-
attribute :tags, default:
|
254
|
+
attribute :tags, default: %w(stuff things)
|
254
255
|
end
|
255
256
|
end
|
256
257
|
|
257
|
-
let(:struct) { Attributor::Struct.construct(attribute_definition, options)}
|
258
|
-
|
259
|
-
let(:data) { {title: 'my post'} }
|
258
|
+
let(:struct) { Attributor::Struct.construct(attribute_definition, options) }
|
260
259
|
|
261
|
-
|
260
|
+
let(:data) { { title: 'my post' } }
|
262
261
|
|
262
|
+
subject(:loaded) { struct.load(data) }
|
263
263
|
|
264
264
|
it 'validates' do
|
265
265
|
expect(loaded.validate).to be_empty
|
266
266
|
end
|
267
|
-
|
268
267
|
end
|
269
268
|
end
|
270
|
-
|
271
269
|
end
|
272
|
-
|
273
270
|
end
|
274
271
|
|
275
|
-
|
276
272
|
context 'instance methods' do
|
277
273
|
subject(:chicken) { Chicken.new }
|
278
274
|
|
@@ -283,36 +279,39 @@ describe Attributor::Model do
|
|
283
279
|
end
|
284
280
|
|
285
281
|
context 'initialize' do
|
286
|
-
|
287
282
|
subject(:chicken) { Chicken.new(attributes_data) }
|
288
283
|
context 'supports passing an initial hash object for attribute values' do
|
289
|
-
let(:attributes_data){ {age: '1', email:'rooster@coup.com'} }
|
284
|
+
let(:attributes_data) { { age: '1', email: 'rooster@coup.com' } }
|
290
285
|
it 'and sets them in loaded format onto the instance attributes' do
|
291
|
-
Chicken.
|
286
|
+
expect(Chicken).to receive(:load).with(attributes_data).and_call_original
|
292
287
|
attributes_data.keys.each do |attr_name|
|
293
|
-
Chicken.attributes[attr_name].
|
288
|
+
expect(Chicken.attributes[attr_name]).to receive(:load)
|
289
|
+
.with(attributes_data[attr_name], instance_of(Array), recurse: false)
|
290
|
+
.and_call_original
|
294
291
|
end
|
295
|
-
subject.age.
|
296
|
-
subject.email.
|
292
|
+
expect(subject.age).to be(1)
|
293
|
+
expect(subject.email).to be(attributes_data[:email])
|
297
294
|
end
|
298
295
|
end
|
299
296
|
context 'supports passing a JSON encoded data object' do
|
300
|
-
let(:attributes_hash){ {age: 1, email:'rooster@coup.com'} }
|
301
|
-
let(:attributes_data){ JSON.dump(attributes_hash) }
|
297
|
+
let(:attributes_hash) { { age: 1, email: 'rooster@coup.com' } }
|
298
|
+
let(:attributes_data) { JSON.dump(attributes_hash) }
|
302
299
|
it 'and sets them in loaded format onto the instance attributes' do
|
303
|
-
Chicken.
|
300
|
+
expect(Chicken).to receive(:load).with(attributes_data).and_call_original
|
304
301
|
attributes_hash.keys.each do |attr_name|
|
305
|
-
Chicken.attributes[attr_name].
|
302
|
+
expect(Chicken.attributes[attr_name]).to receive(:load)
|
303
|
+
.with(attributes_hash[attr_name], instance_of(Array), recurse: false)
|
304
|
+
.and_call_original
|
306
305
|
end
|
307
|
-
subject.age.
|
308
|
-
subject.email.
|
306
|
+
expect(subject.age).to be(1)
|
307
|
+
expect(subject.email).to eq attributes_hash[:email]
|
309
308
|
end
|
310
309
|
end
|
311
310
|
context 'supports passing a native model for the data object' do
|
312
|
-
let(:attributes_data){ Chicken.example }
|
311
|
+
let(:attributes_data) { Chicken.example }
|
313
312
|
it 'sets a new instance pointing to the exact same attributes (careful about modifications!)' do
|
314
313
|
attributes_data.attributes.each do |attr_name, attr_value|
|
315
|
-
subject.send(attr_name).
|
314
|
+
expect(subject.send(attr_name)).to be(attr_value)
|
316
315
|
end
|
317
316
|
end
|
318
317
|
end
|
@@ -323,48 +322,45 @@ describe Attributor::Model do
|
|
323
322
|
let(:age) { 1 }
|
324
323
|
it 'gets and sets attributes' do
|
325
324
|
chicken.age = age
|
326
|
-
chicken.age.
|
325
|
+
expect(chicken.age).to eq age
|
327
326
|
end
|
328
327
|
end
|
329
328
|
|
330
329
|
context 'setting nil' do
|
331
330
|
it 'assigns the default value if there is one' do
|
332
331
|
chicken.age = nil
|
333
|
-
chicken.age.
|
332
|
+
expect(chicken.age).to eq 1
|
334
333
|
end
|
335
334
|
|
336
335
|
it 'sets the value to nil if there is no default' do
|
337
336
|
chicken.email = nil
|
338
|
-
chicken.email.
|
337
|
+
expect(chicken.email).to be nil
|
339
338
|
end
|
340
|
-
|
341
339
|
end
|
342
340
|
|
343
341
|
context 'for unknown attributes' do
|
344
342
|
it 'raises an exception' do
|
345
|
-
expect
|
346
|
-
chicken.invalid_attribute =
|
347
|
-
|
343
|
+
expect do
|
344
|
+
chicken.invalid_attribute = 'value'
|
345
|
+
end.to raise_error(NoMethodError, /undefined method/)
|
348
346
|
end
|
349
347
|
end
|
350
348
|
|
351
349
|
context 'for false attributes' do
|
352
350
|
subject(:person) { Person.example(okay: false) }
|
353
351
|
it 'properly memoizes the value' do
|
354
|
-
person.okay.
|
355
|
-
person.okay.
|
352
|
+
expect(person.okay).to be(false)
|
353
|
+
expect(person.okay).to be(false) # second call to ensure we hit the memoized value
|
356
354
|
end
|
357
355
|
end
|
358
356
|
end
|
359
|
-
|
360
357
|
end
|
361
358
|
|
362
|
-
|
363
359
|
context 'validation' do
|
364
360
|
context 'for simple models' do
|
365
361
|
context 'that are valid' do
|
366
|
-
subject(:chicken)
|
367
|
-
its(:validate) { should be_empty}
|
362
|
+
subject(:chicken) { Chicken.example }
|
363
|
+
its(:validate) { should be_empty }
|
368
364
|
end
|
369
365
|
context 'that are invalid' do
|
370
366
|
subject(:chicken) { Chicken.example(age: 150) }
|
@@ -375,17 +371,17 @@ describe Attributor::Model do
|
|
375
371
|
context 'for models using the "requires" DSL' do
|
376
372
|
subject(:address) { Address.load(state: 'CA') }
|
377
373
|
its(:validate) { should_not be_empty }
|
378
|
-
its(:validate) { should include
|
374
|
+
its(:validate) { should include 'Key name is required for $.' }
|
379
375
|
end
|
380
376
|
context 'for models with circular sub-attributes' do
|
381
377
|
context 'that are valid' do
|
382
378
|
subject(:person) { Person.example }
|
383
|
-
its(:validate) { should be_empty}
|
379
|
+
its(:validate) { should be_empty }
|
384
380
|
end
|
385
381
|
|
386
382
|
context 'that are both invalid' do
|
387
|
-
subject(:person){ Person.load(
|
388
|
-
let(:address){ Address.load(
|
383
|
+
subject(:person) { Person.load(name: 'Joe', title: 'dude', okay: true) }
|
384
|
+
let(:address) { Address.load(name: '1 Main St', state: 'ME') }
|
389
385
|
before do
|
390
386
|
person.address = address
|
391
387
|
address.person = person
|
@@ -395,35 +391,29 @@ describe Attributor::Model do
|
|
395
391
|
|
396
392
|
it 'recursively-validates sub-attributes with the right context' do
|
397
393
|
title_error, state_error = person.validate('person')
|
398
|
-
title_error.
|
399
|
-
state_error.
|
394
|
+
expect(title_error).to match(/^Attribute person\.title:/)
|
395
|
+
expect(state_error).to match(/^Attribute person\.address\.state:/)
|
400
396
|
end
|
401
397
|
end
|
402
|
-
|
403
398
|
end
|
404
399
|
end
|
405
400
|
|
406
|
-
|
407
401
|
context '#dump' do
|
408
|
-
|
409
|
-
|
410
402
|
context 'with circular references' do
|
411
403
|
subject(:person) { Person.example }
|
412
404
|
let(:output) { person.dump }
|
413
405
|
|
414
406
|
it 'terminates' do
|
415
|
-
expect
|
407
|
+
expect do
|
416
408
|
Person.example.dump
|
417
|
-
|
409
|
+
end.to_not raise_error
|
418
410
|
end
|
419
411
|
|
420
412
|
it 'outputs "..." for circular references' do
|
421
|
-
person.address.person.
|
422
|
-
output[:address][:person].
|
413
|
+
expect(person.address.person).to be(person)
|
414
|
+
expect(output[:address][:person]).to eq(Attributor::Model::CIRCULAR_REFERENCE_MARKER)
|
423
415
|
end
|
424
|
-
|
425
416
|
end
|
426
|
-
|
427
417
|
end
|
428
418
|
|
429
419
|
context 'extending' do
|
@@ -446,9 +436,8 @@ describe Attributor::Model do
|
|
446
436
|
end
|
447
437
|
|
448
438
|
it 'adds the attribute' do
|
449
|
-
model.attributes.keys.
|
439
|
+
expect(model.attributes.keys).to match_array [:id, :name, :timestamps]
|
450
440
|
end
|
451
|
-
|
452
441
|
end
|
453
442
|
|
454
443
|
context 'adding to an inner-Struct' do
|
@@ -461,7 +450,7 @@ describe Attributor::Model do
|
|
461
450
|
end
|
462
451
|
|
463
452
|
it 'merges with sub-attributes' do
|
464
|
-
model.attributes[:timestamps].attributes.keys.
|
453
|
+
expect(model.attributes[:timestamps].attributes.keys).to match_array [:created_at, :updated_at]
|
465
454
|
end
|
466
455
|
end
|
467
456
|
|
@@ -477,12 +466,12 @@ describe Attributor::Model do
|
|
477
466
|
subject(:struct) { Attributor::Struct.construct(attributes_block, reference: Cormorant) }
|
478
467
|
|
479
468
|
it 'supports defining sub-attributes using the proper reference' do
|
480
|
-
struct.attributes[:neighbors].options[:required].
|
481
|
-
struct.attributes[:neighbors].type.member_attribute.type.attributes.keys.
|
469
|
+
expect(struct.attributes[:neighbors].options[:required]).to be true
|
470
|
+
expect(struct.attributes[:neighbors].type.member_attribute.type.attributes.keys).to match_array [:name, :age]
|
482
471
|
|
483
472
|
name_options = struct.attributes[:neighbors].type.member_attribute.type.attributes[:name].options
|
484
|
-
name_options[:required].
|
485
|
-
name_options[:description].
|
473
|
+
expect(name_options[:required]).to be true
|
474
|
+
expect(name_options[:description]).to eq 'Name of the Cormorant'
|
486
475
|
end
|
487
476
|
end
|
488
477
|
|
@@ -495,14 +484,10 @@ describe Attributor::Model do
|
|
495
484
|
end
|
496
485
|
|
497
486
|
it 'updates the type properly' do
|
498
|
-
model.attributes[:id].type.
|
487
|
+
expect(model.attributes[:id].type).to be(Attributor::String)
|
499
488
|
end
|
500
|
-
|
501
489
|
end
|
502
|
-
|
503
|
-
|
504
490
|
end
|
505
|
-
|
506
491
|
end
|
507
492
|
|
508
493
|
context 'with no defined attributes' do
|
@@ -518,9 +503,7 @@ describe Attributor::Model do
|
|
518
503
|
its(:attributes) { should be_empty }
|
519
504
|
|
520
505
|
it 'dumps as an empty hash' do
|
521
|
-
example.dump.
|
506
|
+
expect(example.dump).to eq({})
|
522
507
|
end
|
523
|
-
|
524
508
|
end
|
525
|
-
|
526
509
|
end
|