attr_bucket 0.2.2 → 0.3.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.
@@ -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).