hobo_fields 1.3.0.RC

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.
Files changed (43) hide show
  1. data/CHANGES.txt +38 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.txt +8 -0
  4. data/Rakefile +36 -0
  5. data/VERSION +1 -0
  6. data/bin/hobofields +19 -0
  7. data/hobo_fields.gemspec +31 -0
  8. data/lib/generators/hobo/migration/USAGE +47 -0
  9. data/lib/generators/hobo/migration/migration_generator.rb +162 -0
  10. data/lib/generators/hobo/migration/migrator.rb +445 -0
  11. data/lib/generators/hobo/migration/templates/migration.rb.erb +9 -0
  12. data/lib/generators/hobo/model/USAGE +19 -0
  13. data/lib/generators/hobo/model/model_generator.rb +11 -0
  14. data/lib/generators/hobo/model/templates/model_injection.rb.erb +18 -0
  15. data/lib/hobo_fields/extensions/active_record/attribute_methods.rb +48 -0
  16. data/lib/hobo_fields/extensions/active_record/fields_declaration.rb +21 -0
  17. data/lib/hobo_fields/field_declaration_dsl.rb +33 -0
  18. data/lib/hobo_fields/model/field_spec.rb +121 -0
  19. data/lib/hobo_fields/model/index_spec.rb +47 -0
  20. data/lib/hobo_fields/model.rb +226 -0
  21. data/lib/hobo_fields/railtie.rb +13 -0
  22. data/lib/hobo_fields/sanitize_html.rb +23 -0
  23. data/lib/hobo_fields/types/email_address.rb +26 -0
  24. data/lib/hobo_fields/types/enum_string.rb +101 -0
  25. data/lib/hobo_fields/types/html_string.rb +15 -0
  26. data/lib/hobo_fields/types/lifecycle_state.rb +16 -0
  27. data/lib/hobo_fields/types/markdown_string.rb +15 -0
  28. data/lib/hobo_fields/types/password_string.rb +15 -0
  29. data/lib/hobo_fields/types/raw_html_string.rb +13 -0
  30. data/lib/hobo_fields/types/raw_markdown_string.rb +13 -0
  31. data/lib/hobo_fields/types/serialized_object.rb +15 -0
  32. data/lib/hobo_fields/types/text.rb +16 -0
  33. data/lib/hobo_fields/types/textile_string.rb +22 -0
  34. data/lib/hobo_fields.rb +94 -0
  35. data/test/api.rdoctest +244 -0
  36. data/test/doc-only.rdoctest +96 -0
  37. data/test/generators.rdoctest +53 -0
  38. data/test/interactive_primary_key.rdoctest +54 -0
  39. data/test/migration_generator.rdoctest +639 -0
  40. data/test/migration_generator_comments.rdoctest +75 -0
  41. data/test/prepare_testapp.rb +8 -0
  42. data/test/rich_types.rdoctest +394 -0
  43. metadata +140 -0
@@ -0,0 +1,445 @@
1
+ module Generators
2
+ module Hobo
3
+ module Migration
4
+
5
+ class HabtmModelShim < Struct.new(:join_table, :foreign_keys, :connection)
6
+
7
+ def self.from_reflection(refl)
8
+ result = self.new
9
+ result.join_table = refl.options[:join_table].to_s
10
+ result.foreign_keys = [refl.primary_key_name.to_s, refl.association_foreign_key.to_s].sort
11
+ # this may fail in weird ways if HABTM is running across two DB connections (assuming that's even supported)
12
+ # figure that anybody who sets THAT up can deal with their own migrations...
13
+ result.connection = refl.active_record.connection
14
+ result
15
+ end
16
+
17
+ def table_name
18
+ self.join_table
19
+ end
20
+
21
+ def table_exists?
22
+ ActiveRecord::Migration.table_exists? table_name
23
+ end
24
+
25
+ def field_specs
26
+ i = 0
27
+ foreign_keys.inject({}) do |h, v|
28
+ # some trickery to avoid an infinite loop when FieldSpec#initialize tries to call model.field_specs
29
+ h[v] = HoboFields::Model::FieldSpec.new(self, v, :integer, :position => i)
30
+ i += 1
31
+ h
32
+ end
33
+ end
34
+
35
+ def primary_key
36
+ false
37
+ end
38
+
39
+ def index_specs
40
+ []
41
+ end
42
+
43
+ end
44
+
45
+ class Migrator
46
+
47
+ class Error < RuntimeError; end
48
+
49
+ @ignore_models = []
50
+ @ignore_tables = []
51
+
52
+ class << self
53
+ attr_accessor :ignore_models, :ignore_tables, :disable_indexing
54
+ end
55
+
56
+ def self.run(renames={})
57
+ g = Migrator.new
58
+ g.renames = renames
59
+ g.generate
60
+ end
61
+
62
+ def self.default_migration_name
63
+ existing = Dir["#{Rails.root}/db/migrate/*hobo_migration*"]
64
+ max = existing.grep(/([0-9]+)\.rb$/) { $1.to_i }.max
65
+ n = max ? max + 1 : 1
66
+ "hobo_migration_#{n}"
67
+ end
68
+
69
+ def initialize(ambiguity_resolver={})
70
+ @ambiguity_resolver = ambiguity_resolver
71
+ @drops = []
72
+ @renames = nil
73
+ end
74
+
75
+ attr_accessor :renames
76
+
77
+
78
+ def load_rails_models
79
+ if defined? Rails.root
80
+ Dir["#{Rails.root}/app/models/**/[a-z0-9_]*.rb"].each do |f|
81
+ _, filename = *f.match(%r{/app/models/([_a-z0-9/]*).rb$})
82
+ filename.camelize.constantize
83
+ end
84
+ end
85
+ end
86
+
87
+
88
+ # Returns an array of model classes that *directly* extend
89
+ # ActiveRecord::Base, excluding anything in the CGI module
90
+ def table_model_classes
91
+ load_rails_models
92
+ ActiveRecord::Base.send(:descendants).reject {|c| (c.base_class != c) || c.name.starts_with?("CGI::") }
93
+ end
94
+
95
+
96
+ def self.connection
97
+ ActiveRecord::Base.connection
98
+ end
99
+ def connection; self.class.connection; end
100
+
101
+
102
+ def self.fix_native_types(types)
103
+ case connection.class.name
104
+ when /mysql/i
105
+ types[:integer][:limit] ||= 11
106
+ end
107
+ types
108
+ end
109
+
110
+ def self.native_types
111
+ @native_types ||= fix_native_types connection.native_database_types
112
+ end
113
+ def native_types; self.class.native_types; end
114
+
115
+ # list habtm join tables
116
+ def habtm_tables
117
+ reflections = Hash.new { |h, k| h[k] = Array.new }
118
+ ActiveRecord::Base.send(:descendants).map do |c|
119
+ c.reflect_on_all_associations(:has_and_belongs_to_many).each do |a|
120
+ reflections[a.options[:join_table].to_s] << a
121
+ end
122
+ end
123
+ reflections
124
+ end
125
+
126
+ # Returns an array of model classes and an array of table names
127
+ # that generation needs to take into account
128
+ def models_and_tables
129
+ ignore_model_names = Migrator.ignore_models.*.to_s.*.underscore
130
+ all_models = table_model_classes
131
+ hobo_models = all_models.select { |m| m.try.include_in_migration && m.name.underscore.not_in?(ignore_model_names) }
132
+ non_hobo_models = all_models - hobo_models
133
+ db_tables = connection.tables - Migrator.ignore_tables.*.to_s - non_hobo_models.*.table_name
134
+ [hobo_models, db_tables]
135
+ end
136
+
137
+
138
+ # return a hash of table renames and modifies the passed arrays so
139
+ # that renamed tables are no longer listed as to_create or to_drop
140
+ def extract_table_renames!(to_create, to_drop)
141
+ if renames
142
+ # A hash of table renames has been provided
143
+
144
+ to_rename = {}
145
+ renames.each_pair do |old_name, new_name|
146
+ new_name = new_name[:table_name] if new_name.is_a?(Hash)
147
+ next unless new_name
148
+
149
+ if to_create.delete(new_name.to_s) && to_drop.delete(old_name.to_s)
150
+ to_rename[old_name.to_s] = new_name.to_s
151
+ else
152
+ raise Error, "Invalid table rename specified: #{old_name} => #{new_name}"
153
+ end
154
+ end
155
+ to_rename
156
+
157
+ elsif @ambiguity_resolver
158
+ @ambiguity_resolver.call(to_create, to_drop, "table", nil)
159
+
160
+ else
161
+ raise Error, "Unable to resolve migration ambiguities"
162
+ end
163
+ end
164
+
165
+
166
+ def extract_column_renames!(to_add, to_remove, table_name)
167
+ if renames
168
+ to_rename = {}
169
+ column_renames = renames._?[table_name.to_sym]
170
+ if column_renames
171
+ # A hash of table renames has been provided
172
+
173
+ column_renames.each_pair do |old_name, new_name|
174
+ if to_add.delete(new_name.to_s) && to_remove.delete(old_name.to_s)
175
+ to_rename[old_name.to_s] = new_name.to_s
176
+ else
177
+ raise Error, "Invalid rename specified: #{old_name} => #{new_name}"
178
+ end
179
+ end
180
+ end
181
+ to_rename
182
+
183
+ elsif @ambiguity_resolver
184
+ @ambiguity_resolver.call(to_add, to_remove, "column", "#{table_name}.")
185
+
186
+ else
187
+ raise Error, "Unable to resolve migration ambiguities in table #{table_name}"
188
+ end
189
+ end
190
+
191
+
192
+ def always_ignore_tables
193
+ # TODO: figure out how to do this in a sane way and be compatible with 2.2 and 2.3 - class has moved
194
+ sessions_table = CGI::Session::ActiveRecordStore::Session.table_name if
195
+ defined?(CGI::Session::ActiveRecordStore::Session) &&
196
+ defined?(ActionController::Base) &&
197
+ ActionController::Base.session_store == CGI::Session::ActiveRecordStore
198
+ ['schema_info', 'schema_migrations', sessions_table].compact
199
+ end
200
+
201
+
202
+ def generate
203
+ models, db_tables = models_and_tables
204
+ models_by_table_name = {}
205
+ models.each do |m|
206
+ if !models_by_table_name.has_key?(m.table_name)
207
+ models_by_table_name[m.table_name] = m
208
+ elsif m.superclass==models_by_table_name[m.table_name].superclass.superclass
209
+ # we need to ensure that models_by_table_name contains the
210
+ # base class in an STI hierarchy
211
+ models_by_table_name[m.table_name] = m
212
+ end
213
+ end
214
+ # generate shims for HABTM models
215
+ habtm_tables.each do |name, refls|
216
+ models_by_table_name[name] = HabtmModelShim.from_reflection(refls.first)
217
+ end
218
+ model_table_names = models_by_table_name.keys
219
+
220
+ to_create = model_table_names - db_tables
221
+ to_drop = db_tables - model_table_names - always_ignore_tables
222
+ to_change = model_table_names
223
+
224
+ to_rename = extract_table_renames!(to_create, to_drop)
225
+
226
+ renames = to_rename.map do |old_name, new_name|
227
+ "rename_table :#{old_name}, :#{new_name}"
228
+ end * "\n"
229
+ undo_renames = to_rename.map do |old_name, new_name|
230
+ "rename_table :#{new_name}, :#{old_name}"
231
+ end * "\n"
232
+
233
+ drops = to_drop.map do |t|
234
+ "drop_table :#{t}"
235
+ end * "\n"
236
+ undo_drops = to_drop.map do |t|
237
+ revert_table(t)
238
+ end * "\n\n"
239
+
240
+ creates = to_create.map do |t|
241
+ create_table(models_by_table_name[t])
242
+ end * "\n\n"
243
+ undo_creates = to_create.map do |t|
244
+ "drop_table :#{t}"
245
+ end * "\n"
246
+
247
+ changes = []
248
+ undo_changes = []
249
+ index_changes = []
250
+ undo_index_changes = []
251
+ to_change.each do |t|
252
+ model = models_by_table_name[t]
253
+ table = to_rename.key(t) || model.table_name
254
+ if table.in?(db_tables)
255
+ change, undo, index_change, undo_index = change_table(model, table)
256
+ changes << change
257
+ undo_changes << undo
258
+ index_changes << index_change
259
+ undo_index_changes << undo_index
260
+ end
261
+ end
262
+
263
+ up = [renames, drops, creates, changes, index_changes].flatten.reject(&:blank?) * "\n\n"
264
+ down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes].flatten.reject(&:blank?) * "\n\n"
265
+
266
+ [up, down]
267
+ end
268
+
269
+ def create_table(model)
270
+ longest_field_name = model.field_specs.values.map { |f| f.sql_type.to_s.length }.max
271
+ if model.primary_key != "id"
272
+ if model.primary_key
273
+ primary_key_option = ", :primary_key => :#{model.primary_key}"
274
+ else
275
+ primary_key_option = ", :id => false"
276
+ end
277
+ end
278
+ (["create_table :#{model.table_name}#{primary_key_option} do |t|"] +
279
+ model.field_specs.values.sort_by{|f| f.position}.map {|f| create_field(f, longest_field_name)} +
280
+ ["end"] + (Migrator.disable_indexing ? [] : create_indexes(model))) * "\n"
281
+ end
282
+
283
+ def create_indexes(model)
284
+ model.index_specs.map { |i| i.to_add_statement(model.table_name) }
285
+ end
286
+
287
+ def create_field(field_spec, field_name_width)
288
+ args = [field_spec.name.inspect] + format_options(field_spec.options, field_spec.sql_type)
289
+ " t.%-*s %s" % [field_name_width, field_spec.sql_type, args.join(', ')]
290
+ end
291
+
292
+ def change_table(model, current_table_name)
293
+ new_table_name = model.table_name
294
+
295
+ db_columns = model.connection.columns(current_table_name).index_by{|c|c.name}
296
+ key_missing = db_columns[model.primary_key].nil? && model.primary_key
297
+ db_columns -= [model.primary_key]
298
+
299
+ model_column_names = model.field_specs.keys.*.to_s
300
+ db_column_names = db_columns.keys.*.to_s
301
+
302
+ to_add = model_column_names - db_column_names
303
+ to_add += [model.primary_key] if key_missing && model.primary_key
304
+ to_remove = db_column_names - model_column_names
305
+ to_remove = to_remove - [model.primary_key.to_sym] if model.primary_key
306
+
307
+ to_rename = extract_column_renames!(to_add, to_remove, new_table_name)
308
+
309
+ db_column_names -= to_rename.keys
310
+ db_column_names |= to_rename.values
311
+ to_change = db_column_names & model_column_names
312
+
313
+ renames = to_rename.map do |old_name, new_name|
314
+ "rename_column :#{new_table_name}, :#{old_name}, :#{new_name}"
315
+ end
316
+ undo_renames = to_rename.map do |old_name, new_name|
317
+ "rename_column :#{new_table_name}, :#{new_name}, :#{old_name}"
318
+ end
319
+
320
+ to_add = to_add.sort_by {|c| model.field_specs[c].position }
321
+ adds = to_add.map do |c|
322
+ spec = model.field_specs[c]
323
+ args = [":#{spec.sql_type}"] + format_options(spec.options, spec.sql_type)
324
+ "add_column :#{new_table_name}, :#{c}, #{args * ', '}"
325
+ end
326
+ undo_adds = to_add.map do |c|
327
+ "remove_column :#{new_table_name}, :#{c}"
328
+ end
329
+
330
+ removes = to_remove.map do |c|
331
+ "remove_column :#{new_table_name}, :#{c}"
332
+ end
333
+ undo_removes = to_remove.map do |c|
334
+ revert_column(current_table_name, c)
335
+ end
336
+
337
+ old_names = to_rename.invert
338
+ changes = []
339
+ undo_changes = []
340
+ to_change.each do |c|
341
+ col_name = old_names[c] || c
342
+ col = db_columns[col_name]
343
+ spec = model.field_specs[c]
344
+ if spec.different_to?(col)
345
+ change_spec = {}
346
+ change_spec[:limit] = spec.limit unless spec.limit.nil? && col.limit.nil?
347
+ change_spec[:precision] = spec.precision unless spec.precision.nil?
348
+ change_spec[:scale] = spec.scale unless spec.scale.nil?
349
+ change_spec[:null] = spec.null unless spec.null && col.null
350
+ change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
351
+ change_spec[:comment] = spec.comment unless spec.comment.nil? && col.try.comment.nil?
352
+
353
+ changes << "change_column :#{new_table_name}, :#{c}, " +
354
+ ([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, true)).join(", ")
355
+ back = change_column_back(current_table_name, col_name)
356
+ undo_changes << back unless back.blank?
357
+ else
358
+ nil
359
+ end
360
+ end.compact
361
+
362
+ index_changes, undo_index_changes = change_indexes(model, current_table_name)
363
+
364
+ [(renames + adds + removes + changes) * "\n",
365
+ (undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
366
+ index_changes * "\n",
367
+ undo_index_changes * "\n"]
368
+ end
369
+
370
+ def change_indexes(model, old_table_name)
371
+ return [[],[]] if Migrator.disable_indexing || model.is_a?(HabtmModelShim)
372
+ new_table_name = model.table_name
373
+ existing_indexes = HoboFields::Model::IndexSpec.for_model(model, old_table_name)
374
+ model_indexes = model.index_specs
375
+ add_indexes = model_indexes - existing_indexes
376
+ drop_indexes = existing_indexes - model_indexes
377
+ undo_add_indexes = []
378
+ undo_drop_indexes = []
379
+ add_indexes.map! do |i|
380
+ undo_add_indexes << drop_index(old_table_name, i.name)
381
+ i.to_add_statement(new_table_name)
382
+ end
383
+ drop_indexes.map! do |i|
384
+ undo_drop_indexes << i.to_add_statement(old_table_name)
385
+ drop_index(new_table_name, i.name)
386
+ end
387
+ # the order is important here - adding a :unique, for instance needs to remove then add
388
+ [drop_indexes + add_indexes, undo_add_indexes + undo_drop_indexes]
389
+ end
390
+
391
+ def drop_index(table, name)
392
+ # see https://hobo.lighthouseapp.com/projects/8324/tickets/566
393
+ # for why the rescue exists
394
+ "remove_index :#{table}, :name => :#{name} rescue ActiveRecord::StatementInvalid"
395
+ end
396
+
397
+ def format_options(options, type, changing=false)
398
+ options.map do |k, v|
399
+ unless changing
400
+ next if k == :limit && (type == :decimal || v == native_types[type][:limit])
401
+ next if k == :null && v == true
402
+ end
403
+ "#{k.inspect} => #{v.inspect}"
404
+ end.compact
405
+ end
406
+
407
+
408
+ def revert_table(table)
409
+ res = StringIO.new
410
+ ActiveRecord::SchemaDumper.send(:new, ActiveRecord::Base.connection).send(:table, table, res)
411
+ res.string.strip.gsub("\n ", "\n")
412
+ end
413
+
414
+
415
+ def column_options_from_reverted_table(table, column)
416
+ revert = revert_table(table)
417
+ if (md = revert.match(/\s*t\.column\s+"#{column}",\s+(:[a-zA-Z0-9_]+)(?:,\s+(.*?)$)?/m))
418
+ # Ugly migration
419
+ _, type, options = *md
420
+ elsif (md = revert.match(/\s*t\.([a-z_]+)\s+"#{column}"(?:,\s+(.*?)$)?/m))
421
+ # Sexy migration
422
+ _, type, options = *md
423
+ type = ":#{type}"
424
+ end
425
+ [type, options]
426
+ end
427
+
428
+
429
+ def change_column_back(table, column)
430
+ type, options = column_options_from_reverted_table(table, column)
431
+ "change_column :#{table}, :#{column}, #{type}#{', ' + options.strip if options}"
432
+ end
433
+
434
+
435
+ def revert_column(table, column)
436
+ type, options = column_options_from_reverted_table(table, column)
437
+ "add_column :#{table}, :#{column}, #{type}#{', ' + options.strip if options}"
438
+ end
439
+
440
+ end
441
+
442
+ end
443
+ end
444
+ end
445
+
@@ -0,0 +1,9 @@
1
+ class <%= @migration_class_name %> < ActiveRecord::Migration
2
+ def self.up
3
+ <%= @up %>
4
+ end
5
+
6
+ def self.down
7
+ <%= @down %>
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ Description:
2
+ Invokes the active_record:model generator, but the generated
3
+ model file contains the fields block, (and the migration option
4
+ is false by default).
5
+
6
+ Examples:
7
+ $ rails generate hobo:model account
8
+
9
+ creates an Account model, test and fixture:
10
+ Model: app/models/account.rb
11
+ Test: test/unit/account_test.rb
12
+ Fixtures: test/fixtures/accounts.yml
13
+
14
+ $ rails generate hobo:model post title:string body:text published:boolean
15
+
16
+ creates a Post model with a string title, text body, and published flag.
17
+
18
+ After the model is created, and the fields are specified, use hobo:migration
19
+ to create the migrations incrementally.
@@ -0,0 +1,11 @@
1
+ require 'rails/generators/active_record'
2
+ require 'generators/hobo_support/model'
3
+
4
+ module Hobo
5
+ class ModelGenerator < ActiveRecord::Generators::Base
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ include Generators::HoboSupport::Model
9
+
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+
2
+ fields do
3
+ <% for attribute in field_attributes -%>
4
+ <%= "%-#{max_attribute_length}s" % attribute.name %> :<%= attribute.type %>
5
+ <% end -%>
6
+ <% if options[:timestamps] -%>
7
+ timestamps
8
+ <% end -%>
9
+ end
10
+
11
+ <% for bt in bts -%>
12
+ belongs_to :<%= bt %>
13
+ <% end -%>
14
+ <%= "\n" unless bts.empty? -%>
15
+ <% for hm in hms -%>
16
+ has_many :<%= hm %>, :dependent => :destroy
17
+ <% end -%>
18
+ <%= "\n" unless hms.empty? -%>
@@ -0,0 +1,48 @@
1
+ ActiveRecord::Base.class_eval do
2
+ class << self
3
+
4
+ def can_wrap_with_hobo_type?(attr_name)
5
+ if connected?
6
+ type_wrapper = try.attr_type(attr_name)
7
+ type_wrapper.is_a?(Class) && type_wrapper.not_in?(HoboFields::PLAIN_TYPES.values)
8
+ else
9
+ false
10
+ end
11
+ end
12
+
13
+ # Define an attribute reader method. Cope with nil column.
14
+ def define_read_method(symbol, attr_name, column)
15
+
16
+ cast_code = column.type_cast_code('v') if column
17
+ access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
18
+
19
+ unless attr_name.to_s == self.primary_key.to_s
20
+ access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
21
+ end
22
+
23
+ # This is the Hobo hook - add a type wrapper around the field
24
+ # value if we have a special type defined
25
+ if can_wrap_with_hobo_type?(symbol)
26
+ access_code = "val = begin; #{access_code}; end; wrapper_type = self.class.attr_type(:#{attr_name}); " +
27
+ "if HoboFields.can_wrap?(wrapper_type, val); wrapper_type.new(val); else; val; end"
28
+ end
29
+
30
+ if cache_attribute?(attr_name)
31
+ access_code = "@attributes_cache['#{attr_name}'] ||= begin; #{access_code}; end;"
32
+ end
33
+
34
+ generated_attribute_methods.module_eval("def #{symbol}; #{access_code}; end", __FILE__, __LINE__)
35
+ end
36
+
37
+ def define_method_attribute=(attr_name)
38
+ if can_wrap_with_hobo_type?(attr_name)
39
+ src = "begin; wrapper_type = self.class.attr_type(:#{attr_name}); " +
40
+ "if !new_value.is_a?(wrapper_type) && HoboFields.can_wrap?(wrapper_type, new_value); wrapper_type.new(new_value); else; new_value; end; end"
41
+ generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', #{src}); end", __FILE__, __LINE__)
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ ActiveRecord::Base.class_eval do
2
+
3
+ def self.fields(include_in_migration = true, &b)
4
+ # Any model that calls 'fields' gets a bunch of other
5
+ # functionality included automatically, but make sure we only
6
+ # include it once
7
+ include HoboFields::Model unless HoboFields::Model.in?(included_modules)
8
+ @include_in_migration ||= include_in_migration
9
+
10
+ if b
11
+ dsl = HoboFields::FieldDeclarationDsl.new(self)
12
+ if b.arity == 1
13
+ yield dsl
14
+ else
15
+ dsl.instance_eval(&b)
16
+ end
17
+ end
18
+ end
19
+
20
+
21
+ end
@@ -0,0 +1,33 @@
1
+ require 'hobo_fields/types/enum_string'
2
+
3
+ module HoboFields
4
+
5
+ class FieldDeclarationDsl < BlankSlate
6
+
7
+ include HoboFields::Types::EnumString::DeclarationHelper
8
+
9
+ def initialize(model)
10
+ @model = model
11
+ end
12
+
13
+ attr_reader :model
14
+
15
+
16
+ def timestamps
17
+ field(:created_at, :datetime)
18
+ field(:updated_at, :datetime)
19
+ end
20
+
21
+
22
+ def field(name, type, *args)
23
+ @model.declare_field(name, type, *args)
24
+ end
25
+
26
+
27
+ def method_missing(name, *args)
28
+ field(name, args.first, *args.rest)
29
+ end
30
+
31
+ end
32
+
33
+ end