extend_at 0.0.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/.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: []