tableless_model 0.0.7 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +2 -0
- data/Gemfile.lock +29 -11
- data/README.md +274 -0
- data/lib/activerecord/base/class_methods.rb +8 -1
- data/lib/activerecord/base/instance_methods.rb +15 -0
- data/lib/tableless_model.rb +7 -2
- data/lib/tableless_model/class_methods.rb +4 -5
- data/lib/tableless_model/instance_methods.rb +29 -7
- data/lib/tableless_model/version.rb +1 -1
- data/spec/spec_helper.rb +7 -0
- data/spec/tableless_model_spec.rb +203 -193
- data/tableless_model.gemspec +3 -2
- metadata +34 -11
- data/README.rdoc +0 -170
- data/spec/test_helper.rb +0 -157
data/.rspec
ADDED
data/Gemfile.lock
CHANGED
@@ -1,27 +1,45 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
tableless_model (0.0.
|
4
|
+
tableless_model (0.0.7)
|
5
5
|
validatable
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: http://rubygems.org/
|
9
9
|
specs:
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
activemodel (3.0.9)
|
11
|
+
activesupport (= 3.0.9)
|
12
|
+
builder (~> 2.1.2)
|
13
|
+
i18n (~> 0.5.0)
|
14
|
+
activerecord (3.0.9)
|
15
|
+
activemodel (= 3.0.9)
|
16
|
+
activesupport (= 3.0.9)
|
17
|
+
arel (~> 2.0.10)
|
18
|
+
tzinfo (~> 0.3.23)
|
19
|
+
activesupport (3.0.9)
|
20
|
+
arel (2.0.10)
|
21
|
+
builder (2.1.2)
|
22
|
+
diff-lcs (1.1.2)
|
23
|
+
i18n (0.5.0)
|
24
|
+
rspec (2.6.0)
|
25
|
+
rspec-core (~> 2.6.0)
|
26
|
+
rspec-expectations (~> 2.6.0)
|
27
|
+
rspec-mocks (~> 2.6.0)
|
28
|
+
rspec-core (2.6.4)
|
29
|
+
rspec-expectations (2.6.0)
|
30
|
+
diff-lcs (~> 1.1.2)
|
31
|
+
rspec-mocks (2.6.0)
|
32
|
+
sqlite3 (1.3.3)
|
33
|
+
timecop (0.3.5)
|
34
|
+
tzinfo (0.3.29)
|
17
35
|
validatable (1.6.7)
|
18
36
|
|
19
37
|
PLATFORMS
|
20
38
|
ruby
|
21
39
|
|
22
40
|
DEPENDENCIES
|
23
|
-
|
24
|
-
|
41
|
+
activerecord
|
42
|
+
rspec
|
25
43
|
sqlite3
|
26
44
|
tableless_model!
|
27
|
-
|
45
|
+
timecop
|
data/README.md
ADDED
@@ -0,0 +1,274 @@
|
|
1
|
+
Original blog post (January 3, 2011)
|
2
|
+
http://vitobotta.com/serialisable-validatable-tableless-model/
|
3
|
+
|
4
|
+
|
5
|
+
# Tableless Model
|
6
|
+
|
7
|
+
This is an extended Hash that has a defined collection of method-like attributes, and only these attributes can be set or read from the hash.
|
8
|
+
Optionally, you can also set default values and enforce data types for these attributes.
|
9
|
+
|
10
|
+
Tableless Model behaves in a similar way to normal ActiveRecord models in that it also supports validations and can be useful, for example, to reduce database complexity in some cases, by removing associations and therefore tables.
|
11
|
+
|
12
|
+
In particular, by using Tableless Model, you could save tables whenever you have one to one associations between a parent model and a child model containing options, settings, debugging information or any other collection of attributes that belongs uniquely to a single parent object.
|
13
|
+
|
14
|
+
Removing database tables also means reducing the number of queries to fetch associations, therefore it can also help a little bit with performance.
|
15
|
+
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Tableless Model is available as a Rubygem:
|
20
|
+
|
21
|
+
``` bash
|
22
|
+
gem install tableless_model
|
23
|
+
```
|
24
|
+
|
25
|
+
== Usage
|
26
|
+
|
27
|
+
For example's sake, say we have these two models:
|
28
|
+
|
29
|
+
1)
|
30
|
+
|
31
|
+
``` ruby
|
32
|
+
class Page < ActiveRecord::Base
|
33
|
+
|
34
|
+
# having columns such as id, title, etc
|
35
|
+
|
36
|
+
has_one :seo_options
|
37
|
+
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
2)
|
42
|
+
|
43
|
+
``` ruby
|
44
|
+
class SeoOptions < ActiveRecord::Base
|
45
|
+
|
46
|
+
set_table_name "seo_options"
|
47
|
+
|
48
|
+
# having columns such as id, title_tag, meta_description, meta_keywords,
|
49
|
+
# noindex, nofollow, noarchive, page_id
|
50
|
+
|
51
|
+
belongs_to :page
|
52
|
+
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
So that each instance of Page has its own SEO options, and these options/settings only belong to a page, so we have a one to one association, and our database will have the tables "pages", and "seo_options".
|
57
|
+
|
58
|
+
Using Tableless Model, we could remove the association and the table seo_options altogether, by storing those options in a column of the pages table, in a YAML-serialized form. So the models become:
|
59
|
+
|
60
|
+
|
61
|
+
1)
|
62
|
+
|
63
|
+
``` ruby
|
64
|
+
class Page < ActiveRecord::Base
|
65
|
+
|
66
|
+
# having columns such as id, title, seo, etc
|
67
|
+
|
68
|
+
has_tableless :seo => SeoOptions
|
69
|
+
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
2)
|
74
|
+
|
75
|
+
``` ruby
|
76
|
+
class SeoOptions < ActiveRecord::TablelessModel
|
77
|
+
|
78
|
+
attribute :title_tag, :type => :string, :default => "default title tag"
|
79
|
+
attribute :meta_description, :type => :string, :default => ""
|
80
|
+
attribute :meta_keywords, :type => :string, :default => ""
|
81
|
+
attribute :noindex, :type => :boolean, :default => false
|
82
|
+
attribute :nofollow, :type => :boolean, :default => false
|
83
|
+
attribute :noarchive, :type => :boolean, :default => false
|
84
|
+
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
That's it.
|
89
|
+
|
90
|
+
When you now create an instance of SeoOptions, you can get and set its attributes as you would do with a normal model:
|
91
|
+
|
92
|
+
``` ruby
|
93
|
+
seo_options = SeoOptions.new
|
94
|
+
=> <#SeoOptions meta_description="" meta_keywords="" noarchive=false nofollow=false noindex=false title_tag="default title tag">
|
95
|
+
|
96
|
+
seo_options.title_tag
|
97
|
+
=> "default title tag"
|
98
|
+
|
99
|
+
seo_options.title_tag = "new title tag"
|
100
|
+
=> "new title tag"
|
101
|
+
```
|
102
|
+
|
103
|
+
Note that inspect shows the properties of the Tableless Model in the same way it does for ActiveRecord models.
|
104
|
+
Of course, you can also override the default values for the attributes when creating a new instance:
|
105
|
+
|
106
|
+
``` ruby
|
107
|
+
seo_options = SeoOptions.new( :title_tag => "a different title tag" )
|
108
|
+
=> <#SeoOptions meta_description="" meta_keywords="" noarchive=false nofollow=false noindex=false title_tag="a different title tag">
|
109
|
+
```
|
110
|
+
|
111
|
+
Now, if you have used the has_tabless macro in the parent class, Page, each instance of Page will store directly its YAML-serialized SEO settings in the column named "seo".
|
112
|
+
|
113
|
+
``` ruby
|
114
|
+
page = Page.new
|
115
|
+
|
116
|
+
page.seo
|
117
|
+
=> <#SeoOptions meta_description="" meta_keywords="" noarchive=false nofollow=false noindex=false title_tag="default title tag">
|
118
|
+
|
119
|
+
page.seo.title_tag = "changed title tag"
|
120
|
+
=> <#SeoOptions meta_description="" meta_keywords="" noarchive=false nofollow=false noindex=false title_tag="changed title tag">
|
121
|
+
```
|
122
|
+
|
123
|
+
And this is how the content of the serialized column would look like in the database if you saved the changes as in the example
|
124
|
+
|
125
|
+
``` yaml
|
126
|
+
--- !map:SeoOptions
|
127
|
+
noarchive: false
|
128
|
+
meta_description:
|
129
|
+
meta_keywords:
|
130
|
+
nofollow: false
|
131
|
+
title_tag: "changed title tag"
|
132
|
+
noindex: false
|
133
|
+
```
|
134
|
+
|
135
|
+
You can also pass a lambda/Proc when defining the default value of an attribute, so that the actual value will be calculated at runtime when a new instance of the tableless mode is being initialised:
|
136
|
+
|
137
|
+
``` ruby
|
138
|
+
class SeoOptions < ActiveRecord::TablelessModel
|
139
|
+
|
140
|
+
attribute :created_at, :type => :time, :default => lambda { Time.now }
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
```
|
145
|
+
|
146
|
+
Then, when the tableless model is initialised together with the parent model, the default value will be calculated and assigned in that moment:
|
147
|
+
|
148
|
+
``` ruby
|
149
|
+
>> SeoOptions.new
|
150
|
+
=> <#SeoOptions created_at_=Sun Jul 24 19:26:33 +0100 2011>
|
151
|
+
>> SeoOptions.new
|
152
|
+
=> <#SeoOptions created_at_=Sun Jul 24 19:26:37 +0100 2011>
|
153
|
+
>> SeoOptions.new
|
154
|
+
=> <#SeoOptions created_at_=Sun Jul 24 19:26:43 +0100 2011>
|
155
|
+
```
|
156
|
+
|
157
|
+
Of course, if a value is specified for an attribute when creating an instance of the tableless mode, the default value specified for that attribute will be ignored, including lambdas/procs:
|
158
|
+
|
159
|
+
``` ruby
|
160
|
+
>> SeoOptions.new :created_at => Time.local(2011, 7, 24, 18, 47, 0)
|
161
|
+
=> <#SeoOptions created_at=Sun Jul 24 18:47:00 +0100 2011>
|
162
|
+
```
|
163
|
+
|
164
|
+
For each of the attribute defined in the tableless model, shortcuts for both setter and getter are automatically defined in the parent model, unless the parent model already has a method of its own by the same name.
|
165
|
+
|
166
|
+
So, for instance, if you have the tableless model:
|
167
|
+
|
168
|
+
``` ruby
|
169
|
+
class SeoOptions < ActiveRecord::TablelessModel
|
170
|
+
|
171
|
+
attribute :title_tag, :type => :string, :default => "default title tag"
|
172
|
+
|
173
|
+
end
|
174
|
+
```
|
175
|
+
|
176
|
+
which is used by a parent model:
|
177
|
+
|
178
|
+
``` ruby
|
179
|
+
class Page < ActiveRecord::Base
|
180
|
+
|
181
|
+
has_tableless :seo => SeoOptions
|
182
|
+
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
you can get/set attributes of the tableless model directly from the parent model:
|
187
|
+
|
188
|
+
|
189
|
+
``` ruby
|
190
|
+
# this...
|
191
|
+
>> page.title_tag
|
192
|
+
=> "default title tag"
|
193
|
+
|
194
|
+
# is same as...
|
195
|
+
>> page.seo_options.title_tag
|
196
|
+
=> "default title tag"
|
197
|
+
```
|
198
|
+
|
199
|
+
For boolean attributes (or also truthy/falsy ones) you can also make calls to special getters ending with "?", so to get true or false in return, depending on the actual value of the attribute:
|
200
|
+
|
201
|
+
``` ruby
|
202
|
+
# this...
|
203
|
+
>> page.title_tag?
|
204
|
+
=> true
|
205
|
+
```
|
206
|
+
|
207
|
+
|
208
|
+
## Validations
|
209
|
+
|
210
|
+
Tableless Model uses the Validatable gem to support validations methods and callbacks (such as "after_validation").
|
211
|
+
Note: it currently uses the Rails 2.x syntax only.
|
212
|
+
|
213
|
+
Example:
|
214
|
+
|
215
|
+
``` ruby
|
216
|
+
class SeoOptions < ActiveRecord::TablelessModel
|
217
|
+
|
218
|
+
attribute :title_tag, :type => :string, :default => ""
|
219
|
+
attribute :meta_description, :type => :string, :default => ""
|
220
|
+
attribute :meta_keywords, :type => :string, :default => ""
|
221
|
+
attribute :noindex, :type => :boolean, :default => false
|
222
|
+
attribute :nofollow, :type => :boolean, :default => false
|
223
|
+
attribute :noarchive, :type => :boolean, :default => false
|
224
|
+
|
225
|
+
validates_presence_of :meta_keywords
|
226
|
+
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
Testing:
|
231
|
+
|
232
|
+
``` ruby
|
233
|
+
x = SeoOptionsSettings.new
|
234
|
+
=> <#SeoOptions meta_description="" meta_keywords="" noarchive=false nofollow=false noindex=false title_tag="">
|
235
|
+
|
236
|
+
x.valid?
|
237
|
+
=> false
|
238
|
+
|
239
|
+
x.meta_keywords = "test"
|
240
|
+
=> "test"
|
241
|
+
|
242
|
+
x.valid?
|
243
|
+
=> true
|
244
|
+
```
|
245
|
+
|
246
|
+
## TODO
|
247
|
+
|
248
|
+
* Update validations syntax to that of Rails 3
|
249
|
+
* Support for associations
|
250
|
+
|
251
|
+
|
252
|
+
## Authors
|
253
|
+
|
254
|
+
* Vito Botta ( http://vitobotta.com )
|
255
|
+
|
256
|
+
## Contributing
|
257
|
+
|
258
|
+
* Fork the project on Github.
|
259
|
+
* Make your feature addition or bug fix.
|
260
|
+
* Add specs for it, making sure all specs green.
|
261
|
+
* Commit, and send me a pull request.
|
262
|
+
|
263
|
+
## Compatibility
|
264
|
+
|
265
|
+
* Tested with REE/1.8.7, 1.9.2
|
266
|
+
|
267
|
+
## Change log
|
268
|
+
|
269
|
+
24.07.2011 - Added support for passing Proc/lamba when defining the default attribute of a value
|
270
|
+
|
271
|
+
## License
|
272
|
+
|
273
|
+
MIT License. Copyright 2010 Vito Botta. http://vitobotta.com
|
274
|
+
|
@@ -1,6 +1,9 @@
|
|
1
1
|
module Base
|
2
2
|
module ClassMethods
|
3
3
|
|
4
|
+
attr_reader :tableless_models
|
5
|
+
|
6
|
+
|
4
7
|
#
|
5
8
|
#
|
6
9
|
# Macro to attach a tableless model to a parent, table-based model.
|
@@ -24,6 +27,10 @@ module Base
|
|
24
27
|
#
|
25
28
|
def has_tableless(column)
|
26
29
|
column_name = column.class == Hash ? column.collect{|k,v| k}.first.to_sym : column
|
30
|
+
|
31
|
+
@tableless_models ||= []
|
32
|
+
@tableless_models << column_name
|
33
|
+
|
27
34
|
|
28
35
|
# if only the column name is given, the tableless model's class is expected to have that name, classified, as class name
|
29
36
|
class_type = column.class == Hash ? column.collect{|k,v| v}.last : column.to_s.classify.constantize
|
@@ -35,7 +42,7 @@ module Base
|
|
35
42
|
|
36
43
|
# Telling AR that the column has to store an instance of the given tableless model in
|
37
44
|
# YAML serialized format
|
38
|
-
serialize column_name
|
45
|
+
serialize column_name
|
39
46
|
|
40
47
|
# Adding getter for the serialized column,
|
41
48
|
# making sure it always returns an instance of the specified tableless
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class ActiveRecord::Base
|
2
|
+
#
|
3
|
+
#
|
4
|
+
# delegates method calls for unknown methods to the tableless model
|
5
|
+
#
|
6
|
+
def method_missing method, *args, &block
|
7
|
+
self.class.tableless_models.each do |column_name|
|
8
|
+
serialized_attribute = send(column_name)
|
9
|
+
return serialized_attribute.send(method, *args, &block) if serialized_attribute.respond_to?(method)
|
10
|
+
end
|
11
|
+
|
12
|
+
super method, *args, &block
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
data/lib/tableless_model.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
|
1
3
|
require "validatable"
|
2
|
-
require
|
4
|
+
require "activerecord/base/class_methods"
|
5
|
+
require "activerecord/base/instance_methods"
|
6
|
+
require "tableless_model/class_methods"
|
7
|
+
require "tableless_model/instance_methods"
|
8
|
+
require "tableless_model/version"
|
3
9
|
|
4
|
-
Dir[File.join(File.dirname(__FILE__), "tableless_model/*rb")].each {|f| require File.expand_path(f)}
|
5
10
|
|
6
11
|
module ActiveRecord
|
7
12
|
|
@@ -15,21 +15,20 @@ module Tableless
|
|
15
15
|
#
|
16
16
|
#
|
17
17
|
def attribute(name, options = {})
|
18
|
-
# Stringifies all keys... uses a little more memory but it's a bit easier to handle keys internally...
|
19
18
|
attribute_name = name.to_s
|
20
19
|
|
21
20
|
# Defining the new attribute for the tableless model
|
22
|
-
self.attributes[
|
21
|
+
self.attributes[name] = options
|
23
22
|
|
24
23
|
# Defining method-like getter and setter for the new attribute
|
25
24
|
# so it can be used like a regular object's property
|
26
25
|
class_eval %Q{
|
27
26
|
def #{attribute_name}
|
28
|
-
self[
|
27
|
+
self[:#{attribute_name}]
|
29
28
|
end
|
30
29
|
|
31
30
|
def #{attribute_name}=(value)
|
32
|
-
self[
|
31
|
+
self[:#{attribute_name}] = value
|
33
32
|
end
|
34
33
|
}
|
35
34
|
end
|
@@ -53,7 +52,7 @@ module Tableless
|
|
53
52
|
#
|
54
53
|
#
|
55
54
|
def cast(attribute_name, value)
|
56
|
-
type = self.attributes[attribute_name
|
55
|
+
type = self.attributes[attribute_name][:type] || :string
|
57
56
|
|
58
57
|
return nil if value.nil? && ![:string, :integer, :boolean, :float, :decimal].include?(type)
|
59
58
|
|
@@ -12,8 +12,8 @@ module Tableless
|
|
12
12
|
def initialize(init_attributes = {}, &block)
|
13
13
|
super &block
|
14
14
|
|
15
|
-
self.class.attributes.each_pair {|attribute_name, options| self.send("#{attribute_name}=", options[:default])}
|
16
|
-
init_attributes.each_pair {|k,v| self
|
15
|
+
self.class.attributes.each_pair {|attribute_name, options| self.send("#{attribute_name}=".to_sym, options[:default].is_a?(Proc) ? options[:default].call : options[:default])}
|
16
|
+
init_attributes.each_pair {|k,v| self[k] = v } if init_attributes
|
17
17
|
end
|
18
18
|
|
19
19
|
|
@@ -34,8 +34,9 @@ module Tableless
|
|
34
34
|
# so that only the defined attributes can be read
|
35
35
|
#
|
36
36
|
def [](attribute_name)
|
37
|
-
raise NoMethodError, "The attribute #{attribute_name} is undefined" unless self.class.attributes.has_key? attribute_name
|
38
|
-
|
37
|
+
raise NoMethodError, "The attribute #{attribute_name} is undefined" unless self.class.attributes.has_key? attribute_name
|
38
|
+
default = super(attribute_name)
|
39
|
+
self.class.cast(attribute_name, default.is_a?(Proc) ? default.call : default)
|
39
40
|
end
|
40
41
|
|
41
42
|
|
@@ -45,9 +46,9 @@ module Tableless
|
|
45
46
|
# so that only the defined attributes can be set
|
46
47
|
#
|
47
48
|
def []=(attribute_name, value)
|
48
|
-
raise NoMethodError, "The attribute #{attribute_name} is undefined" unless self.class.attributes.has_key? attribute_name
|
49
|
+
raise NoMethodError, "The attribute #{attribute_name} is undefined" unless self.class.attributes.has_key? attribute_name
|
49
50
|
|
50
|
-
return_value = super(attribute_name
|
51
|
+
return_value = super(attribute_name, self.class.cast(attribute_name, value))
|
51
52
|
|
52
53
|
if self.__owner_object
|
53
54
|
# This makes the tableless model compatible with partial_updates:
|
@@ -72,7 +73,10 @@ module Tableless
|
|
72
73
|
# "<#MyTablelessModel a=1 b=2>"
|
73
74
|
#
|
74
75
|
def inspect
|
75
|
-
"<##{self.class.to_s}" << self.keys.sort
|
76
|
+
"<##{self.class.to_s}" << self.keys.sort{ |a,b| a.to_s <=> b.to_s }.inject("") do |result, k|
|
77
|
+
result += " #{k}=#{ self[k].is_a?(Time) ? self[k].strftime("%Y-%m-%d %H:%M:%S %Z") : self[k].inspect}"
|
78
|
+
result
|
79
|
+
end + ">"
|
76
80
|
end
|
77
81
|
|
78
82
|
|
@@ -85,5 +89,23 @@ module Tableless
|
|
85
89
|
raise NoMethodError
|
86
90
|
end
|
87
91
|
|
92
|
+
|
93
|
+
#
|
94
|
+
# allows calls to attribute_name?, returning a true or false depending
|
95
|
+
# on whether the actual value of the attribute is truthy or falsy
|
96
|
+
#
|
97
|
+
def method_missing sym, *args, &block
|
98
|
+
attribute_name = sym.to_s.gsub(/^(.*)\?$/, "\\1")
|
99
|
+
if respond_to?(attribute_name)
|
100
|
+
!!send(attribute_name, *args, &block)
|
101
|
+
else
|
102
|
+
super sym, *args, &block
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def respond_to?(method)
|
107
|
+
super || self.class.attributes.keys.include?(method) || self.class.attributes.keys.include?("#{method}=".to_sym) || super(method.to_s.gsub(/^(.*)\?$/, "\\1"))
|
108
|
+
end
|
109
|
+
|
88
110
|
end
|
89
111
|
end
|