active_attr 0.5.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of active_attr might be problematic. Click here for more details.
- data/.rvmrc +1 -1
- data/CHANGELOG.md +12 -0
- data/Gemfile +4 -3
- data/LICENSE +22 -0
- data/Rakefile +2 -0
- data/active_attr.gemspec +20 -23
- data/gemfiles/rails_3_0.gemfile +1 -0
- data/gemfiles/rails_3_1.gemfile +1 -0
- data/lib/active_attr.rb +1 -0
- data/lib/active_attr/attribute_definition.rb +16 -0
- data/lib/active_attr/attributes.rb +63 -17
- data/lib/active_attr/matchers/have_attribute_matcher.rb +39 -67
- data/lib/active_attr/typecasted_attributes.rb +11 -2
- data/lib/active_attr/typecasting.rb +21 -29
- data/lib/active_attr/typecasting/unknown_typecaster_error.rb +13 -0
- data/lib/active_attr/version.rb +1 -1
- data/spec/functional/active_attr/attribute_defaults_spec.rb +9 -1
- data/spec/functional/active_attr/attributes_spec.rb +72 -24
- data/spec/functional/active_attr/typecasted_attributes_spec.rb +22 -0
- data/spec/support/age.rb +17 -0
- data/spec/unit/active_attr/attribute_definition_spec.rb +24 -0
- data/spec/unit/active_attr/attributes_spec.rb +73 -37
- data/spec/unit/active_attr/typecasting/unknown_typecaster_error_spec.rb +11 -0
- data/spec/unit/active_attr/typecasting_spec.rb +22 -50
- metadata +74 -23
- data/MIT-LICENSE +0 -18
@@ -55,7 +55,7 @@ module ActiveAttr
|
|
55
55
|
#
|
56
56
|
# @since 0.5.0
|
57
57
|
def attribute(name)
|
58
|
-
typecast_attribute(
|
58
|
+
typecast_attribute(_attribute_typecaster(name), super)
|
59
59
|
end
|
60
60
|
|
61
61
|
# Calculates an attribute type
|
@@ -66,6 +66,15 @@ module ActiveAttr
|
|
66
66
|
self.class._attribute_type(attribute_name)
|
67
67
|
end
|
68
68
|
|
69
|
+
# Resolve an attribute typecaster
|
70
|
+
#
|
71
|
+
# @private
|
72
|
+
# @since 0.6.0
|
73
|
+
def _attribute_typecaster(attribute_name)
|
74
|
+
type = _attribute_type(attribute_name)
|
75
|
+
self.class.attributes[attribute_name][:typecaster] || typecaster_for(type) or raise UnknownTypecasterError, "Unable to cast to type #{type}"
|
76
|
+
end
|
77
|
+
|
69
78
|
module ClassMethods
|
70
79
|
# Returns the class name plus its attribute names and types
|
71
80
|
#
|
@@ -78,7 +87,7 @@ module ActiveAttr
|
|
78
87
|
def inspect
|
79
88
|
inspected_attributes = attribute_names.sort.map { |name| "#{name}: #{_attribute_type(name)}" }
|
80
89
|
attributes_list = "(#{inspected_attributes.join(", ")})" unless inspected_attributes.empty?
|
81
|
-
"#{
|
90
|
+
"#{name}#{attributes_list}"
|
82
91
|
end
|
83
92
|
|
84
93
|
# Calculates an attribute type
|
@@ -7,7 +7,7 @@ require "active_attr/typecasting/float_typecaster"
|
|
7
7
|
require "active_attr/typecasting/integer_typecaster"
|
8
8
|
require "active_attr/typecasting/object_typecaster"
|
9
9
|
require "active_attr/typecasting/string_typecaster"
|
10
|
-
require "
|
10
|
+
require "active_attr/typecasting/unknown_typecaster_error"
|
11
11
|
|
12
12
|
module ActiveAttr
|
13
13
|
# Typecasting provides methods to typecast a value to a different type
|
@@ -24,49 +24,41 @@ module ActiveAttr
|
|
24
24
|
#
|
25
25
|
# @since 0.5.0
|
26
26
|
module Typecasting
|
27
|
-
extend ActiveSupport::Concern
|
28
|
-
|
29
|
-
# @private
|
30
|
-
TYPECASTERS = {
|
31
|
-
BigDecimal => BigDecimalTypecaster,
|
32
|
-
Boolean => BooleanTypecaster,
|
33
|
-
Date => DateTypecaster,
|
34
|
-
DateTime => DateTimeTypecaster,
|
35
|
-
Float => FloatTypecaster,
|
36
|
-
Integer => IntegerTypecaster,
|
37
|
-
Object => ObjectTypecaster,
|
38
|
-
String => StringTypecaster,
|
39
|
-
}
|
40
|
-
|
41
27
|
# Typecasts a value using a Class
|
42
28
|
#
|
43
|
-
# @param [
|
29
|
+
# @param [#call] typecaster The typecaster to use for typecasting
|
44
30
|
# @param [Object] value The value to be typecasted
|
45
31
|
#
|
46
32
|
# @return [Object, nil] The typecasted value or nil if it cannot be
|
47
33
|
# typecasted
|
48
34
|
#
|
49
35
|
# @since 0.5.0
|
50
|
-
def typecast_attribute(
|
51
|
-
raise ArgumentError, "a
|
36
|
+
def typecast_attribute(typecaster, value)
|
37
|
+
raise ArgumentError, "a typecaster must be given" unless typecaster.respond_to?(:call)
|
52
38
|
return value if value.nil?
|
53
|
-
|
39
|
+
typecaster.call(value)
|
54
40
|
end
|
55
41
|
|
56
|
-
#
|
57
|
-
# in TYPECASTING_METHODS
|
42
|
+
# Resolve a Class to a typecaster
|
58
43
|
#
|
59
44
|
# @param [Class] type The type to cast to
|
60
|
-
# @param [Object] value The value to be typecasted
|
61
45
|
#
|
62
|
-
# @return [
|
63
|
-
# TYPECASTING_METHODS, nil if no method is found
|
46
|
+
# @return [#call, nil] The typecaster to use
|
64
47
|
#
|
65
|
-
# @since 0.
|
66
|
-
def
|
67
|
-
|
68
|
-
|
69
|
-
|
48
|
+
# @since 0.6.0
|
49
|
+
def typecaster_for(type)
|
50
|
+
typecaster = {
|
51
|
+
BigDecimal => BigDecimalTypecaster,
|
52
|
+
Boolean => BooleanTypecaster,
|
53
|
+
Date => DateTypecaster,
|
54
|
+
DateTime => DateTimeTypecaster,
|
55
|
+
Float => FloatTypecaster,
|
56
|
+
Integer => IntegerTypecaster,
|
57
|
+
Object => ObjectTypecaster,
|
58
|
+
String => StringTypecaster,
|
59
|
+
}[type]
|
60
|
+
|
61
|
+
typecaster.new if typecaster
|
70
62
|
end
|
71
63
|
end
|
72
64
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "active_attr/error"
|
2
|
+
|
3
|
+
module ActiveAttr
|
4
|
+
module Typecasting
|
5
|
+
# This exception is raised if attempting to cast to an unknown type when
|
6
|
+
# using {Typecasting}
|
7
|
+
#
|
8
|
+
# @since 0.6.0
|
9
|
+
class UnknownTypecasterError < TypeError
|
10
|
+
include Error
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/active_attr/version.rb
CHANGED
@@ -58,6 +58,14 @@ module ActiveAttr
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
+
context "an attribute with a default empty Array" do
|
62
|
+
before { model_class.attribute :roles, :default => [] }
|
63
|
+
|
64
|
+
it "the attribute getter returns an empty array by default" do
|
65
|
+
subject.roles.should == []
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
61
69
|
context "an attribute with a dynamic Time.now default" do
|
62
70
|
before { model_class.attribute :created_at, :default => lambda { Time.now } }
|
63
71
|
|
@@ -70,7 +78,7 @@ module ActiveAttr
|
|
70
78
|
end
|
71
79
|
|
72
80
|
it "the attribute default is different per instance" do
|
73
|
-
model_class.new.created_at.should_not
|
81
|
+
model_class.new.created_at.should_not equal model_class.new.created_at
|
74
82
|
end
|
75
83
|
end
|
76
84
|
|
@@ -113,6 +113,7 @@ module ActiveAttr
|
|
113
113
|
include ActiveModel::Serializers::Xml
|
114
114
|
end
|
115
115
|
|
116
|
+
self.include_root_in_json = true
|
116
117
|
attribute :first_name
|
117
118
|
attribute :last_name
|
118
119
|
|
@@ -160,9 +161,11 @@ module ActiveAttr
|
|
160
161
|
before do
|
161
162
|
Object.const_set("Person", model_class)
|
162
163
|
|
163
|
-
|
164
|
-
|
165
|
-
|
164
|
+
FactoryGirl.define do
|
165
|
+
factory :person, :class => :person do
|
166
|
+
first_name "Chris"
|
167
|
+
last_name "Griego"
|
168
|
+
end
|
166
169
|
end
|
167
170
|
end
|
168
171
|
|
@@ -190,41 +193,86 @@ module ActiveAttr
|
|
190
193
|
end
|
191
194
|
|
192
195
|
context "defining dangerous attributes" do
|
193
|
-
shared_examples "
|
194
|
-
it "
|
195
|
-
|
196
|
+
shared_examples "a dangerous attribute" do
|
197
|
+
it ".dangerous_attribute? is true" do
|
198
|
+
model_class.dangerous_attribute?(attribute_name).should be_true
|
199
|
+
end
|
200
|
+
|
201
|
+
it ".attribute raises DangerousAttributeError" do
|
202
|
+
expect { model_class.attribute(attribute_name) }.to raise_error DangerousAttributeError, %{an attribute method named "#{attribute_name}" would conflict with an existing method}
|
203
|
+
end
|
204
|
+
|
205
|
+
it ".attribute! does not raise" do
|
206
|
+
expect { model_class.attribute!(attribute_name) }.not_to raise_error
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
shared_examples "a whitelisted attribute" do
|
211
|
+
it ".dangerous_attribute? is false" do
|
212
|
+
model_class.dangerous_attribute?(attribute_name).should be_false
|
213
|
+
end
|
214
|
+
|
215
|
+
it ".attribute does not raise" do
|
216
|
+
expect { model_class.attribute(attribute_name) }.not_to raise_error
|
217
|
+
end
|
218
|
+
|
219
|
+
it ".attribute! does not raise" do
|
220
|
+
expect { model_class.attribute!(attribute_name) }.not_to raise_error
|
221
|
+
end
|
222
|
+
|
223
|
+
it "can be set and get" do
|
224
|
+
model_class.attribute attribute_name
|
225
|
+
model = model_class.new
|
226
|
+
value = mock
|
227
|
+
model.send "#{attribute_name}=", value
|
228
|
+
model.send(attribute_name).should equal value
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
shared_examples "defining dangerous attributes" do
|
233
|
+
context "an attribute that conflicts with #{described_class}" do
|
234
|
+
let(:attribute_name) { :write_attribute }
|
235
|
+
include_examples "a dangerous attribute"
|
196
236
|
end
|
197
237
|
|
198
|
-
|
199
|
-
|
238
|
+
context "an attribute that conflicts with ActiveModel::AttributeMethods" do
|
239
|
+
let(:attribute_name) { :inspect }
|
240
|
+
include_examples "a dangerous attribute"
|
200
241
|
end
|
201
242
|
|
202
|
-
|
203
|
-
|
243
|
+
context "an attribute that conflicts with Kernel" do
|
244
|
+
let(:attribute_name) { :puts }
|
245
|
+
include_examples "a dangerous attribute"
|
204
246
|
end
|
205
247
|
|
206
|
-
|
207
|
-
|
248
|
+
context "an attribute that conflicts with Object" do
|
249
|
+
let(:attribute_name) { :class }
|
250
|
+
include_examples "a dangerous attribute"
|
208
251
|
end
|
209
252
|
|
210
|
-
|
211
|
-
|
253
|
+
context "an attribute that conflicts with BasicObject" do
|
254
|
+
let(:attribute_name) { :instance_eval }
|
255
|
+
include_examples "a dangerous attribute"
|
212
256
|
end
|
213
257
|
|
214
|
-
|
215
|
-
|
258
|
+
context "an attribute that conflicts with a properly implemented method_missing callback" do
|
259
|
+
let(:attribute_name) { :my_proper_missing_method }
|
260
|
+
include_examples "a dangerous attribute"
|
216
261
|
end
|
217
262
|
|
218
|
-
|
219
|
-
|
263
|
+
context "an attribute that conflicts with a less properly implemented method_missing callback" do
|
264
|
+
let(:attribute_name) { :my_less_proper_missing_method }
|
265
|
+
include_examples "a dangerous attribute"
|
220
266
|
end
|
221
267
|
|
222
|
-
|
223
|
-
|
268
|
+
context "an :id attribute" do
|
269
|
+
let(:attribute_name) { :id }
|
270
|
+
include_examples "a whitelisted attribute"
|
224
271
|
end
|
225
272
|
|
226
|
-
|
227
|
-
|
273
|
+
context "a :type attribute" do
|
274
|
+
let(:attribute_name) { :type }
|
275
|
+
include_examples "a whitelisted attribute"
|
228
276
|
end
|
229
277
|
end
|
230
278
|
|
@@ -250,12 +298,12 @@ module ActiveAttr
|
|
250
298
|
|
251
299
|
context "on a model class" do
|
252
300
|
let(:model_class) { dangerous_model_class }
|
253
|
-
include_examples "defining
|
301
|
+
include_examples "defining dangerous attributes"
|
254
302
|
end
|
255
303
|
|
256
304
|
context "on a child class" do
|
257
305
|
let(:model_class) { Class.new(dangerous_model_class) }
|
258
|
-
include_examples "defining
|
306
|
+
include_examples "defining dangerous attributes"
|
259
307
|
end
|
260
308
|
end
|
261
309
|
end
|
@@ -10,6 +10,7 @@ module ActiveAttr
|
|
10
10
|
include TypecastedAttributes
|
11
11
|
|
12
12
|
attribute :typeless
|
13
|
+
attribute :age, :type => Age, :typecaster => lambda { |value| Age.new(value) }
|
13
14
|
attribute :object, :type => Object
|
14
15
|
attribute :big_decimal, :type => BigDecimal
|
15
16
|
attribute :boolean, :type => Typecasting::Boolean
|
@@ -18,6 +19,12 @@ module ActiveAttr
|
|
18
19
|
attribute :float, :type => Float
|
19
20
|
attribute :integer, :type => Integer
|
20
21
|
attribute :string, :type => String
|
22
|
+
|
23
|
+
attribute :unknown, :type => Class.new {
|
24
|
+
def self.to_s
|
25
|
+
"Unknown"
|
26
|
+
end
|
27
|
+
}
|
21
28
|
end
|
22
29
|
end
|
23
30
|
|
@@ -27,6 +34,16 @@ module ActiveAttr
|
|
27
34
|
subject.typeless.should be_nil
|
28
35
|
end
|
29
36
|
|
37
|
+
it "an attribute with no known typecaster raises" do
|
38
|
+
subject.unknown = nil
|
39
|
+
expect { subject.unknown }.to raise_error Typecasting::UnknownTypecasterError, "Unable to cast to type Unknown"
|
40
|
+
end
|
41
|
+
|
42
|
+
it "an attribute with an inline typecaster returns nil" do
|
43
|
+
subject.age = nil
|
44
|
+
subject.age.should be_nil
|
45
|
+
end
|
46
|
+
|
30
47
|
it "an Object attribute returns nil" do
|
31
48
|
subject.object = nil
|
32
49
|
subject.object.should be_nil
|
@@ -121,6 +138,11 @@ module ActiveAttr
|
|
121
138
|
subject.string = "1.0"
|
122
139
|
subject.string.should eql "1.0"
|
123
140
|
end
|
141
|
+
|
142
|
+
it "an attribute using an inline typecaster returns the result of the inline typecaster" do
|
143
|
+
subject.age = 2
|
144
|
+
subject.age.should == Age.new(2)
|
145
|
+
end
|
124
146
|
end
|
125
147
|
end
|
126
148
|
end
|
data/spec/support/age.rb
ADDED
@@ -61,6 +61,30 @@ module ActiveAttr
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
+
describe "#inspect" do
|
65
|
+
it "generates attribute definition code for an attribute without options" do
|
66
|
+
described_class.new(:first_name).inspect.should == %{attribute :first_name}
|
67
|
+
end
|
68
|
+
|
69
|
+
it "generates attribute definition code for an attribute with a single option" do
|
70
|
+
described_class.new(:first_name, :type => String).inspect.should == %{attribute :first_name, :type => String}
|
71
|
+
end
|
72
|
+
|
73
|
+
it "generates attribute definition code for an attribute with a single option, inspecting the option value" do
|
74
|
+
described_class.new(:first_name, :default => "John").inspect.should == %{attribute :first_name, :default => "John"}
|
75
|
+
end
|
76
|
+
|
77
|
+
it "generates attribute definition code for an attribute with multiple options sorted alphabetically" do
|
78
|
+
expected = %{attribute :first_name, :default => "John", :type => String}
|
79
|
+
described_class.new(:first_name, :default => "John", :type => String).inspect.should eq expected
|
80
|
+
described_class.new(:first_name, :type => String, :default => "John").inspect.should == expected
|
81
|
+
end
|
82
|
+
|
83
|
+
it "generate attribute definition code for an attribute with a string option key" do
|
84
|
+
described_class.new(:first_name, "foo" => "bar").inspect.should == %{attribute :first_name, "foo" => "bar"}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
64
88
|
describe "#name" do
|
65
89
|
it { should respond_to(:name) }
|
66
90
|
end
|
@@ -41,61 +41,97 @@ module ActiveAttr
|
|
41
41
|
end
|
42
42
|
|
43
43
|
let :attributeless do
|
44
|
-
Class.new do
|
45
|
-
|
44
|
+
Class.new.tap do |attributeless|
|
45
|
+
attributeless.class_eval do
|
46
|
+
include Attributes
|
46
47
|
|
47
|
-
|
48
|
-
|
48
|
+
def self.name
|
49
|
+
"Foo"
|
50
|
+
end
|
49
51
|
end
|
50
52
|
end
|
51
53
|
end
|
52
54
|
|
53
55
|
describe ".attribute" do
|
54
|
-
|
55
|
-
model_class.
|
56
|
-
end
|
56
|
+
context "a dangerous attribute" do
|
57
|
+
before { model_class.stub(:dangerous_attribute?).and_return(true) }
|
57
58
|
|
58
|
-
|
59
|
-
Class.new(model_class).attribute(:address).should == AttributeDefinition.new(:address)
|
59
|
+
it { expect { model_class.attribute(:address) }.to raise_error DangerousAttributeError }
|
60
60
|
end
|
61
61
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
62
|
+
context "a harmless attribute" do
|
63
|
+
it "creates an attribute with no options" do
|
64
|
+
model_class.attributes.values.should include(AttributeDefinition.new(:first_name))
|
65
|
+
end
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
end
|
67
|
+
it "returns the attribute definition" do
|
68
|
+
model_class.attribute(:address).should == AttributeDefinition.new(:address)
|
69
|
+
end
|
71
70
|
|
72
|
-
|
73
|
-
|
74
|
-
|
71
|
+
it "defines an attribute reader that calls #attribute" do
|
72
|
+
subject.should_receive(:attribute).with("first_name")
|
73
|
+
subject.first_name
|
74
|
+
end
|
75
|
+
|
76
|
+
it "defines an attribute reader that can be called via super" do
|
77
|
+
subject.should_receive(:attribute).with("amount")
|
78
|
+
subject.amount
|
79
|
+
end
|
80
|
+
|
81
|
+
it "defines an attribute writer that calls #attribute=" do
|
82
|
+
subject.should_receive(:attribute=).with("first_name", "Ben")
|
83
|
+
subject.first_name = "Ben"
|
84
|
+
end
|
85
|
+
|
86
|
+
it "defines an attribute writer that can be called via super" do
|
87
|
+
subject.should_receive(:attribute=).with("amount", 1)
|
88
|
+
subject.amount = 1
|
89
|
+
end
|
90
|
+
|
91
|
+
it "defining an attribute twice does not give the class two attribute definitions" do
|
92
|
+
Class.new do
|
93
|
+
include Attributes
|
94
|
+
attribute :name
|
95
|
+
attribute :name
|
96
|
+
end.should have(1).attributes
|
97
|
+
end
|
98
|
+
|
99
|
+
it "redefining an attribute replaces the attribute definition" do
|
100
|
+
klass = Class.new do
|
101
|
+
include Attributes
|
102
|
+
attribute :name, :type => Symbol
|
103
|
+
attribute :name, :type => String
|
104
|
+
end
|
105
|
+
|
106
|
+
klass.should have(1).attributes
|
107
|
+
klass.attributes[:name].should == AttributeDefinition.new(:name, :type => String)
|
108
|
+
end
|
75
109
|
end
|
110
|
+
end
|
76
111
|
|
77
|
-
|
78
|
-
|
79
|
-
|
112
|
+
describe ".attribute!" do
|
113
|
+
it "can create an attribute with no options" do
|
114
|
+
attributeless.attribute! :first_name
|
115
|
+
attributeless.attributes.values.should include AttributeDefinition.new(:first_name)
|
80
116
|
end
|
81
117
|
|
82
|
-
it "
|
83
|
-
|
84
|
-
include Attributes
|
85
|
-
attribute :name
|
86
|
-
attribute :name
|
87
|
-
end.should have(1).attributes
|
118
|
+
it "returns the attribute definition" do
|
119
|
+
attributeless.attribute!(:address).should == AttributeDefinition.new(:address)
|
88
120
|
end
|
89
121
|
|
90
|
-
it "
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
122
|
+
it "defines an attribute reader that calls #attribute" do
|
123
|
+
attributeless.attribute! :first_name
|
124
|
+
instance = attributeless.new
|
125
|
+
result = mock
|
126
|
+
instance.should_receive(:attribute).with("first_name").and_return(result)
|
127
|
+
instance.first_name.should equal result
|
128
|
+
end
|
96
129
|
|
97
|
-
|
98
|
-
|
130
|
+
it "defines an attribute writer that calls #attribute=" do
|
131
|
+
attributeless.attribute! :first_name
|
132
|
+
instance = attributeless.new
|
133
|
+
instance.should_receive(:attribute=).with("first_name", "Ben")
|
134
|
+
instance.first_name = "Ben"
|
99
135
|
end
|
100
136
|
end
|
101
137
|
|