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.

@@ -55,7 +55,7 @@ module ActiveAttr
55
55
  #
56
56
  # @since 0.5.0
57
57
  def attribute(name)
58
- typecast_attribute(_attribute_type(name), super)
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
- "#{self.name}#{attributes_list}"
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 "active_support/concern"
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 [Class] type The type to cast to
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(type, value)
51
- raise ArgumentError, "a Class must be given" unless type
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
- typecast_value(type, value)
39
+ typecaster.call(value)
54
40
  end
55
41
 
56
- # Typecasts a value according to a predefined set of mapping rules defined
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 [Object, nil] The result of a method call defined in
63
- # TYPECASTING_METHODS, nil if no method is found
46
+ # @return [#call, nil] The typecaster to use
64
47
  #
65
- # @since 0.5.0
66
- def typecast_value(type, value)
67
- if typecaster = TYPECASTERS[type]
68
- typecaster.new.call(value)
69
- end
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
@@ -1,5 +1,5 @@
1
1
  module ActiveAttr
2
2
  # Complete version string
3
3
  # @since 0.1.0
4
- VERSION = "0.5.1"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -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 == model_class.new.created_at
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
- Factory.define :person, :class => :person do |model|
164
- model.first_name "Chris"
165
- model.last_name "Griego"
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 "defining a dangerous attribute" do
194
- it "defining an attribute that conflicts with #{described_class} raises DangerousAttributeError" do
195
- expect { model_class.attribute(:write_attribute) }.to raise_error DangerousAttributeError, %{an attribute method named "write_attribute" would conflict with an existing method}
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
- it "defining an attribute that conflicts with ActiveModel::AttributeMethods raises DangerousAttributeError" do
199
- expect { model_class.attribute(:inspect) }.to raise_error DangerousAttributeError, %{an attribute method named "inspect" would conflict with an existing method}
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
- it "defining an :id attribute does not raise" do
203
- expect { model_class.attribute(:id) }.not_to raise_error
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
- it "defining a :type attribute does not raise" do
207
- expect { model_class.attribute(:type) }.not_to raise_error
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
- it "defining an attribute that conflicts with Kernel raises DangerousAttributeError" do
211
- expect { model_class.attribute(:puts) }.to raise_error DangerousAttributeError
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
- it "defining an attribute that conflicts with Object raises DangerousAttributeError" do
215
- expect { model_class.attribute(:class) }.to raise_error DangerousAttributeError
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
- it "defining an attribute that conflicts with BasicObject raises DangerousAttributeError" do
219
- expect { model_class.attribute(:instance_eval) }.to raise_error DangerousAttributeError
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
- it "defining an attribute that conflicts with a properly implemented method_missing callback raises DangerousAttributeError" do
223
- expect { model_class.attribute(:my_proper_missing_method) }.to raise_error DangerousAttributeError
268
+ context "an :id attribute" do
269
+ let(:attribute_name) { :id }
270
+ include_examples "a whitelisted attribute"
224
271
  end
225
272
 
226
- it "defining an attribute that conflicts with a less properly implemented method_missing callback raises DangerousAttributeError" do
227
- expect { model_class.attribute(:my_less_proper_missing_method) }.to raise_error DangerousAttributeError
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 a dangerous attribute"
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 a dangerous attribute"
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
@@ -0,0 +1,17 @@
1
+ class Age
2
+ def ==(other)
3
+ other.kind_of?(self.class) && other.to_i == to_i
4
+ end
5
+
6
+ def initialize(years)
7
+ @years = years.to_i
8
+ end
9
+
10
+ def inspect
11
+ "#{@years} years old"
12
+ end
13
+
14
+ def to_i
15
+ @years
16
+ end
17
+ end
@@ -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
- include Attributes
44
+ Class.new.tap do |attributeless|
45
+ attributeless.class_eval do
46
+ include Attributes
46
47
 
47
- def self.name
48
- "Foo"
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
- it "creates an attribute with no options" do
55
- model_class.attributes.values.should include(AttributeDefinition.new(:first_name))
56
- end
56
+ context "a dangerous attribute" do
57
+ before { model_class.stub(:dangerous_attribute?).and_return(true) }
57
58
 
58
- it "returns the attribute definition" do
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
- it "defines an attribute reader that calls #attribute" do
63
- subject.should_receive(:attribute).with("first_name")
64
- subject.first_name
65
- end
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
- it "defines an attribute reader that can be called via super" do
68
- subject.should_receive(:attribute).with("amount")
69
- subject.amount
70
- end
67
+ it "returns the attribute definition" do
68
+ model_class.attribute(:address).should == AttributeDefinition.new(:address)
69
+ end
71
70
 
72
- it "defines an attribute writer that calls #attribute=" do
73
- subject.should_receive(:attribute=).with("first_name", "Ben")
74
- subject.first_name = "Ben"
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
- it "defines an attribute writer that can be called via super" do
78
- subject.should_receive(:attribute=).with("amount", 1)
79
- subject.amount = 1
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 "defining an attribute twice does not give the class two attribute definitions" do
83
- Class.new do
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 "redefining an attribute replaces the attribute definition" do
91
- klass = Class.new do
92
- include Attributes
93
- attribute :name, :type => Symbol
94
- attribute :name, :type => String
95
- end
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
- klass.should have(1).attributes
98
- klass.attributes[:name].should == AttributeDefinition.new(:name, :type => String)
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