extend_at 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .directory
2
+ *~
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in acts_as_configuration.gemspec
4
+ gemspec
data/MIT-license.txt ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2011 Andrés Borek
2
+
3
+ Permission is hereby granted, free of charge, to any
4
+ person obtaining a copy of this software and associated
5
+ documentation files (the "Software"), to deal in the
6
+ Software without restriction, including without limitation
7
+ the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the
9
+ Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice
13
+ shall be included in all copies or substantial portions of
14
+ the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
19
+ PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
20
+ OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
21
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
22
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,208 @@
1
+ # Extend at
2
+
3
+ This gem allows you to extend the columns from your model without migrations, you can, i.e., develop your own content types, like in Drupal
4
+
5
+ ## Installation
6
+
7
+ <code>gem install extend_at</code>
8
+
9
+ ### Rails 3
10
+ Add in your Gemfile:
11
+ <code>gem 'extend_at', :git => 'git://github.com/anga/extend_at.git'</code>
12
+
13
+ ## Usage
14
+
15
+ Only you need to add the next line in your model.
16
+
17
+ <code>extend_at :configuration</code>
18
+
19
+ For example:
20
+
21
+ class User < ActiveRecord::Base
22
+ extend_at :extra
23
+ end
24
+
25
+ Now you can write your configuration like:
26
+
27
+ user.extra.private_photos = true
28
+ user.extra.subscribe_to_news = false
29
+ user.extra.perfil_description = ''
30
+ user.save
31
+
32
+ ### Columns configuration
33
+
34
+ You can configurate each column.
35
+
36
+ #### Set column type
37
+
38
+ You can set the type of the colum.
39
+
40
+ class User < ActiveRecord::Base
41
+ extend_at :extra, :columns => {
42
+ :private_photos => {
43
+ :type => :boolean
44
+ }, :age => {
45
+ :type => :get_type
46
+ }, :perfil_description => {
47
+ :type => lambda {
48
+ String
49
+ }
50
+ }, :last_loggin => {
51
+ :type => Time.now.class
52
+ }, :subscribe_to_rss => :get_rss_config
53
+ }
54
+
55
+ protected
56
+ def get_type
57
+ Fixnum
58
+ end
59
+
60
+ def get_rss_config
61
+ {
62
+ :type => :boolean
63
+ }
64
+ end
65
+ end
66
+
67
+ You can use any class, but if you need use boolean values, you must use :boolean.
68
+
69
+ #### Set default value
70
+
71
+ class User < ActiveRecord::Base
72
+ extend_at :extra, :columns => {
73
+ :private_photos => {
74
+ :type => :boolean,
75
+ :default => true
76
+ }, :age => {
77
+ :type => :get_type,
78
+ :default => 1
79
+ }, :perfil_description => {
80
+ :type => lambda {
81
+ String
82
+ },
83
+ :default => :get_default_perfil_description
84
+ }, :last_loggin => {
85
+ :type => Time.now.class,
86
+ :default => lambda {
87
+ self.created_at.time
88
+ }
89
+ }, :subscribe_to_rss => :get_rss_config
90
+ }
91
+
92
+ protected
93
+ def get_type
94
+ Fixnum
95
+ end
96
+
97
+ def get_rss_config
98
+ {
99
+ :type => :boolean,
100
+ :default => true
101
+ }
102
+ end
103
+
104
+ def get_default_perfil_description
105
+ Description.where(:user_id => self.id).default
106
+ end
107
+ end
108
+
109
+ #### Set validation
110
+ class User < ActiveRecord::Base
111
+ extend_at :extra, :columns => {
112
+ :private_photos => {
113
+ :type => :boolean,
114
+ :default => true
115
+ }, :age => {
116
+ :type => :get_type,
117
+ :default => 1,
118
+ :validate => lambda {
119
+ |age|
120
+ errors.add :config_age, "Are you Matusalén?" if age > 150
121
+ errors.add :config_age, "Are you a fetus?" if age <= 0
122
+ }
123
+ }, :perfil_description => {
124
+ :type => lambda {
125
+ String
126
+ },
127
+ :default => :get_default_perfil_description,
128
+ :lambda => :must_not_have_strong_language
129
+ }, :last_loggin => {
130
+ :type => Time.now.class,
131
+ :default => lambda {
132
+ self.created_at.time
133
+ },
134
+ :validate => lambda {
135
+ |time|
136
+ errors.add :config_last_loggin, "You can't loggin in the future" if time > Time.now
137
+ }
138
+ }, :subscribe_to_rss => :get_rss_config
139
+ }
140
+
141
+ protected
142
+ STRONG_WORD = [
143
+ #...
144
+ ]
145
+
146
+ def get_type
147
+ Fixnum
148
+ end
149
+
150
+ def get_rss_config
151
+ {
152
+ :type => :boolean,
153
+ :default => true
154
+ }
155
+ end
156
+
157
+ def get_default_perfil_description
158
+ Description.where(:user_id => self.id).default
159
+ end
160
+
161
+ def must_not_have_strong_language(desc)
162
+ errors.add :cofig_perfil_description, "You must not have strong language" if desc =~ /(#{STRONG_WORD.join('|')})/
163
+ end
164
+ end
165
+
166
+ ### Integration in the views
167
+
168
+ If you like to use come configuration variable in your views you only need put the name of the input like <code>:config_name</code>, for example:
169
+
170
+ <% form_for(@user) do |f| %>
171
+ ...
172
+ <div class="field">
173
+ <%= f.label :extra_private_photos %><br />
174
+ <%= f.check_box :extra_private_photos %>
175
+ </div>
176
+ ...
177
+ <% end %>
178
+
179
+ ### Tips
180
+
181
+ If you like to do something more dynamic, like create columns and validations depending of some model or configuration, the you can do something like this:
182
+
183
+ class User < ActiveRecord::Base
184
+ extend_at :extra, :columns => :get_columns
185
+ serialize :columns_name
186
+
187
+ protected
188
+ def get_columns
189
+ columns = {}
190
+ columns_name.each do |name|
191
+ config = ColumConfig.where(:user_id => self.id, :column => name).first
192
+ columns[name.to_sym] = {
193
+ :type => eval(config.class_type),
194
+ :default => config.default_value,
195
+ :validate => get_validation(config.validation)
196
+ }
197
+ end
198
+
199
+ columns
200
+ end
201
+
202
+ # Accept a name of a validation and return the Proc with the validation code
203
+ def get_validation(validation_type)
204
+ # ...
205
+ end
206
+ end
207
+
208
+ This code read the configuration of the columns when you acces to extra column
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/extend_at.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "extend_at/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "extend_at"
7
+ s.version = ExtendModelAt::VERSION
8
+ s.authors = ["Andrés José Borek"]
9
+ s.email = ["andres.b.dev@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Create dynamic fields in your models (like content types in Drupal)}
12
+ s.description = %q{This gem allows you to extend the columns from your model without migrations, you can, i.e., develop your own content types, like in Drupal}
13
+
14
+ s.rubyforge_project = "extend_at"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "rspec"
22
+ end
@@ -0,0 +1,3 @@
1
+ module ExtendModelAt
2
+ VERSION = "0.0.1"
3
+ end
data/lib/extend_at.rb ADDED
@@ -0,0 +1,294 @@
1
+ # encoding: utf-8
2
+ require "extend_at/version"
3
+
4
+ module ExtendModelAt
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ # The object how controll the data
10
+ class Extention
11
+ def initialize(options={})
12
+ @model = options[:model]
13
+ @column_name = options[:column_name].to_s
14
+ @columns = options[:columns]
15
+ @value = get_defaults_values options
16
+
17
+ raise "#{@column_name} should by text or string not #{options[:model].column_for_attribute(@column_name.to_sym).type}" if not [:text, :stiring].include? options[:model].column_for_attribute(@column_name.to_sym).type
18
+
19
+ out = YAML.parse(@model[@column_name].to_s)
20
+ if out == false
21
+ db_value = nil
22
+ else
23
+ db_value = out.to_ruby
24
+ end
25
+ @value.merge! db_value if db_value.kind_of? Hash
26
+
27
+ initialize_values
28
+
29
+ @model.attributes[@column_name] = @value
30
+ @model.save(:validate => false)
31
+ end
32
+
33
+ def [](key)
34
+ @value[key.to_s]
35
+ end
36
+
37
+ def []=(key, value)
38
+ if @columns[key.to_sym].kind_of? Hash and ((@columns[key.to_sym][:type] == :boolean and (not [true.class, false.class].include? value.class)) or
39
+ ((not [:boolean, nil].include?(@columns[key.to_sym][:type])) and @columns[key.to_sym][:type] != value.class ))
40
+ raise "#{value.inspect} is not a valid type, expected #{@columns[key.to_sym][:type]}"
41
+ end
42
+ @value[key.to_s] = value
43
+ @model.send :"#{@column_name}=", @value.to_yaml
44
+ end
45
+
46
+ # Use the undefined method as a column
47
+ def method_missing(m, *args, &block)
48
+ # If the method don't finish with "=" is fore read
49
+ if m !~ /\=$/
50
+ self[m.to_s]
51
+ # but if finish with "=" is for wirte
52
+ else
53
+ column_name = m.to_s.gsub(/\=$/, '')
54
+ self[column_name.to_s] = args.first
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def initialize_values
61
+ if not @value.kind_of? Hash
62
+ @model.attributes[@column_name] = {}.to_yaml
63
+ @model.save
64
+ end
65
+ end
66
+
67
+ def get_defaults_values(options = {})
68
+ defaults_ = {}
69
+ options[:columns].each do |column, config|
70
+ defaults_[column.to_s] = @columns[column.to_sym][:default] || nil
71
+ end
72
+ defaults_
73
+ end
74
+ end
75
+
76
+ module ClassMethods
77
+ def extend_at(column_name, options = {})
78
+ assign_attributes_eval = "
79
+ # Rewrite the mass assignment method because we need to accept write code like User.new :config_born => 10.years.ago
80
+ def assign_attributes(attributes = nil, options = {})
81
+ attributes.each_pair do |key, value|
82
+ if key.to_s =~ /^#{column_name}_/
83
+ rb = \"#{column_name}.\#\{key.to_s.gsub(/^#{column_name}_/,'')\} = value\"
84
+ eval rb, binding
85
+ end
86
+ end
87
+ attributes.delete_if do |key,value|
88
+ key.to_s =~ /^#{column_name}_/
89
+ end
90
+ super attributes, options
91
+ end
92
+
93
+ # Return the value of <<attributes>> methods like <column name>_<extended column name>
94
+ def [](column)
95
+ if column.to_s =~ /^#{column_name}_[a-zA-Z_][a-zA-Z_0-9]*\=?$/
96
+ rb = \"#{column_name}.\#\{column.to_s.gsub(/^#{column_name}_/,'')\}\"
97
+ eval rb, binding
98
+ else
99
+ super
100
+ end
101
+ end
102
+
103
+ # Write the value of <<attributes>> methods like <column name>_<extended column name>
104
+ def []=(column, value)
105
+ if column.to_s =~ /^#{column_name}_[a-zA-Z_][a-zA-Z_0-9]*\=?$/
106
+ rb = \"#{column_name}.\#\{column\} = value\"
107
+ eval rb, binding
108
+ else
109
+ super
110
+ end
111
+ end
112
+
113
+ # Respond to ethod like <column name>_<extended column name> for read or write
114
+ def self.respond_to?(symbol, include_private=false)
115
+ if symbol.to_s =~ /^#{column_name}_[a-zA-Z_][a-zA-Z_0-9]*\=?$/
116
+ return true
117
+ else
118
+ super
119
+ end
120
+ end
121
+
122
+ # Accept method like <column name>_<extended column name> for read or write
123
+ def method_missing(m, *args, &block)
124
+ if m.to_s =~ /^#{column_name}_[a-zA-Z_][a-zA-Z_0-9]*\=$/
125
+ rb = \"self.#{column_name}.\#\{m.to_s.gsub(/^#{column_name}_/, '')} = args.first\"
126
+ return eval rb, binding
127
+ elsif m.to_s =~ /^#{column_name}_[a-zA-Z_][a-zA-Z_0-9]*$/
128
+ rb = \"self.#{column_name}.\#\{m.to_s.gsub(/^#{column_name}_/, '')}\"
129
+ return eval rb, binding
130
+ else
131
+ super
132
+ end
133
+ end
134
+ "
135
+
136
+ self.class_eval <<-EOS
137
+ eval assign_attributes_eval
138
+ EOS
139
+
140
+ class_eval <<-EOV
141
+ public
142
+ validate :extend_at_validations
143
+
144
+ def #{column_name.to_s}
145
+ if not @#{column_name.to_s}_configuration.kind_of? ExtendModelAt::Extention
146
+ opts = initialize_options(#{options})
147
+ options = {
148
+ :extensible => true # If is false, only the columns defined in :columns can be used
149
+ }.merge! opts
150
+ columns = initialize_columns expand_options(options, { :not_call_symbol => [:boolean], :not_expand => [:validate, :default] }) if options.kind_of? Hash
151
+ @#{column_name.to_s}_configuration ||= ExtendModelAt::Extention.new({:model => self, :column_name => :#{column_name.to_s}, :columns => columns})
152
+ end
153
+ @#{column_name.to_s}_configuration
154
+ end
155
+
156
+ protected
157
+ def extend_at_validations
158
+ self.#{column_name}.valid?
159
+ @extend_at_validation ||= {} if not @extend_at_validation.kind_of? Hash
160
+ @extend_at_validation.each do |column, validation|
161
+ if validation.kind_of? Symbol
162
+ self.send validation, eval("@#{column_name.to_s}_configuration.\#\{column.to_s\}")
163
+ elsif validation.kind_of? Proc
164
+ validation.call @#{column_name.to_s}_configuration[column.to_sym]
165
+ end
166
+ end
167
+ end
168
+
169
+ def initialize_options(options={})
170
+ opts = expand_options options, { :not_call_symbol => [:boolean], :not_expand => [:validate, :default] }
171
+ end
172
+
173
+ # Initialize each column configuration
174
+ def initialize_columns(options = {})
175
+ columns = {}
176
+ if options[:columns].kind_of? Hash
177
+ options[:columns].each do |column, config|
178
+ columns[column] = initialize_column column, config
179
+ end
180
+ elsif options[:columns].kind_of? Symbol
181
+ hash = self.send options[:columns]
182
+ raise "Invalid columns configuration" if not hash.kind_of? Hash
183
+ columns = initialize_columns :columns => hash
184
+ elsif options[:columns].kind_of? Proc
185
+ hash = options[:columns].call
186
+ raise "Invalid columns configuration" if not hash.kind_of? Hash
187
+ columns = initialize_columns :columns => hash
188
+ end
189
+ columns
190
+ end
191
+
192
+ def initialize_column(column,config={})
193
+ raise "The column \#\{column\} have an invalid configuration (\#\{config.class\} => \#\{config\})" if not config.kind_of? Hash
194
+ column = column.to_sym
195
+ column_config = {}
196
+
197
+ # Stablish the type
198
+ if config[:type].class == Class
199
+ # If exist :type, is a static column
200
+ column_config[:type] = config[:type]
201
+ else
202
+ # if not, is a dynamic column
203
+ if config[:type].to_sym == :any
204
+ column_config[:type] = nil
205
+ elsif config[:type].to_sym == :boolean
206
+ column_config[:type] = :boolean
207
+ else
208
+ raise "\#\{config[:type]\} is not a valid column type"
209
+ end
210
+ end
211
+
212
+ # Stablish the default value
213
+ # if is a symbol, we execute the function from the model
214
+ if config[:default].kind_of? Symbol
215
+ column_config[:default] = self.send(:config[:default])
216
+ elsif config[:default].kind_of? Proc
217
+ column_config[:default] = config[:default].call
218
+ else
219
+ # If the column have a type, we verify the type
220
+ if not column_config[:type].nil?
221
+ if (column_config[:type] == :boolean and (not [true.class, false.class].include? config[:default].class)) or
222
+ ((not [:boolean, nil].include?(column_config[:type])) and column_config[:type] != config[:default].class )
223
+ raise "The column \#\{column\} has an invalid default value. Expected \#\{column_config[:type]}, not \#\{config[:default].class}"
224
+ end
225
+ column_config[:default] = config[:default]
226
+ else
227
+ # If is dynamic, only we set the default value
228
+ column_config[:default] = config[:default]
229
+ end
230
+ end
231
+
232
+ # Set the validation
233
+ if [Symbol, Proc].include? config[:validate].class
234
+ column_config[:validate] = config[:validate]
235
+ create_validation_for column, config[:validate]
236
+ else
237
+ raise "The validation of \#\{column\} is invalid"
238
+ end
239
+
240
+
241
+ column_config
242
+ end
243
+
244
+ def create_validation_for(column, validation)
245
+ column = column.to_sym
246
+ @extend_at_validation ||= {}
247
+ @extend_at_validation[column] = validation
248
+ end
249
+
250
+ def expand_options(options={}, opts={})
251
+ config_opts = {
252
+ :not_expand => [],
253
+ :not_call_symbol => []
254
+ }.merge! opts
255
+ if options.kind_of? Hash
256
+ opts = {}
257
+ options.each do |column, config|
258
+ if not config_opts[:not_expand].include? column.to_sym
259
+ if not config_opts[:not_call_symbol].include? config
260
+ opts[column.to_sym] = expand_options(get_value_of(config), config_opts)
261
+ else
262
+ opts[column.to_sym] = expand_options(config, config_opts)
263
+ end
264
+ else
265
+ opts[column.to_sym] = config
266
+ end
267
+ end
268
+ return opts
269
+ else
270
+ return get_value_of options
271
+ end
272
+ end
273
+
274
+ def get_value_of(value)
275
+ if value.kind_of? Symbol
276
+ # If the function exist, we execute it
277
+ if self.respond_to? value
278
+ return self.send value
279
+ # if the the function not exist, whe set te symbol as a value
280
+ else
281
+ return value
282
+ end
283
+ elsif value.kind_of? Proc
284
+ return value.call
285
+ else
286
+ return value
287
+ end
288
+ end
289
+ EOV
290
+ end
291
+ end
292
+ end
293
+
294
+ ActiveRecord::Base.class_eval { include ExtendModelAt }
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: extend_at
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andrés José Borek
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &10360080 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *10360080
25
+ description: This gem allows you to extend the columns from your model without migrations,
26
+ you can, i.e., develop your own content types, like in Drupal
27
+ email:
28
+ - andres.b.dev@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - .gitignore
34
+ - Gemfile
35
+ - MIT-license.txt
36
+ - README.markdown
37
+ - Rakefile
38
+ - extend_at.gemspec
39
+ - lib/extend_at.rb
40
+ - lib/extend_at/version.rb
41
+ homepage: ''
42
+ licenses: []
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project: extend_at
61
+ rubygems_version: 1.8.11
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: Create dynamic fields in your models (like content types in Drupal)
65
+ test_files: []