Module: ModalFields

Defined in:
lib/modalfields/modalfields.rb

Overview

This is a hybrid between HoboFields and model annotators.

It works like other annotators, by updating the model annotations from the DB schema. But the annotations are syntactic Ruby as in HoboFields rather than comments.

Apart from looking better to my eyes, this allows triggering special functionality from the field declations (such as specifying validations).

The annotations are kept up to date by the migration tasks. Comments and validation, etc. specifications modified manually are preserved, at least if the field block syntax is kept as generated (one line per field, one line for the block start and end…)

Custom type fields and hooks can be define in files (e.g. fields.rb) in config/initializers/

Defined Under Namespace

Modules: FieldDeclarationClassMethods Classes: DeclarationsDsl, DefinitionsDsl, DslBase, FieldDeclaration, HooksDsl

Constant Summary collapse

SPECIFIERS =
[:indexed, :unique, :required]
COMMON_ATTRIBUTES =
{:default=>nil, :null=>true}

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.definitionsObject (readonly)

Returns the value of attribute definitions.



169
170
171
# File 'lib/modalfields/modalfields.rb', line 169

def definitions
  @definitions
end

.hooksObject (readonly)

Returns the value of attribute hooks.



169
170
171
# File 'lib/modalfields/modalfields.rb', line 169

def hooks
  @hooks
end

.migration_hooksObject (readonly)

Returns the value of attribute migration_hooks.



169
170
171
# File 'lib/modalfields/modalfields.rb', line 169

def migration_hooks
  @migration_hooks
end

.show_primary_keysObject

Define declaration of primary keys

ModalFields.show_primary_keys = false # the default: do not show primary keys
ModalFields.show_primary_keys = true  # always declare primary keys
ModalFields.show_primary_keys = :id   # only declare if named 'id' (otherwise the model will have a primary_key declaration)
ModalFields.show_primary_keys = :except_id   # only declare if named differently from 'id'


175
176
177
# File 'lib/modalfields/modalfields.rb', line 175

def show_primary_keys
  @show_primary_keys
end

Class Method Details

.alias(aliases) ⇒ Object

Define type synonyms



188
189
190
# File 'lib/modalfields/modalfields.rb', line 188

def alias(aliases)
  @type_aliases.merge! aliases
end

.checkObject



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/modalfields/modalfields.rb', line 252

def check
  dbmodels(dbmodel_options).each do |model, file|
    new_fields, modified_fields, deleted_fields, deleted_model = diff(model)
    unless new_fields.empty? && modified_fields.empty? && deleted_fields.empty?
      rel_file = file && file.sub(/\A#{Rails.root}/,'')
      puts "#{model} (#{rel_file}):"
      puts "  (deleted)" if deleted_model
      [['+',new_fields],['*',modified_fields],['-',deleted_fields]].each do |prefix, fields|
        puts fields.map{|field| "  #{prefix} #{field}"}*"\n" unless fields.empty?
        # TODO: report index differences
      end
      puts ""
    end
  end
end

.column_to_field_declaration(&blk) ⇒ Object

Define a custom column to field declaration conversion



198
199
200
# File 'lib/modalfields/modalfields.rb', line 198

def column_to_field_declaration(&blk)
  @column_to_field_declaration_hook = blk
end

.define(&blk) ⇒ Object

Run a definition block that executes field type definitions



183
184
185
# File 'lib/modalfields/modalfields.rb', line 183

def define(&blk)
  DefinitionsDsl.new.instance_eval(&blk)
end

.enableObject

Enable the ModalFields plugin (adds the fields declarator to model classes)



203
204
205
206
207
208
209
210
# File 'lib/modalfields/modalfields.rb', line 203

def enable
  if defined?(::Rails)
    # class ::ActiveRecord::Base
    #   extend FieldDeclarationClassMethods
    # end
    ::ActiveRecord::Base.send :extend, FieldDeclarationClassMethods
  end
end

.hook(&blk) ⇒ Object

Run a hooks block that defines field declaration processors



193
194
195
# File 'lib/modalfields/modalfields.rb', line 193

def hook(&blk)
  HooksDsl.new.instance_eval(&blk)
end

.migrationObject



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/modalfields/modalfields.rb', line 268

def migration
  up = ""
  down = ""
  dbmodels(dbmodel_options).each do |model, file|
    new_fields, modified_fields, deleted_fields, deleted_model = diff(model).map{|fields|
      fields.kind_of?(Array) ? fields.map{|f| migration_declaration(model, f)} : fields
    }
    unless new_fields.empty? && modified_fields.empty? && deleted_fields.empty?
      up << "\n"
      down << "\n"
      rel_file = file && file.sub(/\A#{Rails.root}/,'')
      if deleted_model && modified_fields.empty? && new_fields.empty?
        up << "  create_table #{model.table_name.to_sym.inspect} do |t|\n"
        deleted_fields.each do |field|
          up << "    t.#{field.type} #{field.name.inspect}"
          unless field.attributes.empty?
            up << ", " + field.attributes.inspect.unwrap('{}')
          end
          up << "\n"
        end
        up << "  end\n"
        down << "  drop_table #{model.table_name.to_sym.inspect}\n"
      else
        deleted_fields.each do |field|
          up << "  add_column #{model.table_name.to_sym.inspect}, #{field.name.inspect}, #{field.type.inspect}"
          unless field.attributes.empty?
            up << ", " + field.attributes.inspect.unwrap('{}')
          end
          up << "\n"
          down << "  remove_column #{model.table_name.to_sym.inspect}, #{field.name.inspect}\n"
        end
        modified_fields.each do |field|
          changed = model.fields_info.find{|f| f.name.to_sym==field.name.to_sym}
          changed &&= migration_declaration(model, changed)
          up << "  change_column #{model.table_name.to_sym.inspect}, #{changed.name.inspect}, #{changed.type.inspect}"
          unless changed.attributes.empty?
            up << ", " + changed.attributes.inspect.unwrap('{}')
          end
          up << "\n"
          down << "  change_column #{model.table_name.to_sym.inspect}, #{field.name.inspect}, #{field.type.inspect}"
          unless field.attributes.empty?
            down << ", " + field.attributes.inspect.unwrap('{}')
          end
          down << "\n"
        end
        new_fields.each do |field|
          up << "  remove_column #{model.table_name.to_sym.inspect}, #{field.name.inspect}\n"
          down << "  add_column #{model.table_name.to_sym.inspect}, #{field.name.inspect}, #{field.type.inspect}"
          unless field.attributes.empty?
            down << ", " + field.attributes.inspect.unwrap('{}')
          end
          down << "\n"
        end
      end
      # TODO: indices
    end
  end
  unless up.blank?
    puts "\n\# up:"
    puts up
  end
  unless down.blank?
    puts "\n\# down:"
    puts down
  end
  puts ""
end

.models(options = nil) ⇒ Object



415
416
417
# File 'lib/modalfields/modalfields.rb', line 415

def models(options=nil)
  dbmodels options || dbmodel_options
end

.parameters(params = nil) ⇒ Object



177
178
179
180
# File 'lib/modalfields/modalfields.rb', line 177

def parameters(params=nil)
  @parameters.merge! params if params
  @parameters
end

.report(options = {}) ⇒ Object



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# File 'lib/modalfields/modalfields.rb', line 344

def report(options={})
  models = Array(options[:model])
  models = dbmodels(dbmodel_options) if models.blank?
  models.each do |model, file|
    if options[:tables]
      yield :table, model.table_name, nil, {:model=>model}
    end

    submodels = model.send(:subclasses)
    existing_fields, association_fields, pk_fields = model_existing_fields(model, submodels)
    unless file.nil?
      pre, start_fields, fields, end_fields, post = split_model_file(file)
    else
    end

    pks = pk_fields.map{|pk_field_name| existing_fields.find{|f| f.name==pk_field_name}}
    existing_fields = existing_fields.reject{|f| f.name.in? pk_fields}
    if options[:primary_keys]
      pks.each do |pk_field|
        yield :primary_key, model.table_name, pk_field.name, field_data(pk_field)
      end
    end

    assoc_cols = []
    association_fields.each do |assoc, cols|

      if options[:associations]
        if assoc.options[:polymorphic]
          foreign_table = :polymorphic
        else
          foreign_table = assoc.klass.table_name
        end
        yield :association, model.table_name, assoc.name, {:foreign_table=>foreign_table}
      end
      Array(cols).each do |col|
        col = existing_fields.find{|f| f.name.to_s==col.to_s}
        next unless col
        assoc_cols << col
        if options[:foreign_keys]
          yield :foreign_key, model.table_name, col.name, field_data(col, :assoc=>assoc)
        end
      end
    end
    existing_fields -= assoc_cols

    has_fields_info = model.respond_to?(:fields_info) && model.fields_info != :omitted
    fields = Array(fields).reject{|line, name, comment| name.blank?}
    if has_fields_info
      field_order = model.fields_info.map(&:name).map(&:to_s) & existing_fields.map(&:name)
    else
      field_order = []
    end
    if options[:undeclared_fields]
      field_order += existing_fields.map(&:name).reject{|name| field_order.include?(name.to_s)}
    end
    field_comments = Hash[fields.map{|line, name, comment| [name,comment]}]
    field_extras = has_fields_info ? Hash[ model.fields_info.map{|fi| [fi.name.to_s,fi.attributes]}] : {}
    field_order.each do |field_name|
      field_info = existing_fields.find{|f| f.name.to_s==field_name}
      field_comment = field_comments[field_name]
      field_extra = field_extras[field_name]
      if field_info.blank?
        raise " MISSING FIELD: #{field_name} (#{model})"
      else
        yield :field, model.table_name, field_info.name, field_data(field_info, :comments=>field_comment, :extra=>field_extra)
      end
    end

  end
end

.update(modify = true) ⇒ Object

Update the field declarations of all the models. This modifies the source files of all the models (touches only the fields block or adds one if not present). It is recommended to run this on a clearn working directory (no uncommitted changes), so that the changes can be easily reviewed.



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/modalfields/modalfields.rb', line 216

def update(modify=true)
  dbmodels(dbmodel_options).each do |model, file|
    next if file.nil?
    model.reset_column_information
    new_fields, modified_fields, deleted_fields, deleted_model = diff(model)
    unless new_fields.empty? && modified_fields.empty? && deleted_fields.empty?
      pre, start_fields, fields, end_fields, post = split_model_file(file)
      deleted_names = deleted_fields.map{|f| f.name.to_s}
      fields = fields.reject{|line, name, comment| deleted_names.include?(name)}
      fields = fields.map{|line, name, comment|
        mod_field = modified_fields.detect{|f| f.name.to_s==name}
        if mod_field
          line = "    "+mod_field.to_s
          line << " #{comment}" if comment
          line << "\n"
        end
        [line, name, comment]
      }
      pk_names = Array(model.primary_key).map(&:to_s)
      created_at = new_fields.detect{|f| f.name.to_s=='created_at'}
      updated_at = new_fields.detect{|f| f.name.to_s=='updated_at'}
      if created_at && updated_at && created_at.type.to_sym==:datetime && updated_at.type.to_sym==:datetime
        with_timestamps = true
        new_fields -= [created_at, updated_at]
      end
      fields += new_fields.map{|f|
        comments = pk_names.include?(f.name.to_s) ? " \# PK" : ""
        ["    #{f}#{comments}\n" ]
      }
      fields << ["    timestamps\n"] if with_timestamps
      output_file = modify ? file : "#{file}_with_fields.rb"
      join_model_file(output_file, pre, start_fields, fields, end_fields, post)
    end
  end
end

.validate(declaration) ⇒ Object



336
337
338
339
340
341
342
# File 'lib/modalfields/modalfields.rb', line 336

def validate(declaration)
  definition = definitions[declaration.type.to_sym]
  raise "Field type #{declaration.type} not defined (#{declaration.inspect})" unless definition
  # TODO: validate declaration.specifiers
  # TODO: validate declaration.attributes with definition
  true
end