dynamic-fields 0.0.4
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/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
|
+
|