attr_bucket 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  module AttrBucket
2
- VERSION = "0.2.2"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/attr_bucket.rb CHANGED
@@ -1,178 +1,174 @@
1
1
  module AttrBucket
2
- def self.included(base) #:nodoc:
3
- base.extend ClassMethods
4
- base.class_attribute :bucketed_attributes
5
- base.bucketed_attributes = []
6
- base.instance_eval do
7
- alias_method_chain :assign_multiparameter_attributes, :attr_bucket
8
- end
9
- end
10
-
11
2
  private
12
3
 
13
- # Retrieve the attribute bucket, or if it's not yet a Hash,
14
- # initialize it as one.
15
- def get_attr_bucket(name)
16
- unless self[name].is_a?(Hash)
17
- self[name] = {}
4
+ # Accepts an options hash of the format:
5
+ #
6
+ # :<bucket-name> => <bucket-attributes>,
7
+ # [:<bucket-name> => <bucket-attributes>], [...]
8
+ #
9
+ # where <tt><bucket-name></tt> is the +text+ column being used for
10
+ # serializing the objects, and <tt><bucket-attributes></tt> is:
11
+ #
12
+ # * A single attribute name in symbol format (not much point...)
13
+ # * An array of attribute names
14
+ # * A hash, in which the keys are the attribute names in Symbol format,
15
+ # and the values describe what they should be typecast as. The valid
16
+ # choices are +string+, +text+, +integer+, +float+, +decimal+, +datetime+,
17
+ # +timestamp+, +time+, +date+, +binary+, or +boolean+. This will invoke
18
+ # the same typecasting behavior as is normally used by the underlying
19
+ # ActiveRecord column (specific to your database).
20
+ #
21
+ # Alternately, you may specify a Proc or another object that responds to
22
+ # +call+, and it will be invoked with the value being assigned to the
23
+ # attribute as its only parameter, for custom typecasting behavior.
24
+ #
25
+ # Example:
26
+ #
27
+ # attr_bucket :bucket => {
28
+ # :is_awesome => :boolean
29
+ # :circumference => :integer,
30
+ # :flanderized_name => proc {|val| "#{val}-diddly"}
31
+ # }
32
+ def attr_bucket(opts = {})
33
+ unless include? InstanceMethods
34
+ include InstanceMethods
35
+
36
+ class_attribute :bucketed_attributes
37
+ self.bucketed_attributes = []
18
38
  end
19
- self[name]
20
- end
21
39
 
22
- def valid_class(value, type)
23
- case type
24
- when :integer then Fixnum === value
25
- when :float then Float === value
26
- when :decimal then BigDecimal === value
27
- when :datetime then Time === value
28
- when :date then Date === value
29
- when :timestamp then Time === value
30
- when :time then Time === value
31
- when :text, :string then String === value
32
- when :binary then String === value
33
- when :boolean then [TrueClass, FalseClass].grep value
34
- else false
35
- end
36
- end
40
+ opts.map do |bucket_name, attrs|
41
+ bucket_column = self.columns_hash[bucket_name.to_s]
42
+ unless bucket_column.type == :text
43
+ raise ArgumentError,
44
+ "#{bucket_name} is of type #{bucket_column.type}, not text"
45
+ end
46
+ serialize bucket_name, Hash
37
47
 
38
- # We have to override assign_multiparameter_attributes to catch
39
- # dates/times for bucketed columns and handle them ourselves
40
- # before passing the remainder on for ActiveRecord::Base to handle.
41
- def assign_multiparameter_attributes_with_attr_bucket(pairs)
42
- bucket_pairs = pairs.select {|p| self.class.bucketed_attributes.include?(p.first.split('(').first)}
43
- extract_callstack_for_multiparameter_attributes(bucket_pairs).each do |name, value|
44
- send(name + '=', value.compact.empty? ? nil : value)
48
+ if attrs.is_a?(Hash)
49
+ attrs.map do|attr_name, attr_type|
50
+ define_bucket_reader bucket_name, attr_name
51
+ define_bucket_writer bucket_name, attr_name, attr_type, bucket_column.class
52
+ end
53
+ else
54
+ Array.wrap(attrs).each do |attr_name|
55
+ define_bucket_reader bucket_name, attr_name
56
+ define_bucket_writer bucket_name, attr_name, :string, bucket_column.class
57
+ end
58
+ end
45
59
  end
46
- assign_multiparameter_attributes_without_attr_bucket(pairs - bucket_pairs)
47
60
  end
48
61
 
49
- # Swipe the nifty column typecasting from the column class
50
- # underlying the bucket column, or use the call method of
51
- # the object supplied for +type+ if it responds to call.
52
- #
53
- # This allows custom typecasting by supplying a proc, etc
54
- # as the value side of the hash in an attr_bucket definition.
55
- def explicitly_type_cast(value, type, column_class)
56
- return nil if value.nil?
57
-
58
- return type.call(value) if type.respond_to?(:call)
59
-
60
- typecasted = case type
61
- when :string then value.to_s
62
- when :text then value.to_s
63
- when :integer then value.to_i rescue value ? 1 : 0
64
- when :float then value.to_f
65
- when :decimal then column_class.value_to_decimal(value)
66
- when :datetime then cast_to_time(value, column_class)
67
- when :timestamp then cast_to_time(value, column_class)
68
- when :time then cast_to_time(value, column_class, true)
69
- when :date then cast_to_date(value, column_class)
70
- when :binary then column_class.binary_to_string(value)
71
- when :boolean then column_class.value_to_boolean(value)
72
- else value
73
- end
62
+ alias :i_has_a_bucket :attr_bucket
74
63
 
75
- raise ArgumentError, "Unable to typecast #{value} to #{type}" unless valid_class(typecasted, type)
64
+ def define_bucket_reader(bucket_name, attr_name) #:nodoc:
65
+ self.bucketed_attributes += [attr_name.to_s]
66
+ define_method attr_name do
67
+ get_attr_bucket(bucket_name)[attr_name]
68
+ end unless method_defined? attr_name
69
+ end
76
70
 
77
- typecasted
71
+ def define_bucket_writer(bucket_name, attr_name, attr_type, column_class) #:nodoc:
72
+ define_method "#{attr_name}=" do |val|
73
+ # TODO: Make this more resilient/granular for multiple bucketed changes
74
+ send("#{bucket_name}_will_change!")
75
+ typecasted = explicitly_type_cast(val, attr_type, column_class)
76
+ get_attr_bucket(bucket_name)[attr_name] = typecasted
77
+ end unless method_defined? "#{attr_name}="
78
78
  end
79
79
 
80
- def cast_to_date(value, column_class)
81
- if value.is_a?(Array)
82
- begin
83
- values = value.collect { |v| v.nil? ? 1 : v }
84
- Date.new(*values)
85
- rescue ArgumentError => e
86
- Time.time_with_datetime_fallback(self.class.default_timezone, *values).to_date
80
+ module InstanceMethods
81
+ # Retrieve the attribute bucket, or if it's not yet a Hash,
82
+ # initialize it as one.
83
+ def get_attr_bucket(name)
84
+ unless self[name].is_a?(Hash)
85
+ self[name] = {}
87
86
  end
88
- else
89
- column_class.string_to_date(value)
87
+ self[name]
90
88
  end
91
- end
92
89
 
93
- def cast_to_time(value, column_class, dummy_time = false)
94
- if value.is_a?(Array)
95
- value[0] ||= Date.today.year
96
- Time.time_with_datetime_fallback(self.class.default_timezone, *value)
97
- else
98
- dummy_time ? column_class.string_to_dummy_time(value) : column_class.string_to_time(value)
90
+ def valid_class(value, type)
91
+ case type
92
+ when :integer then Fixnum === value
93
+ when :float then Float === value
94
+ when :decimal then BigDecimal === value
95
+ when :datetime then Time === value
96
+ when :date then Date === value
97
+ when :timestamp then Time === value
98
+ when :time then Time === value
99
+ when :text, :string then String === value
100
+ when :binary then String === value
101
+ when :boolean then [TrueClass, FalseClass].grep value
102
+ else false
103
+ end
99
104
  end
100
- end
101
105
 
102
- module ClassMethods
103
- private
106
+ # We have to override assign_multiparameter_attributes to catch
107
+ # dates/times for bucketed columns and handle them ourselves
108
+ # before passing the remainder on for ActiveRecord::Base to handle.
109
+ def assign_multiparameter_attributes(pairs)
110
+ bucket_pairs = pairs.select {|p| self.class.bucketed_attributes.include?(p.first.split('(').first)}
111
+ extract_callstack_for_multiparameter_attributes(bucket_pairs).each do |name, value|
112
+ send(name + '=', value.compact.empty? ? nil : value)
113
+ end
114
+ super(pairs - bucket_pairs)
115
+ end
104
116
 
105
- # Accepts an options hash of the format:
106
- #
107
- # :<bucket-name> => <bucket-attributes>,
108
- # [:<bucket-name> => <bucket-attributes>], [...]
109
- #
110
- # where <tt><bucket-name></tt> is the +text+ column being used for
111
- # serializing the objects, and <tt><bucket-attributes></tt> is:
112
- #
113
- # * A single attribute name in symbol format (not much point...)
114
- # * An array of attribute names
115
- # * A hash, in which the keys are the attribute names in Symbol format,
116
- # and the values describe what they should be typecast as. The valid
117
- # choices are +string+, +text+, +integer+, +float+, +decimal+, +datetime+,
118
- # +timestamp+, +time+, +date+, +binary+, or +boolean+. This will invoke
119
- # the same typecasting behavior as is normally used by the underlying
120
- # ActiveRecord column (specific to your database).
121
- #
122
- # Alternately, you may specify a Proc or another object that responds to
123
- # +call+, and it will be invoked with the value being assigned to the
124
- # attribute as its only parameter, for custom typecasting behavior.
117
+ # Swipe the nifty column typecasting from the column class
118
+ # underlying the bucket column, or use the call method of
119
+ # the object supplied for +type+ if it responds to call.
125
120
  #
126
- # Example:
127
- #
128
- # attr_bucket :bucket => {
129
- # :is_awesome => :boolean
130
- # :circumference => :integer,
131
- # :flanderized_name => proc {|val| "#{val}-diddly"}
132
- # }
133
- def attr_bucket(opts = {})
134
- opts.map do |bucket_name, attrs|
135
- bucket_column = self.columns_hash[bucket_name.to_s]
136
- unless bucket_column.type == :text
137
- raise ArgumentError,
138
- "#{bucket_name} is of type #{bucket_column.type}, not text"
139
- end
140
- serialize bucket_name, Hash
141
-
142
- if attrs.is_a?(Hash)
143
- attrs.map do|attr_name, attr_type|
144
- define_bucket_reader bucket_name, attr_name
145
- define_bucket_writer bucket_name, attr_name, attr_type, bucket_column.class
146
- end
147
- else
148
- Array.wrap(attrs).each do |attr_name|
149
- define_bucket_reader bucket_name, attr_name
150
- define_bucket_writer bucket_name, attr_name, :string, bucket_column.class
151
- end
152
- end
121
+ # This allows custom typecasting by supplying a proc, etc
122
+ # as the value side of the hash in an attr_bucket definition.
123
+ def explicitly_type_cast(value, type, column_class)
124
+ return nil if value.nil?
125
+
126
+ return type.call(value) if type.respond_to?(:call)
127
+
128
+ typecasted = case type
129
+ when :string then value.to_s
130
+ when :text then value.to_s
131
+ when :integer then value.to_i rescue value ? 1 : 0
132
+ when :float then value.to_f
133
+ when :decimal then column_class.value_to_decimal(value)
134
+ when :datetime then cast_to_time(value, column_class)
135
+ when :timestamp then cast_to_time(value, column_class)
136
+ when :time then cast_to_time(value, column_class, true)
137
+ when :date then cast_to_date(value, column_class)
138
+ when :binary then column_class.binary_to_string(value)
139
+ when :boolean then column_class.value_to_boolean(value)
140
+ else value
153
141
  end
154
- end
155
142
 
156
- alias :i_has_a_bucket :attr_bucket
143
+ raise ArgumentError, "Unable to typecast #{value} to #{type}" unless valid_class(typecasted, type)
157
144
 
158
- def define_bucket_reader(bucket_name, attr_name) #:nodoc:
159
- self.bucketed_attributes += [attr_name.to_s]
160
- define_method attr_name do
161
- get_attr_bucket(bucket_name)[attr_name]
162
- end unless method_defined? attr_name
145
+ typecasted
163
146
  end
164
147
 
165
- def define_bucket_writer(bucket_name, attr_name, attr_type, column_class) #:nodoc:
166
- define_method "#{attr_name}=" do |val|
167
- # TODO: Make this more resilient/granular for multiple bucketed changes
168
- send("#{bucket_name}_will_change!")
169
- typecasted = explicitly_type_cast(val, attr_type, column_class)
170
- get_attr_bucket(bucket_name)[attr_name] = explicitly_type_cast(val, attr_type, column_class)
171
- end unless method_defined? "#{attr_name}="
148
+ def cast_to_date(value, column_class)
149
+ if value.is_a?(Array)
150
+ begin
151
+ values = value.collect { |v| v.nil? ? 1 : v }
152
+ Date.new(*values)
153
+ rescue ArgumentError => e
154
+ Time.time_with_datetime_fallback(self.class.default_timezone, *values).to_date
155
+ end
156
+ else
157
+ column_class.string_to_date(value)
158
+ end
159
+ end
160
+
161
+ def cast_to_time(value, column_class, dummy_time = false)
162
+ if value.is_a?(Array)
163
+ value[0] ||= Date.today.year
164
+ Time.time_with_datetime_fallback(self.class.default_timezone, *value)
165
+ else
166
+ dummy_time ? column_class.string_to_dummy_time(value) : column_class.string_to_time(value)
167
+ end
172
168
  end
173
169
  end
174
170
  end
175
171
 
176
172
  require 'active_record'
177
173
 
178
- ActiveRecord::Base.send(:include, AttrBucket)
174
+ ActiveRecord::Base.extend AttrBucket
@@ -103,6 +103,12 @@ describe AttrBucket do
103
103
  @o.hired_at.should eq Time.local(2011, 1, 1, 8, 0)
104
104
  end
105
105
 
106
+ it 'replaces a nil year with the current year in cast to Time' do
107
+ @o.hired_at = [nil, 1, 1, 8, 0]
108
+ @o.hired_at.should be_a Time
109
+ @o.hired_at.should eq Time.local(Date.today.year, 1, 1, 8, 0)
110
+ end
111
+
106
112
  it 'typecasts to Time for :timestamp' do
107
113
  @o.stamp = '2011-01-01 08:00:00'
108
114
  @o.stamp.should be_a Time
metadata CHANGED
@@ -1,12 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attr_bucket
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 0
7
- - 2
8
- - 2
9
- version: 0.2.2
4
+ prerelease:
5
+ version: 0.3.0
10
6
  platform: ruby
11
7
  authors:
12
8
  - Ernie Miller
@@ -14,7 +10,7 @@ autorequire:
14
10
  bindir: bin
15
11
  cert_chain: []
16
12
 
17
- date: 2011-01-30 00:00:00 -05:00
13
+ date: 2011-02-03 00:00:00 -05:00
18
14
  default_executable:
19
15
  dependencies:
20
16
  - !ruby/object:Gem::Dependency
@@ -25,10 +21,6 @@ dependencies:
25
21
  requirements:
26
22
  - - ~>
27
23
  - !ruby/object:Gem::Version
28
- segments:
29
- - 3
30
- - 0
31
- - 0
32
24
  version: 3.0.0
33
25
  type: :runtime
34
26
  version_requirements: *id001
@@ -40,10 +32,6 @@ dependencies:
40
32
  requirements:
41
33
  - - ~>
42
34
  - !ruby/object:Gem::Version
43
- segments:
44
- - 2
45
- - 4
46
- - 0
47
35
  version: 2.4.0
48
36
  type: :development
49
37
  version_requirements: *id002
@@ -55,8 +43,6 @@ dependencies:
55
43
  requirements:
56
44
  - - ">="
57
45
  - !ruby/object:Gem::Version
58
- segments:
59
- - 0
60
46
  version: "0"
61
47
  type: :development
62
48
  version_requirements: *id003
@@ -96,21 +82,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
82
  requirements:
97
83
  - - ">="
98
84
  - !ruby/object:Gem::Version
99
- segments:
100
- - 0
101
85
  version: "0"
102
86
  required_rubygems_version: !ruby/object:Gem::Requirement
103
87
  none: false
104
88
  requirements:
105
89
  - - ">="
106
90
  - !ruby/object:Gem::Version
107
- segments:
108
- - 0
109
91
  version: "0"
110
92
  requirements: []
111
93
 
112
94
  rubyforge_project: attr_bucket
113
- rubygems_version: 1.3.7
95
+ rubygems_version: 1.5.0
114
96
  signing_key:
115
97
  specification_version: 3
116
98
  summary: Your model can has a bucket (for its attributes).