dynamic-fields 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/dynamic_fields.rb +21 -0
- data/lib/dynamic_fields/field.rb +41 -0
- data/lib/dynamic_fields/fields.rb +154 -0
- data/lib/dynamic_fields/index.rb +40 -0
- data/lib/dynamic_fields/migration_generator.rb +1 -0
- data/lib/dynamic_fields/railtie.rb +9 -0
- data/lib/generators/dynamic_fields/migration/USAGE +13 -0
- data/lib/generators/dynamic_fields/migration/migration_generator.rb +58 -0
- data/lib/generators/dynamic_fields/migration/templates/create_table.rb +20 -0
- data/lib/generators/dynamic_fields/migration/templates/update_table.rb +15 -0
- data/lib/tasks/dynamic_fields.rake +49 -0
- metadata +72 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'rails/generators/base'
|
3
|
+
|
4
|
+
module DynamicFields
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
included do
|
7
|
+
include Fields
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.models
|
11
|
+
ActiveRecord::Base.subclasses.select do |ar|
|
12
|
+
ar.included_modules.include?(DynamicFields)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'dynamic_fields/field'
|
19
|
+
require 'dynamic_fields/index'
|
20
|
+
require 'dynamic_fields/fields'
|
21
|
+
require 'dynamic_fields/railtie'
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module DynamicFields
|
2
|
+
class Field
|
3
|
+
attr_reader :name, :type, :type_class, :default, :options
|
4
|
+
# Create the new field with a name and optional additional options. Valid
|
5
|
+
# options are :default
|
6
|
+
#
|
7
|
+
# Options:
|
8
|
+
#
|
9
|
+
# name: The name of the field as a +Symbol+.
|
10
|
+
# options: A +Hash+ of options for the field.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
#
|
14
|
+
# <tt>Field.new(:score, :default => 0)</tt>
|
15
|
+
def initialize(name, options = {})
|
16
|
+
@name, @default = name, options.delete(:default)
|
17
|
+
@type = options.delete(:type) || :string
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
def options_with_default
|
22
|
+
has_default? ? options.merge(:default => default) : options
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_default?
|
26
|
+
default.present? || default == false || default == ''
|
27
|
+
end
|
28
|
+
|
29
|
+
def migration_string_for table_state, action
|
30
|
+
args = [name.to_sym.inspect]
|
31
|
+
if action.to_sym == :add
|
32
|
+
args << type.inspect if table_state.to_sym == :update
|
33
|
+
args += options_with_default.map do |k, v|
|
34
|
+
"#{k.to_sym.inspect} => #{v.inspect}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
args.join(', ').gsub('\\', '')
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module DynamicFields
|
2
|
+
module Fields
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
# Set up the class attributes that must be available to all subclasses.
|
7
|
+
class_inheritable_accessor :fields
|
8
|
+
self.fields = []
|
9
|
+
|
10
|
+
class_inheritable_accessor :indices
|
11
|
+
self.indices = []
|
12
|
+
|
13
|
+
|
14
|
+
delegate :fields, :indices, :to => "self.class"
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods #:nodoc
|
18
|
+
# Defines all the fields that are accessible on the model
|
19
|
+
#
|
20
|
+
# Options:
|
21
|
+
#
|
22
|
+
# name: The name of the field, as a +Symbol+.
|
23
|
+
# options: A +Hash+ of options to supply to the +Field+.
|
24
|
+
#
|
25
|
+
# Example:
|
26
|
+
#
|
27
|
+
# <tt>field :score, :default => 0</tt>
|
28
|
+
def field name, options = {}
|
29
|
+
fields << ::DynamicFields::Field.new(name, options) unless field_names.include?(name.to_s)
|
30
|
+
end
|
31
|
+
|
32
|
+
# All field names (as defined in the model)
|
33
|
+
def field_names
|
34
|
+
fields.map(&:name).map(&:to_s)
|
35
|
+
end
|
36
|
+
|
37
|
+
# All pre-existing field names
|
38
|
+
def real_field_names
|
39
|
+
table_exists? ? column_names : []
|
40
|
+
end
|
41
|
+
|
42
|
+
# All pre-existing fields
|
43
|
+
def real_fields
|
44
|
+
return [] unless table_exists?
|
45
|
+
columns.map do |c|
|
46
|
+
next if c.primary == true
|
47
|
+
::DynamicFields::Field.new(c.name, :type => c.type, :default => c.default)
|
48
|
+
end.compact
|
49
|
+
end
|
50
|
+
|
51
|
+
# Any fields which have not been add to the table
|
52
|
+
def new_fields
|
53
|
+
fields.reject {|f| real_field_names.include?(f.name.to_s) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Any fields in the table, but not in the model
|
57
|
+
def old_fields
|
58
|
+
real_fields.reject { |f| field_names.include?(f.name.to_s) }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Defines all the indices on the model
|
62
|
+
#
|
63
|
+
# Options:
|
64
|
+
#
|
65
|
+
# name: The name of the index, as a +String+.
|
66
|
+
# options: A +Hash+ of options to supply to the +Index+.
|
67
|
+
#
|
68
|
+
# Example:
|
69
|
+
#
|
70
|
+
# <tt>index :user_id</tt>
|
71
|
+
# <tt>index [:user_id, :user_group_id], :name => "user_groups", :unique => true</tt>
|
72
|
+
def index field_or_fields, options={}
|
73
|
+
options[:name] = options.delete(:as) || options.delete(:name) || connection.index_name(table_name, :column => field_or_fields)
|
74
|
+
idx = ::DynamicFields::Index.new(field_or_fields, options)
|
75
|
+
indices << idx unless index_ids.include?(idx.id)
|
76
|
+
end
|
77
|
+
|
78
|
+
# All index ids (as defined in the model)
|
79
|
+
def index_ids
|
80
|
+
indices.map(&:id)
|
81
|
+
end
|
82
|
+
|
83
|
+
# All pre-existing index ids
|
84
|
+
def real_index_ids
|
85
|
+
real_indices.map(&:id)
|
86
|
+
end
|
87
|
+
|
88
|
+
# All pre-existing indices
|
89
|
+
def real_indices
|
90
|
+
return [] unless table_exists?
|
91
|
+
unless connection.respond_to?(:indexes)
|
92
|
+
p "Dynamic indices not supported - remove old indices manually"
|
93
|
+
return []
|
94
|
+
else
|
95
|
+
connection.indexes(table_name).map do |idx|
|
96
|
+
idxs = idx.columns.size > 1 ? idx.columns : idx.columns.first
|
97
|
+
::DynamicFields::Index.new(idxs, {:name => idx.name, :unique => idx.unique})
|
98
|
+
end.compact
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Any indicies which have not been add to the table
|
103
|
+
def new_indices
|
104
|
+
indices.reject { |idx| real_index_ids.include?(idx.id) }
|
105
|
+
end
|
106
|
+
|
107
|
+
# Any indices in the table, but not in the model
|
108
|
+
def old_indices
|
109
|
+
real_indices.reject { |idx| index_ids.include?(idx.id) }
|
110
|
+
end
|
111
|
+
|
112
|
+
# Check in the model requires a migration (any new or old fields/indices?)
|
113
|
+
def requires_migration?
|
114
|
+
(new_fields + new_indices + old_fields + old_indices).any?
|
115
|
+
end
|
116
|
+
|
117
|
+
def migration_name
|
118
|
+
return "create_#{table_name}" unless table_exists?
|
119
|
+
"update_#{table_name}_" + [
|
120
|
+
new_fields_migration_name,
|
121
|
+
new_indices_migration_name,
|
122
|
+
old_fields_migration_name,
|
123
|
+
old_indices_migration_name
|
124
|
+
].compact.join("_and_")
|
125
|
+
end
|
126
|
+
|
127
|
+
protected
|
128
|
+
|
129
|
+
def new_fields_migration_name #:nodoc
|
130
|
+
migration_name_for_collection new_fields, "add"
|
131
|
+
end
|
132
|
+
|
133
|
+
def old_fields_migration_name #:nodoc
|
134
|
+
migration_name_for_collection old_fields, "remove"
|
135
|
+
end
|
136
|
+
|
137
|
+
def new_indices_migration_name #:nodoc
|
138
|
+
new_migration_name = migration_name_for_collection(new_indices, "add_#{new_indices.size > 1 ? 'indices' : 'index'}")
|
139
|
+
new_migration_name.present? ? new_migration_name.gsub("index_#{table_name}_on_", '') : nil
|
140
|
+
end
|
141
|
+
|
142
|
+
def old_indices_migration_name #:nodoc
|
143
|
+
new_migration_name = migration_name_for_collection(old_indices, "remove_#{old_indices.size > 1 ? 'indices' : 'index'}")
|
144
|
+
new_migration_name.present? ? new_migration_name.gsub("index_#{table_name}_on_", '') : nil
|
145
|
+
end
|
146
|
+
|
147
|
+
def migration_name_for_collection collection, prefix=nil #:nodoc
|
148
|
+
new_migration_name = collection.map {|i| i.name.to_s }.to_sentence.gsub(/,?\W/, '_')
|
149
|
+
new_migration_name.present? ? [prefix, new_migration_name].compact.join('_') : nil
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module DynamicFields
|
2
|
+
class Index
|
3
|
+
attr_reader :name, :fields, :options
|
4
|
+
|
5
|
+
def initialize field_or_fields, options={}
|
6
|
+
@fields = field_or_fields
|
7
|
+
@name = options[:name]
|
8
|
+
@unique = options.delete(:unique) == true
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def id
|
13
|
+
name
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
name
|
18
|
+
end
|
19
|
+
|
20
|
+
def unique?
|
21
|
+
@unique == true
|
22
|
+
end
|
23
|
+
|
24
|
+
def migration_string_for table_state, action
|
25
|
+
args = []
|
26
|
+
case action = action.to_sym
|
27
|
+
when :add
|
28
|
+
args << (fields.is_a?(Array) ? fields : fields.to_sym).inspect
|
29
|
+
args << ":name => #{name.inspect}" if fields.is_a?(Array)
|
30
|
+
args << ":unique => true" if unique?
|
31
|
+
when :remove
|
32
|
+
args << fields.to_sym.inspect unless fields.is_a?(Array)
|
33
|
+
args << ":name => #{name.to_sym.inspect}" if fields.is_a?(Array)
|
34
|
+
end
|
35
|
+
args.join(', ').gsub('\\', '')
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__)+'/../generators/dynamic_fields/migration/migration_generator')
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Description:
|
2
|
+
Generate a migration file for a specific Model using DynamicFields
|
3
|
+
|
4
|
+
Usage:
|
5
|
+
Pass the name of the Model using DynamicFields, either CamelCased or under_scored
|
6
|
+
|
7
|
+
Examples:
|
8
|
+
rails generate dynamic_fields:migration Post
|
9
|
+
|
10
|
+
If the Post model has no table yet, will create a migration called "create_posts"
|
11
|
+
containing a create_table migration. However, if the Post model already has a table,
|
12
|
+
will create a migration called "update_posts" containing all the add_column and
|
13
|
+
remove_column migrations.
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module DynamicFields
|
4
|
+
class MigrationGenerator < Rails::Generators::Base
|
5
|
+
include Rails::Generators::Migration
|
6
|
+
|
7
|
+
argument :model, :type => :string, :default => nil, :banner => "Model"
|
8
|
+
|
9
|
+
def create_migration_file
|
10
|
+
set_local_assigns!
|
11
|
+
parse_attributes!
|
12
|
+
migration_template "#{migration_action}_table.rb", "db/migrate/#{migration_name}.rb"
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
attr_reader :migration_action, :migration_klass, :table_name, :attributes, :indices, :migration_name
|
17
|
+
|
18
|
+
def parse_attributes! #:nodoc:
|
19
|
+
if migration_action == 'create'
|
20
|
+
@attributes = migration_klass.new_fields
|
21
|
+
@indices = migration_klass.new_indices
|
22
|
+
else
|
23
|
+
@attributes = {}
|
24
|
+
@attributes[:add] = migration_klass.new_fields
|
25
|
+
@attributes[:remove] = migration_klass.old_fields
|
26
|
+
@indices = {}
|
27
|
+
@indices[:add] = migration_klass.new_indices
|
28
|
+
@indices[:remove] = migration_klass.old_indices
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_local_assigns!
|
33
|
+
@migration_klass = model.camelize.constantize
|
34
|
+
@migration_action = @migration_klass.table_exists? ? 'update' : 'create'
|
35
|
+
@table_name = @migration_klass.table_name
|
36
|
+
@migration_name = @migration_klass.migration_name
|
37
|
+
end
|
38
|
+
|
39
|
+
# Set the current directory as base for the inherited generators.
|
40
|
+
def self.base_root
|
41
|
+
File.dirname(__FILE__)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.source_root
|
45
|
+
File.expand_path(base_root + '/templates')
|
46
|
+
end
|
47
|
+
|
48
|
+
# Implement the required interface for Rails::Generators::Migration.
|
49
|
+
def self.next_migration_number(dirname) #:nodoc:
|
50
|
+
next_migration_number = current_migration_number(dirname) + 1
|
51
|
+
if ActiveRecord::Base.timestamped_migrations
|
52
|
+
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
53
|
+
else
|
54
|
+
"%.3d" % next_migration_number
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :<%= table_name %> do |t|
|
4
|
+
<% attributes.each do |attribute| -%>
|
5
|
+
t.<%= attribute.type %> <%= attribute.migration_string_for :create, :add %>
|
6
|
+
<% end -%>
|
7
|
+
end
|
8
|
+
|
9
|
+
<% indices.each do |index| -%>
|
10
|
+
add_index :<%= table_name %>, <%= index.migration_string_for :create, :add %>
|
11
|
+
<% end -%>
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
<% indices.each do |index| -%>
|
16
|
+
remove_index :<%= table_name %>, <%= index.migration_string_for :create, :remove %>
|
17
|
+
<% end -%>
|
18
|
+
drop_table :<%= table_name %>
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def self.up<% attributes.each do |action, attrs| %><% attrs.each do |attribute| %>
|
3
|
+
<%= action %>_column :<%= table_name %>, <%= attribute.migration_string_for :update, action %><% end -%>
|
4
|
+
<% end %><% indices.each do |action, idxs| %><% idxs.each do |index| %>
|
5
|
+
<%= action %>_index :<%= table_name %>, <%= index.migration_string_for :update, action %><% end -%>
|
6
|
+
<% end %>
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.down<% indices.each do |action, idxs| %><% idxs.each do |index| %>
|
10
|
+
<%= action == :add ? 'remove' : 'add' %>_index :<%= table_name %>, <%= index.migration_string_for(:update, action == :add ? :remove : :add ) %><% end -%>
|
11
|
+
<% end %><% attributes.each do |action, attrs| %><% attrs.reverse.each do |attribute| %>
|
12
|
+
<%= action == :add ? 'remove' : 'add' %>_column :<%= table_name %>, <%= attribute.migration_string_for(:update, action == :add ? :remove : :add ) %><% end -%>
|
13
|
+
<% end %>
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'dynamic_fields/migration_generator'
|
2
|
+
|
3
|
+
namespace :dynamic_fields do
|
4
|
+
|
5
|
+
desc "Generate a migration for the specified MODEL"
|
6
|
+
task :migration => :environment do
|
7
|
+
generate_migration_for ENV["MODEL"].classify.constantize
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Generate migrations for all Models"
|
11
|
+
task :migrations => :load_models do
|
12
|
+
DynamicFields.models.each do |model|
|
13
|
+
generate_migration_for model
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Generate migrations for all Models and migrate the database"
|
18
|
+
task :migrate => :migrations do
|
19
|
+
Rake::Task['db:migrate'].invoke
|
20
|
+
end
|
21
|
+
|
22
|
+
task :load_models => :environment do
|
23
|
+
Dir["#{Rails.root}/app/models/**/*.rb"].each { |model| require_or_load model }
|
24
|
+
Rails::Engine.subclasses.each do |engine|
|
25
|
+
engine_load_path = engine.paths.app.models.paths.first
|
26
|
+
Dir[engine_load_path + '/*.rb', engine_load_path + '/**/*.rb'].each { |model| require_or_load model rescue LoadError }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
namespace :df do
|
33
|
+
desc "Generate a migration for the specified MODEL"
|
34
|
+
task :migration => "dynamic_fields:migration"
|
35
|
+
desc "Generate migrations for all Models"
|
36
|
+
task :migrations => "dynamic_fields:migrations"
|
37
|
+
desc "Generate migrations for all Models and migrate the database"
|
38
|
+
task :migrate => "dynamic_fields:migrate"
|
39
|
+
end
|
40
|
+
|
41
|
+
def generate_migration_for(klass)
|
42
|
+
return unless klass.name.present? # no annoymous classes
|
43
|
+
if klass.requires_migration?
|
44
|
+
puts "Generating a migration for #{klass.name}"
|
45
|
+
DynamicFields::MigrationGenerator.start klass.name
|
46
|
+
else
|
47
|
+
puts "#{klass.name} is up to date!"
|
48
|
+
end
|
49
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dynamic-fields
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 4
|
9
|
+
version: 0.0.4
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Alex Neill
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-07-02 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Allows for automatic migrations with ActiveRecord
|
22
|
+
email: alex@featureless.co.uk
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- lib/dynamic_fields.rb
|
31
|
+
- lib/dynamic_fields/field.rb
|
32
|
+
- lib/dynamic_fields/fields.rb
|
33
|
+
- lib/dynamic_fields/index.rb
|
34
|
+
- lib/dynamic_fields/migration_generator.rb
|
35
|
+
- lib/dynamic_fields/railtie.rb
|
36
|
+
- lib/generators/dynamic_fields/migration/USAGE
|
37
|
+
- lib/generators/dynamic_fields/migration/migration_generator.rb
|
38
|
+
- lib/generators/dynamic_fields/migration/templates/create_table.rb
|
39
|
+
- lib/generators/dynamic_fields/migration/templates/update_table.rb
|
40
|
+
- lib/tasks/dynamic_fields.rake
|
41
|
+
has_rdoc: true
|
42
|
+
homepage: http://github.com/ajn/dynamic-fields
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options:
|
47
|
+
- --charset=UTF-8
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
requirements: []
|
65
|
+
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.3.6
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: Auto-migrate ActiveRecord models
|
71
|
+
test_files: []
|
72
|
+
|