hash-migrations 0.0.1

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,19 @@
1
+ .idea
2
+ .DS_Store
3
+ *.gem
4
+ *.rbc
5
+ .bundle
6
+ .config
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem "rake"
7
+ gem "rspec"
8
+ gem "guard"
9
+ gem "guard-bundler"
10
+ gem "guard-rspec"
11
+ gem "rb-fsevent"
12
+ gem "terminal-notifier-guard"
13
+ end
@@ -0,0 +1,13 @@
1
+ guard 'bundler' do
2
+ watch('Gemfile')
3
+ watch(/^.+\.gemspec/)
4
+ end
5
+
6
+ guard 'rspec' do
7
+ watch(%r{^spec/unit/.+_spec\.rb$})
8
+ watch(%r{^spec/functional/.+_spec\.rb$})
9
+ watch(%r{^lib/hash/migrations/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
10
+ watch(%r{^lib/hash/migrations/(.+)\.rb$}) { |m| "spec/functional/hash_migrations_spec.rb" }
11
+ watch('lib/hash/migrations/core_ext.rb') { 'spec' }
12
+ watch('spec/spec_helper.rb') { 'spec' }
13
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Martin Englund
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,54 @@
1
+ # Hash Migrations
2
+
3
+ Using a yaml file and then loading it into a hash, to store settings is a common pattern.
4
+ Over time the settings usually change, which will cause the code to contain checks for which version of the settings is used.
5
+
6
+ This gem introduces migrations for hashes so you can keep the code simple and maintain changes to the hash in separate migration files.
7
+
8
+ It can be used in two ways, either it returns a new hash:
9
+
10
+ migrated_hash = Hash::Migrator.run(hash, dir, options)
11
+
12
+
13
+ Or it can modify the existing hash, if you call the instance method.
14
+
15
+ hash.migrate(dir, options)
16
+
17
+
18
+ A sample hash migration looks like this, and the file should be named `YYYYMMDDHHmmss_description.rb`
19
+
20
+ Hash.migration do
21
+ up do |hash|
22
+ hash[:foo] = hash['foo']
23
+ hash.delete('foo')
24
+ end
25
+
26
+ down do
27
+ hash['foo'] = hash[:foo]
28
+ hash.delete(:foo)
29
+ end
30
+ end
31
+
32
+ The migration assumes you have a top-level hash key named `:schema_version` (initialized to `0` if not present) which is used to track the migrations needed.
33
+
34
+ ## Installation
35
+
36
+ Add this line to your application's Gemfile:
37
+
38
+ gem 'hash-migrations'
39
+
40
+ And then execute:
41
+
42
+ $ bundle
43
+
44
+ Or install it yourself as:
45
+
46
+ $ gem install hash-migrations
47
+
48
+ ## Contributing
49
+
50
+ 1. Fork it
51
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
52
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
53
+ 4. Push to the branch (`git push origin my-new-feature`)
54
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hash/migrations/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "hash-migrations"
8
+ gem.version = Hash::Migrations::VERSION
9
+ gem.authors = ["Martin Englund"]
10
+ gem.email = ["martin@englund.nu"]
11
+ gem.description = %q{Migrations to manage hashes}
12
+ gem.summary = %q{Migrations for hashes}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
@@ -0,0 +1,19 @@
1
+ require 'hash/migrations/version'
2
+ require 'hash/migrations/errors'
3
+ require 'hash/migrations/migration'
4
+ require 'hash/migrations/migrator'
5
+ require 'hash/migrations/core_ext'
6
+
7
+ module Hash::Migrations
8
+ # Apply migrations to a hash
9
+ # @param [Hash] hash hash to be migrated
10
+ # @param [String] dir path to the directory containing the migrations to apply
11
+ # @param [Hash] options options
12
+ # @return [Hash] migrated hash
13
+ def run(hash, dir, options={direction: :up})
14
+ hash.dup.migrate(dir, options)
15
+ end
16
+
17
+ module_function :run
18
+ end
19
+
@@ -0,0 +1,28 @@
1
+ module Hash::Migrations::InstanceMethods
2
+ # Apply migrations to the hash
3
+ # @param [String] dir path to the directory containing the migrations to apply
4
+ # @param [Hash] options options
5
+ def migrate(dir, options={})
6
+ @direction = options.fetch(:direction, :up)
7
+ Hash::Migrations::Migrator.new(dir).run(self, options)
8
+ end
9
+
10
+ def up(&block)
11
+ yield self if @direction == :up
12
+ end
13
+
14
+ def down(&block)
15
+ yield self if @direction == :down
16
+ end
17
+ end
18
+
19
+ module Hash::Migrations::ClassMethods
20
+ def migration(&block)
21
+ block.call
22
+ end
23
+ end
24
+
25
+ class Hash
26
+ include Hash::Migrations::InstanceMethods
27
+ extend Hash::Migrations::ClassMethods
28
+ end
@@ -0,0 +1,4 @@
1
+ module Hash::Migrations
2
+ class MigrationFailed < StandardError; end
3
+ class VersionError < StandardError; end
4
+ end
@@ -0,0 +1,33 @@
1
+ module Hash::Migrations
2
+ class Migration
3
+ attr_reader :migration, :version
4
+
5
+ VERSION_REGEXP = %r{/(\d+)_.+}.freeze
6
+
7
+ def to_str
8
+ migration
9
+ end
10
+
11
+ def load(path)
12
+ @path = path
13
+
14
+ unless File.exists?(@path)
15
+ raise ArgumentError, "unable to load migration '#@path'"
16
+ end
17
+
18
+ @version = version_from_path
19
+ @migration = File.new(@path).read
20
+ end
21
+
22
+ private
23
+
24
+ def version_from_path
25
+ match = @path.match(VERSION_REGEXP)
26
+ unless match
27
+ raise VersionError, "unable to extract version from '#@path'"
28
+ end
29
+ match[1].to_i
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,52 @@
1
+ module Hash::Migrations
2
+ class Migrator
3
+ # @return [Array[Hash::Migrations::Migration]] the list of migrations in this migration
4
+ attr_reader :migrations
5
+
6
+ def initialize(migrations_dir=nil)
7
+ @migrations = []
8
+
9
+ return unless migrations_dir
10
+
11
+ unless Dir.exists?(migrations_dir)
12
+ raise ArgumentError, "#{migrations_dir} is not a directory"
13
+ end
14
+
15
+ Dir.entries(migrations_dir).each do |entry|
16
+ unless entry.match(/^\.+$/)
17
+ path = File.join(migrations_dir, entry)
18
+ migration = Hash::Migrations::Migration.new
19
+ migration.load(path)
20
+ add(migration)
21
+ end
22
+ end
23
+ end
24
+
25
+ def add(migration)
26
+ @migrations << migration
27
+ end
28
+
29
+ # @param [Hash] hash hash to be migrated
30
+ # @param [Hash] options options
31
+ # @return [Hash] migrated hash
32
+ def run(hash, options={})
33
+ # TODO raise error on existing version format?
34
+ # TODO only migrate to a certain version
35
+ schema_version = options.fetch(:schema_version, :schema_version)
36
+ version = hash[schema_version] || 0
37
+
38
+ migrations.each do |migration|
39
+ if version < migration.version
40
+ hash.instance_eval migration
41
+ version = migration.version
42
+ hash[schema_version] = version
43
+ end
44
+ end
45
+
46
+ hash
47
+ rescue => e
48
+ # TODO better error message?
49
+ raise Hash::Migrations::MigrationFailed.new(e)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ module Hash::Migrations
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,5 @@
1
+ Hash.migration do
2
+ up do |hash|
3
+ hash[:foo] = Array(hash[:foo][:bar])
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ Hash.migration do
2
+
3
+ # turn keys which are strings into symbols
4
+ up do |hash|
5
+ hash.keys.each do |key|
6
+ if key.is_a?(String)
7
+ hash[key.to_sym] = hash[key]
8
+ hash.delete(key)
9
+ end
10
+ end
11
+ end
12
+
13
+ # turn keys which are symbols into strings
14
+ down do |hash|
15
+ hash.keys.each do |key|
16
+ if key.is_a?(Symbol)
17
+ hash[key.to_s] = hash[key]
18
+ hash.delete(key)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ Hash.migration do
2
+ up do |hash|
3
+ hash[:foo] = Array(hash[:foo])
4
+ end
5
+
6
+ down do |hash|
7
+ hash[:foo] = hash[:foo].first
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ Hash.migration do
2
+ up do |hash|
3
+ hash[:bar] = 'foobar'
4
+ end
5
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hash::Migrations do
4
+
5
+ context 'on hash' do
6
+ it 'should change self' do
7
+ hash = {:foo => 'bar'}
8
+
9
+ hash.migrate(asset('migrations'))
10
+
11
+ hash[:foo].should == %w[bar]
12
+ end
13
+
14
+ it 'should run all migrations in the directory' do
15
+ hash = {'foo' => 'bar'}
16
+
17
+ hash.migrate(asset('migrations'))
18
+
19
+ hash[:foo].should == %w[bar]
20
+ end
21
+
22
+ it 'should support a different schema_version name' do
23
+ hash = {'foo' => 'bar'}
24
+
25
+ hash.migrate(asset('migrations'), schema_version: :version)
26
+
27
+ hash[:foo].should == %w[bar]
28
+ hash[:version].should == 20130317115514
29
+ end
30
+
31
+ it 'should skip migrations less than the version' do
32
+ hash = {schema_version: 20130317115504, foo: 'bar'}
33
+
34
+ hash.migrate(asset('migrations'))
35
+
36
+ hash[:schema_version].should == 20130317115514
37
+ hash[:foo].should == %w[bar]
38
+ hash[:bar].should == 'foobar'
39
+ end
40
+
41
+ it 'should raise an error if a migration fails' do
42
+ expect {
43
+ {}.migrate(asset('error'))
44
+ }.to raise_error Hash::Migrations::MigrationFailed
45
+ end
46
+ end
47
+
48
+ context 'on module' do
49
+ it 'should return a new hash' do
50
+ hash = {:foo => 'bar'}
51
+ dup = hash.dup
52
+
53
+ migrated_hash = Hash::Migrations.run(hash, asset('migrations'))
54
+
55
+ migrated_hash[:foo].should == %w[bar]
56
+ migrated_hash.should_not == hash
57
+ hash.should == dup
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,13 @@
1
+ require 'hash/migrations'
2
+
3
+ def asset(file)
4
+ File.expand_path(File.join('..', 'assets', file), __FILE__)
5
+ end
6
+
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ config.order = 'random'
13
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hash::Migrations::Migration do
4
+ let(:migration_file) { File.join(asset('error'), '20130318164134_error.rb') }
5
+ let(:migration) {
6
+ m = described_class.new
7
+ m.load(migration_file)
8
+ m
9
+ }
10
+
11
+ describe 'version' do
12
+ it 'should extract the version from the path' do
13
+ migration.version.should == 20130318164134
14
+ end
15
+
16
+ it 'should raise an error if it can not extract version from the path' do
17
+ incorrect_file = File.join(asset('fail'), 'fail.rb')
18
+ expect {
19
+ described_class.new.load(incorrect_file)
20
+ }.to raise_error Hash::Migrations::VersionError, %r[unable to extract version from '#{incorrect_file}']
21
+ end
22
+ end
23
+
24
+ describe 'migration' do
25
+ it 'should raise an exception when it can not load the migration' do
26
+ missing_file = File.join(asset('fail'), 'missing.rb')
27
+
28
+ expect {
29
+ described_class.new.load(missing_file)
30
+ }.to raise_error ArgumentError, %r[unable to load migration '#{missing_file}']
31
+ end
32
+
33
+ it 'should load migration from a path' do
34
+ migration.to_str.should == 'foobar'
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hash::Migrations::Migrator do
4
+ it 'should raise an error if the directory does not exist' do
5
+ expect {
6
+ described_class.new('/path/to/non/existing/directory')
7
+ }.to raise_error ArgumentError, '/path/to/non/existing/directory is not a directory'
8
+ end
9
+
10
+ it 'should skip the migration if the version is higher' do
11
+ migration = double(Hash::Migrations::Migration)
12
+ migration.stub(version: 20130318)
13
+
14
+ migrator = described_class.new
15
+ migrator.add(migration)
16
+
17
+ migrated_hash = migrator.run({})
18
+ migrated_hash[:schema_version].should == 20130318
19
+ end
20
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hash-migrations
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Martin Englund
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-14 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Migrations to manage hashes
15
+ email:
16
+ - martin@englund.nu
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - .rspec
23
+ - Gemfile
24
+ - Guardfile
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - hash-migrations.gemspec
29
+ - lib/hash/migrations.rb
30
+ - lib/hash/migrations/core_ext.rb
31
+ - lib/hash/migrations/errors.rb
32
+ - lib/hash/migrations/migration.rb
33
+ - lib/hash/migrations/migrator.rb
34
+ - lib/hash/migrations/version.rb
35
+ - spec/assets/error/20130318164134_error.rb
36
+ - spec/assets/fail/fail.rb
37
+ - spec/assets/migrations/20130317115503_initial.rb
38
+ - spec/assets/migrations/20130317115507_second.rb
39
+ - spec/assets/migrations/20130317115514_third.rb
40
+ - spec/functional/hash_migrations_spec.rb
41
+ - spec/spec_helper.rb
42
+ - spec/unit/migraton_spec.rb
43
+ - spec/unit/migrator_spec.rb
44
+ homepage: ''
45
+ licenses: []
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ segments:
57
+ - 0
58
+ hash: -1303593863770819748
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ segments:
66
+ - 0
67
+ hash: -1303593863770819748
68
+ requirements: []
69
+ rubyforge_project:
70
+ rubygems_version: 1.8.23
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: Migrations for hashes
74
+ test_files:
75
+ - spec/assets/error/20130318164134_error.rb
76
+ - spec/assets/fail/fail.rb
77
+ - spec/assets/migrations/20130317115503_initial.rb
78
+ - spec/assets/migrations/20130317115507_second.rb
79
+ - spec/assets/migrations/20130317115514_third.rb
80
+ - spec/functional/hash_migrations_spec.rb
81
+ - spec/spec_helper.rb
82
+ - spec/unit/migraton_spec.rb
83
+ - spec/unit/migrator_spec.rb