dynamic-fields 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,9 @@
1
+ require 'dynamic_fields'
2
+ require 'rails'
3
+ module DynamicFields
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load "tasks/dynamic_fields.rake"
7
+ end
8
+ end
9
+ end
@@ -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
+