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.
- data/lib/attr_bucket/version.rb +1 -1
- data/lib/attr_bucket.rb +143 -147
- data/spec/attr_bucket_spec.rb +6 -0
- metadata +4 -22
data/lib/attr_bucket/version.rb
CHANGED
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
|
-
#
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
81
|
-
if
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
89
|
-
column_class.string_to_date(value)
|
87
|
+
self[name]
|
90
88
|
end
|
91
|
-
end
|
92
89
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
103
|
-
|
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
|
-
#
|
106
|
-
#
|
107
|
-
#
|
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
|
-
#
|
127
|
-
#
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
143
|
+
raise ArgumentError, "Unable to typecast #{value} to #{type}" unless valid_class(typecasted, type)
|
157
144
|
|
158
|
-
|
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
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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.
|
174
|
+
ActiveRecord::Base.extend AttrBucket
|
data/spec/attr_bucket_spec.rb
CHANGED
@@ -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:
|
5
|
-
|
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-
|
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.
|
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).
|