tmx_data_update 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 TMX Credit
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # TMX Data Update
2
+
3
+ The problem of seed data can get annoying once your Rails app is in production.
4
+ Ordinarily, you would place your seeds data in `seeds.rb`. Unfortunately, once
5
+ your application goes live, you will likely not be in a position to reload your
6
+ seeds. This leaves you with Rails migrations as the likely choice, but
7
+ that now makes development harder, because things seed out of order (migrations
8
+ run before seeds). This gem solves these problems.
9
+
10
+ ## Installation
11
+
12
+ In your Gemfile:
13
+
14
+ ```ruby
15
+ gem 'tmx_data_update'
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Data updates are defined similar to migrations. Each file contains a class
21
+ definition which should ideally extend `TmxDataUpdate::Updater` but doesn't have
22
+ to as long as it implements `apply_update` and `revert_update`. They need to
23
+ follow the default Rails naming convention; a file called
24
+ `update_order_types.rb` should contain the class `UpdateOrderTypes`. It is
25
+ highly recommended that each file have a prefix that defines its order. The
26
+ format is pretty flexible, but the prefix must start with a number and not have
27
+ any underscores. So `01_update_order_types.rb` is fine, so is
28
+ `1A5_update_order_types.rb`. In each of these cases, the name of the class is
29
+ still `UpdateOrderTypes`. If you extend `TmxDataUpdate::Updater` you only need to
30
+ override `revert_update` if you need your migration to be reversible, otherwise
31
+ it's not necessary.
32
+
33
+ Here's an example data update class definition:
34
+
35
+ ```ruby
36
+ class UpdateOrderTypes < TmxDataUpdates::Updater
37
+ def perform_update
38
+ OrderType.create :type_code => 'very_shiny'
39
+ end
40
+
41
+ # Overriden in case we need to roll back this migration.
42
+ def undo_update
43
+ OrderType.where(:type_code => 'very_shiny').first.delete
44
+ end
45
+ end
46
+ ```
47
+
48
+ ### Migrations
49
+
50
+ Assuming we have created the update file above in `db/data_updates` in a
51
+ file named `01_update_order_types.rb`
52
+
53
+ `root_updates_path` and optionally `should_run?(update_name)` must be defined
54
+ in every migration where we intend to do data updates. Realistically, our app
55
+ should extend `TmxDataUpdate` and then include the new module in each migration
56
+ where needed.
57
+
58
+ ```ruby
59
+ module CoreDataUpdate
60
+ include TmxDataUpdate
61
+
62
+ def root_updates_path
63
+ Rails.root.join('db','data_updates')
64
+ end
65
+
66
+ def should_run?(update_name)
67
+ Rails.env == 'production'
68
+ end
69
+ end
70
+ ```
71
+
72
+ Now, define a migration that will perform our data update.
73
+
74
+ ```ruby
75
+ class CreateVeryShinyOrderType < ActiveRecord::Migration
76
+ include CoreDataUpdate
77
+
78
+ def up
79
+ apply_update :01_update_order_types
80
+ end
81
+
82
+ def down
83
+ revert_update :01_update_order_types
84
+ end
85
+ end
86
+ ```
87
+
88
+ Note that the update prefix is optional. The following will also work.
89
+
90
+ ```ruby
91
+ class CreateVeryShinyOrderType < ActiveRecord::Migration
92
+ include CoreDataUpdate
93
+
94
+ def up
95
+ apply_update :update_order_types
96
+ end
97
+
98
+ def down
99
+ revert_update :update_order_types
100
+ end
101
+ end
102
+ ```
103
+
104
+ Old style migrations, i.e. `def self.up` are not supported.
105
+
106
+ ### Seeds
107
+
108
+ At the bottom of your `seeds.rb`, include the following:
109
+
110
+ ```ruby
111
+ include TmxDataUpdate::Seeds
112
+ apply_updates Rails.root.join('db','data_updates')
113
+ ```
114
+
115
+ ### Generators
116
+
117
+ Two generators are added for your convenience:
118
+
119
+ * install: Updates the seeds file as specified above and creates a custom
120
+ data update module for engines. See
121
+ {file:lib/generators/tmx\_data\_update/create/USAGE} for examples
122
+ * create: Creates new data\_update and migration files as specified above. See
123
+ {file:lib/generators/tmx\_data\_update/install/USAGE} for examples
124
+
125
+ NOTE: If you're using this from a Rails engine with Rails 3.2 and you're using
126
+ RSpec to test, invoking the generator will likely put the generated files in
127
+ `spec/dummy/db` instead of `db`. Just something to be aware of.
128
+
129
+ ## Testing
130
+
131
+ Install all the gem dependencies.
132
+
133
+ bundle install
134
+
135
+ Run tests
136
+
137
+ rake spec
138
+
139
+ ## License
140
+
141
+ Copyright (c) 2013 TMX Credit.
142
+ Released under the MIT License. See LICENSE file for details.
@@ -0,0 +1,10 @@
1
+ Description:
2
+ Creates files necessary for a data update
3
+
4
+ Example:
5
+ rails generate tmx_data_update:create foo
6
+
7
+ This will:
8
+ * Create db/migrate/<timestamp>_foo.rb
9
+ * Create db/data_updates/<timestamp>_foo_data_update.rb
10
+
@@ -0,0 +1,17 @@
1
+ require 'rails/generators/active_record'
2
+ require 'generators/tmx_data_update/helper'
3
+
4
+ # Generator to create a data update + associated migration
5
+ class TmxDataUpdate::CreateGenerator < Rails::Generators::NamedBase
6
+ include Generators::TmxDataUpdate::Helper
7
+ include Rails::Generators::Migration
8
+ extend ActiveRecord::Generators::Migration
9
+
10
+ source_root File.expand_path('../templates', __FILE__)
11
+
12
+ # Creates the data update file and the migration file.
13
+ def create_helper_file
14
+ migration_template "data_update.rb", "db/data_updates/#{file_name}_data_update.rb"
15
+ migration_template "data_update_migration.rb", "db/migrate/#{file_name}.rb"
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ class <%= data_update_class_name %> < TmxDataUpdate::Updater
2
+ def perform_update
3
+ # <%= class_name %>.create :type_code => 'very_shiny'
4
+ end
5
+
6
+ # Overridden in case we need to roll back this migration.
7
+ def undo_update
8
+ # <%= class_name %>.where(:type_code => 'very_shiny').first.delete
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ include <%= application_class_name %>DataUpdate
3
+
4
+ def up
5
+ apply_update '<%= data_update_file_name %>'
6
+ end
7
+
8
+ def down
9
+ revert_update '<%= data_update_file_name %>'
10
+ end
11
+ end
@@ -0,0 +1,48 @@
1
+ module Generators # :nodoc:
2
+ module TmxDataUpdate # :nodoc:
3
+ # Helper methods for generators
4
+ module Helper
5
+ # Is this a Rails Application or Engine?
6
+ # @return [Boolean]
7
+ def application?
8
+ Rails.application.is_a?(Rails::Application)
9
+ end
10
+
11
+ # Name of the application or engine useful for files and directories
12
+ # @return [String]
13
+ def application_name
14
+ if defined?(Rails) && Rails.application
15
+ application_class_name.underscore
16
+ else
17
+ "application"
18
+ end
19
+ end
20
+
21
+ # Fully qualified name of the application or engine
22
+ # @example AppName::Engine or AppName::Application
23
+ # @return [String]
24
+ def full_application_class_name
25
+ Rails.application.class.name
26
+ end
27
+
28
+ # Regular name of the application or engine
29
+ # @example AppName
30
+ # @return [String]
31
+ def application_class_name
32
+ full_application_class_name.split('::').first
33
+ end
34
+
35
+ # Class name for use in data_update files
36
+ # @return [String]
37
+ def data_update_class_name
38
+ migration_class_name
39
+ end
40
+
41
+ # Name useful for the data update file
42
+ # @return [String]
43
+ def data_update_file_name
44
+ "#{migration_number}_#{file_name}_data_update"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,11 @@
1
+ Description:
2
+ Creates the necessary files to integrate with TmxDataUpdate
3
+
4
+ Example:
5
+ rails generate tmx_data_update:install
6
+
7
+ This will:
8
+ * Update your db/seeds.rb to run the data update
9
+ * Add an initializer:
10
+ (Application) config/initializers/<application_name>_data_update.rb
11
+ (Engine) lib/<application_name>/<application_name>_data_update.rb
@@ -0,0 +1,33 @@
1
+ require 'generators/tmx_data_update/helper'
2
+
3
+ # Generator to install tmx data update in a new rails system.
4
+ class TmxDataUpdate::InstallGenerator < Rails::Generators::Base
5
+ include Generators::TmxDataUpdate::Helper
6
+
7
+ source_root File.expand_path('../templates', __FILE__)
8
+
9
+ # Create the initializer file with default options.
10
+ def create_initializer
11
+ log :initializer, "Adding custom data update module"
12
+
13
+ if application?
14
+ template "data_update_module.rb", "config/initializers/#{application_name}_data_update.rb"
15
+ else
16
+ template "data_update_module.rb", "lib/#{application_name}/#{application_name}_data_update.rb"
17
+ end
18
+ end
19
+
20
+ # Update seeds.rb
21
+ def update_seeds
22
+ log :initializer, "Adding data update seeder to seeds.rb"
23
+
24
+ seed_code =<<SEED
25
+ include TmxDataUpdate::Seeds
26
+ apply_updates #{full_application_class_name}.root.join('db', 'data_updates')
27
+ SEED
28
+
29
+ in_root do
30
+ inject_into_file 'db/seeds.rb', "\n#{seed_code}\n", { :before => /\z/, :verbose => false }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,15 @@
1
+ # Facilitates conducting data migrations.
2
+ module <%= application_class_name %>DataUpdate
3
+ include TmxDataUpdate
4
+
5
+ # Returns the path where data updates are
6
+ def root_updates_path
7
+ <%= full_application_class_name %>.root.join('db','data_updates')
8
+ end
9
+
10
+ # Updates will run in production, but not in test an dev.
11
+ def should_run?(_)
12
+ Rails.env.production?
13
+ end
14
+
15
+ end
@@ -0,0 +1,48 @@
1
+ module TmxDataUpdate
2
+
3
+ # Should be included in seeds.rb to enable running data updates.
4
+ module Seeds
5
+ include UpdateClassLoader
6
+
7
+ # Applies all the updates from the given file system path.
8
+ #
9
+ # Basically, requires all the files from the given directory with a .rb
10
+ # suffix, instantiates the class in the file, and calls 'perform_update'
11
+ # on it. This naturally requires that classes obey the default naming
12
+ # convention, i.e. a file named sample_update.rb should define a class
13
+ # named SampleUpdate.
14
+ #
15
+ # @param [String|Pathname|Symbol] updates_path
16
+ # @return [Hash] A hash or results; update => result
17
+ def apply_updates(updates_path)
18
+ update_files = get_update_files(updates_path)
19
+ results = {}
20
+ update_files.each { |file|
21
+ update = file.split('.').first
22
+ unless update.blank?
23
+ update_class = get_update_class(updates_path, update)
24
+ res = update_class.new.perform_update
25
+ results[update] = res
26
+ end
27
+ }
28
+ results
29
+ end
30
+
31
+ # Gets the list of what should be the update files from the given file
32
+ # system path, sorted in alphabetical order. Returns an empty array if the
33
+ # updates_path is not on the file system or is not a directory.
34
+ # @param [String] updates_path
35
+ # @return [Array]
36
+ def get_update_files(updates_path)
37
+ if File.exists?(updates_path) && File.directory?(updates_path)
38
+ Dir.entries(updates_path).select { |file|
39
+ file.ends_with? '.rb'
40
+ }.sort
41
+ else
42
+ []
43
+ end
44
+ end
45
+ private :get_update_files
46
+
47
+ end
48
+ end
@@ -0,0 +1,57 @@
1
+ module TmxDataUpdate
2
+ # Machinery to load the actual data update classes
3
+ module UpdateClassLoader
4
+
5
+ # Loads the class corresponding to the given update name from the given
6
+ # file system path.
7
+ #
8
+ # @param [String|Pathname|Symbol] root_path
9
+ # @param [String|Symbol] update_name
10
+ #
11
+ # @return [::Class]
12
+ def get_update_class(root_path, update_name)
13
+ update = strip_seq_prefix(update_name.to_s)
14
+ file_name = find_file(root_path, update)
15
+ if file_name
16
+ file_name = File.basename(file_name, '.rb')
17
+ if root_path.to_s.ends_with?('/')
18
+ require "#{root_path}#{file_name}"
19
+ else
20
+ require "#{root_path}/#{file_name}"
21
+ end
22
+ update.camelize.constantize
23
+ else
24
+ raise LoadError, "Unable to find file for update #{update_name} in #{root_path}"
25
+ end
26
+ end
27
+
28
+ # Determines what's the actual filename for the given update name
29
+ def find_file(root_path, update)
30
+ update_file = "#{update}.rb"
31
+ Dir.entries(root_path).find{|entry| strip_seq_prefix(entry) == update_file }
32
+ end
33
+ private :find_file
34
+
35
+ # Takes the given file name and strips out the sequence prefix if any. If
36
+ # the file name starts with a digit, all characters up to the first '_' are
37
+ # considered part of the prefix. For example, for '00234ab_foo_bar' the
38
+ # prefix would be '00234ab' and 'foo_bar' would be returned.
39
+ #
40
+ # @param [String] update_name
41
+ #
42
+ # @return [String]
43
+ def strip_seq_prefix(update_name)
44
+ index = update_name =~ /_/
45
+ if index
46
+ start = update_name[0,1]
47
+ # If the first letter of the prefix is a digit, we assume the prefix is
48
+ # for sequence ordering purposes.
49
+ if %w(0 1 2 3 4 5 6 7 8 9).include?(start)
50
+ update_name = update_name[index + 1, update_name.size - index]
51
+ end
52
+ end
53
+ update_name
54
+ end
55
+ private :strip_seq_prefix
56
+ end
57
+ end
@@ -0,0 +1,31 @@
1
+ require 'active_record'
2
+
3
+
4
+ module TmxDataUpdate
5
+
6
+ # Adds support for methods which are part of ActiveRecord::Migration interface.
7
+ # This is a convenience feature to make using data updates easier.
8
+ # The module should only implement methods relevant in the context of data
9
+ # updates.
10
+ module ActiveRecordMigrationCompatible
11
+ delegate :execute, :to => ::ActiveRecord::Base
12
+ end
13
+
14
+ # The base class for all post-release data updates
15
+ #
16
+ # @abstract
17
+ class Updater
18
+ include ::TmxDataUpdate::ActiveRecordMigrationCompatible
19
+
20
+ # Performs the data update. The subclass must override this method.
21
+ def perform_update
22
+ raise "#{self.class} should override 'update_data'"
23
+ end
24
+
25
+ # Reverses the update. If the update is reversible, this class should
26
+ # be overriden in the subclass.
27
+ def undo_update
28
+ raise "#{self.class} does not support rolling back the update"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,58 @@
1
+ require 'active_support/all' # Should be required first.
2
+ require 'tmx_data_update/update_class_loader' #Should be required second.
3
+
4
+ require 'tmx_data_update/updater'
5
+ require 'tmx_data_update/seeds'
6
+
7
+ # Extends the migrations DSL to include the functionality to execute data updates.
8
+ #
9
+ # Note that each data update class is instantiated regardless of whether it is
10
+ # expected to run. This enables the developer to discover any data update that
11
+ # is incorrectly referenced in a migration, prior to deployment to production.
12
+ module TmxDataUpdate
13
+ include TmxDataUpdate::UpdateClassLoader
14
+
15
+ # Returns the root data updates path.
16
+ def root_updates_path
17
+ raise "Must override in subclass!"
18
+ end
19
+
20
+ # Return +true+ if the named update should run, +false+ otherwise. Subclasses
21
+ # are expected to override this as needed.
22
+ def should_run?(update_name)
23
+ true
24
+ end
25
+
26
+ # Applies the given update.
27
+ # @param [String|Symbol] update_name
28
+ def apply_update(update_name)
29
+ perform(update_name, :perform_update)
30
+ end
31
+
32
+ # Reverts the given update.
33
+ # @param [String|Symbol] update_name
34
+ def revert_update(update_name)
35
+ perform(update_name, :undo_update)
36
+ end
37
+
38
+ # Perform the update action.
39
+ # @param [String] update_name
40
+ # @param [Symbol] action Update action.
41
+ # Either :perform_update or :undo_update
42
+ def perform(update_name, action)
43
+ update = load_updater_class(update_name)
44
+
45
+ update.send(action) if should_run?(update_name)
46
+ end
47
+ private :perform
48
+
49
+ # Load the updater class and instantiate it. This enables the developer
50
+ # to discover when the data update has been incorrectly referenced in the
51
+ # migration, prior to deployment to production.
52
+ # @param [String] update_name
53
+ def load_updater_class(update_name)
54
+ get_update_class(root_updates_path, update_name).new
55
+ end
56
+ private :load_updater_class
57
+
58
+ end
metadata ADDED
@@ -0,0 +1,159 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tmx_data_update
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Arthur Shagall
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.1'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '3.1'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activerecord
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '3.1'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '3.1'
46
+ - !ruby/object:Gem::Dependency
47
+ name: i18n
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: jeweler
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: yard
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rspec
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: ! 'Allows us to cleanly handle updates to seed data post-launch.
111
+
112
+ '
113
+ email: arthur.shagall@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files:
117
+ - LICENSE
118
+ - README.md
119
+ files:
120
+ - lib/generators/tmx_data_update/create/USAGE
121
+ - lib/generators/tmx_data_update/create/create_generator.rb
122
+ - lib/generators/tmx_data_update/create/templates/data_update.rb
123
+ - lib/generators/tmx_data_update/create/templates/data_update_migration.rb
124
+ - lib/generators/tmx_data_update/helper.rb
125
+ - lib/generators/tmx_data_update/install/USAGE
126
+ - lib/generators/tmx_data_update/install/install_generator.rb
127
+ - lib/generators/tmx_data_update/install/templates/data_update_module.rb
128
+ - lib/tmx_data_update.rb
129
+ - lib/tmx_data_update/seeds.rb
130
+ - lib/tmx_data_update/update_class_loader.rb
131
+ - lib/tmx_data_update/updater.rb
132
+ - LICENSE
133
+ - README.md
134
+ homepage: http://www.tmxcredit.com
135
+ licenses: []
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ none: false
142
+ requirements:
143
+ - - ! '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ none: false
148
+ requirements:
149
+ - - ! '>='
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 1.8.24
155
+ signing_key:
156
+ specification_version: 3
157
+ summary: Handle post-release data updates through migrations
158
+ test_files: []
159
+ has_rdoc: