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 ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile.lock CHANGED
@@ -1,27 +1,45 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tableless_model (0.0.4)
4
+ tableless_model (0.0.7)
5
5
  validatable
6
6
 
7
7
  GEM
8
8
  remote: http://rubygems.org/
9
9
  specs:
10
- ansi (1.2.2)
11
- ffi (1.0.4)
12
- rake (>= 0.8.7)
13
- minitest (2.0.2)
14
- rake (0.8.7)
15
- sqlite3 (0.1.1)
16
- ffi (>= 0.6.3)
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
- ansi
24
- minitest
41
+ activerecord
42
+ rspec
25
43
  sqlite3
26
44
  tableless_model!
27
- validatable
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, ActiveRecord::TablelessModel
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
+
@@ -1,7 +1,12 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+
1
3
  require "validatable"
2
- require File.expand_path(File.join(File.dirname(__FILE__), "activerecord/base/class_methods"))
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[attribute_name] = options
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["#{attribute_name}"]
27
+ self[:#{attribute_name}]
29
28
  end
30
29
 
31
30
  def #{attribute_name}=(value)
32
- self["#{attribute_name}"] = value
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.to_s][:type] || :string
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.send("#{k}=", v)} if init_attributes
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.to_s
38
- self.class.cast(attribute_name, super(attribute_name.to_s))
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.to_s
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.to_s, self.class.cast(attribute_name, value))
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.inject(""){|result, k| result << " #{k}=#{self[k].inspect}"; result } << ">"
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