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.
- data/CHANGES.txt +38 -0
- data/LICENSE.txt +22 -0
- data/README.txt +8 -0
- data/Rakefile +36 -0
- data/VERSION +1 -0
- data/bin/hobofields +19 -0
- data/hobo_fields.gemspec +31 -0
- data/lib/generators/hobo/migration/USAGE +47 -0
- data/lib/generators/hobo/migration/migration_generator.rb +162 -0
- data/lib/generators/hobo/migration/migrator.rb +445 -0
- data/lib/generators/hobo/migration/templates/migration.rb.erb +9 -0
- data/lib/generators/hobo/model/USAGE +19 -0
- data/lib/generators/hobo/model/model_generator.rb +11 -0
- data/lib/generators/hobo/model/templates/model_injection.rb.erb +18 -0
- data/lib/hobo_fields/extensions/active_record/attribute_methods.rb +48 -0
- data/lib/hobo_fields/extensions/active_record/fields_declaration.rb +21 -0
- data/lib/hobo_fields/field_declaration_dsl.rb +33 -0
- data/lib/hobo_fields/model/field_spec.rb +121 -0
- data/lib/hobo_fields/model/index_spec.rb +47 -0
- data/lib/hobo_fields/model.rb +226 -0
- data/lib/hobo_fields/railtie.rb +13 -0
- data/lib/hobo_fields/sanitize_html.rb +23 -0
- data/lib/hobo_fields/types/email_address.rb +26 -0
- data/lib/hobo_fields/types/enum_string.rb +101 -0
- data/lib/hobo_fields/types/html_string.rb +15 -0
- data/lib/hobo_fields/types/lifecycle_state.rb +16 -0
- data/lib/hobo_fields/types/markdown_string.rb +15 -0
- data/lib/hobo_fields/types/password_string.rb +15 -0
- data/lib/hobo_fields/types/raw_html_string.rb +13 -0
- data/lib/hobo_fields/types/raw_markdown_string.rb +13 -0
- data/lib/hobo_fields/types/serialized_object.rb +15 -0
- data/lib/hobo_fields/types/text.rb +16 -0
- data/lib/hobo_fields/types/textile_string.rb +22 -0
- data/lib/hobo_fields.rb +94 -0
- data/test/api.rdoctest +244 -0
- data/test/doc-only.rdoctest +96 -0
- data/test/generators.rdoctest +53 -0
- data/test/interactive_primary_key.rdoctest +54 -0
- data/test/migration_generator.rdoctest +639 -0
- data/test/migration_generator_comments.rdoctest +75 -0
- data/test/prepare_testapp.rb +8 -0
- data/test/rich_types.rdoctest +394 -0
- 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,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
|