active_attr 0.5.1 → 0.6.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.
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
|
|