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.
@@ -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
+