tmx_data_update 0.3.0

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/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: