attr_bucket 0.1.0 → 0.1.1
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/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
|
|