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.

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