attr_bucket 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +151 -2
- data/attr_bucket.gemspec +2 -2
- data/lib/attr_bucket.rb +19 -12
- data/lib/attr_bucket/version.rb +1 -1
- metadata +4 -4
data/README.rdoc
CHANGED
@@ -1,7 +1,156 @@
|
|
1
1
|
= attr_bucket
|
2
2
|
|
3
|
-
|
3
|
+
Sometimes you're tempted to use STI in your Rails app, but your STI classes
|
4
|
+
don't share all of the same attributes. So, you're left with two choices:
|
4
5
|
|
5
|
-
|
6
|
+
1. Add _all_ of the columns your descendant classes will need.
|
7
|
+
2. Remember that this occurrence is a telltale sign that STI is the wrong
|
8
|
+
design pattern to be using...
|
9
|
+
|
10
|
+
<b>OR IS IT?</b> Yes, it is. Probably.
|
11
|
+
|
12
|
+
But wait! I present you with a third option:
|
13
|
+
|
14
|
+
3. Give your model a bucket. So it can hold all its extra attributes.
|
15
|
+
|
16
|
+
Hoo boy. This is probably a horrible idea.
|
17
|
+
|
18
|
+
== Usage
|
19
|
+
|
20
|
+
Let's say we have a table containing animals. Animals have some universal traits,
|
21
|
+
such as a number of legs. We want to be able to search on those. But they also have
|
22
|
+
some specific traits that we'd like to display on the animal's detail page. We
|
23
|
+
don't (and this part is important) care about searching on these traits, we just want
|
24
|
+
to display them. OK, let's have at it:
|
25
|
+
|
26
|
+
class Animal < ActiveRecord::Base
|
27
|
+
# t.string :type
|
28
|
+
# t.string :name
|
29
|
+
# t.integer :number_of_legs
|
30
|
+
# t.boolean :can_fly
|
31
|
+
# t.boolean :is_cuddly
|
32
|
+
# t.text :unique_traits
|
33
|
+
end
|
34
|
+
|
35
|
+
We'll only track one unique attribute for birds, for now. This will be a string,
|
36
|
+
because strings are the default type for attributes in a bucket.
|
37
|
+
|
38
|
+
class Bird < Animal
|
39
|
+
attr_bucket :unique_traits => :group_name # "Murder" of crows, for instance
|
40
|
+
end
|
41
|
+
|
42
|
+
We might want to know "fresh" or "salt" for fish, (and whether they go best with
|
43
|
+
rice or asparagus). These will also be strings.
|
44
|
+
|
45
|
+
class Fish < Animal
|
46
|
+
attr_bucket :unique_traits => [:best_served_with, :water_type]
|
47
|
+
end
|
48
|
+
|
49
|
+
Now, we have a special animal that we want to add some non-string attributes to,
|
50
|
+
so we'll define our bucket with a hash, instead:
|
51
|
+
|
52
|
+
class Lolrus < Animal
|
53
|
+
# Yes, this actually works. It's an alias. Come on, I had to.
|
54
|
+
i_has_a_bucket :unique_traits => {
|
55
|
+
:in_possession_of_bucket => :boolean, # Has it been stolen yet?
|
56
|
+
:tusk_length_in_inches => :integer,
|
57
|
+
:also_known_as => :string
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
Now, we can create some animals. Let's start with a crow.
|
62
|
+
|
63
|
+
crow = Bird.create(
|
64
|
+
:name => 'Crow',
|
65
|
+
:number_of_legs => 2,
|
66
|
+
:can_fly => true,
|
67
|
+
:is_cuddly => false,
|
68
|
+
:group_name => 'Murder'
|
69
|
+
)
|
70
|
+
=> #<Bird id: 1, type: "Bird", name: "Crow", ...>
|
71
|
+
|
72
|
+
Looks about like we'd expect. Now we can retrieve our bucketed attributes by name,
|
73
|
+
without worrying about the bucket they're in.
|
74
|
+
|
75
|
+
crow.group_name
|
76
|
+
=> "Murder"
|
77
|
+
|
78
|
+
Let's create a fish of some kind. How about a salmon?
|
79
|
+
|
80
|
+
salmon = Fish.create(
|
81
|
+
:name => 'Salmon',
|
82
|
+
:number_of_legs => 0,
|
83
|
+
:can_fly => false,
|
84
|
+
:is_cuddly => false,
|
85
|
+
:water_type => 'fresh',
|
86
|
+
:best_served_with => 'Pan-fried asparagus'
|
87
|
+
)
|
88
|
+
=> #<Fish id: 3, type: "Fish", name: "Salmon", ...>
|
89
|
+
|
90
|
+
As before, we can retrieve the bucketed attribute:
|
91
|
+
|
92
|
+
salmon.best_served_with
|
93
|
+
=> "Pan-fried asparagus"
|
94
|
+
|
95
|
+
We can also change the attribute using a standard attribute writer, so this works just
|
96
|
+
fine with Rails forms.
|
97
|
+
|
98
|
+
salmon.best_served_with = 'A light red wine'
|
99
|
+
=> "A light red wine"
|
100
|
+
salmon.save
|
101
|
+
=> true
|
102
|
+
|
103
|
+
Let's pull it back out of the database and make sure everything looks right:
|
104
|
+
|
105
|
+
fish = Fish.find 3
|
106
|
+
=> #<Fish id: 3, type: "Fish", name: "Salmon", ...>
|
107
|
+
fish == salmon
|
108
|
+
=> true
|
109
|
+
|
110
|
+
And it maintained our updated attribute.
|
111
|
+
|
112
|
+
fish.best_served_with
|
113
|
+
=> "A light red wine"
|
114
|
+
|
115
|
+
Now let's try an animal with some typecasting -- I'll create using string values for
|
116
|
+
the bucketed attributes since that's what a Rails form would send:
|
117
|
+
|
118
|
+
lolrus = Lolrus.create(
|
119
|
+
:name => 'The LOLRUS',
|
120
|
+
:number_of_legs => 0, # Do flippers count as legs?
|
121
|
+
:can_fly => false,
|
122
|
+
:is_cuddly => true,
|
123
|
+
:also_known_as => 'The Holder of the Bucket',
|
124
|
+
:in_possession_of_bucket => 't',
|
125
|
+
:tusk_length_in_inches => '6'
|
126
|
+
)
|
127
|
+
=> #<Lolrus id: 5, type: "Lolrus", name: "The LOLRUS", ...>
|
128
|
+
|
129
|
+
Let's make sure the attributes got cast to the proper type:
|
130
|
+
|
131
|
+
lolrus.tusk_length_in_inches
|
132
|
+
=> 6
|
133
|
+
lolrus.in_possession_of_bucket
|
134
|
+
=> true
|
135
|
+
|
136
|
+
That lolrus looks to be in fine shape, indeed.
|
137
|
+
|
138
|
+
== Caveats
|
139
|
+
|
140
|
+
This whole thing's a caveat, really. If you're looking to do this, be absolutely,
|
141
|
+
positively sure that you really want to bucket your attributes rather than inherit
|
142
|
+
from an abstract class or something a bit, well, more sane.
|
143
|
+
|
144
|
+
attr_bucket isn't for you if:
|
145
|
+
|
146
|
+
* You ever intend to search against your custom attributes
|
147
|
+
* You plan to instantiate tons of records at a time (the serialization overhead
|
148
|
+
will hurt -- though it'll hurt less with psych)
|
149
|
+
* You want to get invited to all the cool software engineer parties
|
150
|
+
|
151
|
+
All that caveat-ing aside, give it a try, and let me know if you find a particularly
|
152
|
+
clever use case!
|
153
|
+
|
154
|
+
== Copyright (or blame, depending on how you look at it)
|
6
155
|
|
7
156
|
Copyright (c) 2011 {Ernie Miller}[http://metautonomo.us]. See LICENSE for details.
|
data/attr_bucket.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.authors = ["Ernie Miller"]
|
10
10
|
s.email = ["ernie@metautonomo.us"]
|
11
11
|
s.homepage = "http://metautonomo.us"
|
12
|
-
s.summary = %q{
|
13
|
-
s.description = %q{
|
12
|
+
s.summary = %q{Your model can has a bucket (for its attributes).}
|
13
|
+
s.description = %q{Store a few extra (non-searchable) attributes away in a bucket. This is probably a horrible idea, but try it anyway.}
|
14
14
|
|
15
15
|
s.add_runtime_dependency(%q<activerecord>, ["~> 3.0.0"])
|
16
16
|
|
data/lib/attr_bucket.rb
CHANGED
@@ -12,20 +12,20 @@ module AttrBucket
|
|
12
12
|
read_attribute(name)
|
13
13
|
end
|
14
14
|
|
15
|
-
def explicitly_type_cast(value, type)
|
15
|
+
def explicitly_type_cast(value, type, column_class)
|
16
16
|
return nil if value.nil?
|
17
17
|
case type
|
18
18
|
when :string then value.to_s
|
19
19
|
when :text then value.to_s
|
20
20
|
when :integer then value.to_i rescue value ? 1 : 0
|
21
21
|
when :float then value.to_f
|
22
|
-
when :decimal then
|
23
|
-
when :datetime then
|
24
|
-
when :timestamp then
|
25
|
-
when :time then
|
26
|
-
when :date then
|
27
|
-
when :binary then
|
28
|
-
when :boolean then
|
22
|
+
when :decimal then column_class.value_to_decimal(value)
|
23
|
+
when :datetime then column_class.string_to_time(value)
|
24
|
+
when :timestamp then column_class.string_to_time(value)
|
25
|
+
when :time then column_class.string_to_dummy_time(value)
|
26
|
+
when :date then column_class.string_to_date(value)
|
27
|
+
when :binary then column_class.binary_to_string(value)
|
28
|
+
when :boolean then column_class.value_to_boolean(value)
|
29
29
|
else value
|
30
30
|
end
|
31
31
|
end
|
@@ -35,33 +35,40 @@ module AttrBucket
|
|
35
35
|
|
36
36
|
def attr_bucket(opts = {})
|
37
37
|
opts.map do |bucket_name, attrs|
|
38
|
+
bucket_column = self.columns_hash[bucket_name.to_s]
|
39
|
+
unless bucket_column.type == :text
|
40
|
+
raise ArgumentError,
|
41
|
+
"#{bucket_name} is a #{bucket_column.type} column, not text"
|
42
|
+
end
|
38
43
|
serialize bucket_name, Hash
|
39
44
|
|
40
45
|
if attrs.is_a?(Hash)
|
41
46
|
attrs.map do|attr_name, attr_type|
|
42
47
|
define_bucket_reader bucket_name, attr_name
|
43
|
-
define_bucket_writer bucket_name, attr_name, attr_type
|
48
|
+
define_bucket_writer bucket_name, attr_name, attr_type, bucket_column.class
|
44
49
|
end
|
45
50
|
else
|
46
51
|
Array.wrap(attrs).each do |attr_name|
|
47
52
|
define_bucket_reader bucket_name, attr_name
|
48
|
-
define_bucket_writer bucket_name, attr_name, :string
|
53
|
+
define_bucket_writer bucket_name, attr_name, :string, bucket_column.class
|
49
54
|
end
|
50
55
|
end
|
51
56
|
end
|
52
57
|
end
|
53
58
|
|
59
|
+
alias :i_has_a_bucket :attr_bucket
|
60
|
+
|
54
61
|
def define_bucket_reader(bucket_name, attr_name)
|
55
62
|
define_method attr_name do
|
56
63
|
get_attr_bucket(bucket_name)[attr_name]
|
57
64
|
end unless method_defined? attr_name
|
58
65
|
end
|
59
66
|
|
60
|
-
def define_bucket_writer(bucket_name, attr_name, attr_type)
|
67
|
+
def define_bucket_writer(bucket_name, attr_name, attr_type, column_class)
|
61
68
|
define_method "#{attr_name}=" do |val|
|
62
69
|
# TODO: Make this more resilient/granular for multiple bucketed changes
|
63
70
|
send("#{bucket_name}_will_change!")
|
64
|
-
get_attr_bucket(bucket_name)[attr_name] = explicitly_type_cast(val, attr_type)
|
71
|
+
get_attr_bucket(bucket_name)[attr_name] = explicitly_type_cast(val, attr_type, column_class)
|
65
72
|
end unless method_defined? "#{attr_name}="
|
66
73
|
end
|
67
74
|
end
|
data/lib/attr_bucket/version.rb
CHANGED
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 1
|
8
|
-
-
|
9
|
-
version: 0.1.
|
8
|
+
- 1
|
9
|
+
version: 0.1.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Ernie Miller
|
@@ -32,7 +32,7 @@ dependencies:
|
|
32
32
|
version: 3.0.0
|
33
33
|
type: :runtime
|
34
34
|
version_requirements: *id001
|
35
|
-
description:
|
35
|
+
description: Store a few extra (non-searchable) attributes away in a bucket. This is probably a horrible idea, but try it anyway.
|
36
36
|
email:
|
37
37
|
- ernie@metautonomo.us
|
38
38
|
executables: []
|
@@ -81,6 +81,6 @@ rubyforge_project: attr_bucket
|
|
81
81
|
rubygems_version: 1.3.7
|
82
82
|
signing_key:
|
83
83
|
specification_version: 3
|
84
|
-
summary:
|
84
|
+
summary: Your model can has a bucket (for its attributes).
|
85
85
|
test_files: []
|
86
86
|
|