hobo_fields 1.3.0.RC

Sign up to get free protection for your applications and to get access to all the features.
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