transfer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in transfer.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,172 @@
1
+ ## Transfer Gem
2
+ Transfer data from source database to ActiveRecord, SequelModel or Mongoid models.
3
+
4
+ ### Installation
5
+ Include Transfer in Gemfile:
6
+
7
+ ```ruby
8
+ gem 'transfer'
9
+ ```
10
+
11
+ You can run bundle from command line:
12
+
13
+ ```console
14
+ bundle install
15
+ ```
16
+
17
+
18
+ ### Compatibility
19
+ Source database: all, supported by [Sequel](http://sequel.rubyforge.org/documentation.html).
20
+
21
+ Destination: ActiveRecord, SequelModel, Mongoid.
22
+
23
+
24
+ ## Configure
25
+ Set connection options of source database and global parameters:
26
+
27
+ ```ruby
28
+ Transfer.configure do |c|
29
+ c.host = "localhost"
30
+ c.adapter = "postgres"
31
+ c.database = "source_database"
32
+ c.user = "username"
33
+ c.password = "password"
34
+ end
35
+ ```
36
+
37
+ Available options:
38
+
39
+ * `validate` on/off model validations. Values: `true` or `false`, default is `false`.
40
+ * `failure_strategy` sets strategy if save of model is not successfully. Values: `:ignore` or `:rollback`, defult is `:ignore`.
41
+ * `before` [global callback](#global_callbacks).
42
+ * `success` [global callback](#global_callbacks).
43
+ * `failure` [global callback](#global_callbacks).
44
+ * `after` [global callback](#global_callbacks).
45
+ * another options interpreted as [Sequel database connection options](http://sequel.rubyforge.org/rdoc/files/doc/opening_databases_rdoc.html).
46
+
47
+
48
+ ## Usage
49
+ Direct transfer from source table `:users` to `User` model. All columns, including protected, existing in source table and destination model, will transferred:
50
+
51
+ ```ruby
52
+ transfer :users => User
53
+ ```
54
+
55
+ Filling the field `country` a constant value:
56
+
57
+ ```ruby
58
+ transfer :users => User do
59
+ country "England"
60
+ end
61
+ ```
62
+ Transfer `:name` column from source table `:users` into `first_name` of `User` model:
63
+
64
+ ```ruby
65
+ transfer :users => User do
66
+ first_name :name
67
+ end
68
+ ```
69
+ To produce, dynamic value (e.g. `dist_name` field), you can pass a block and access the row of source table:
70
+
71
+ ```ruby
72
+ transfer :users => User do
73
+ dist_name {|row| "Mr. #{row[:first_name]}"}
74
+ end
75
+ ```
76
+
77
+ ### Global callbacks <a name="global_callbacks"/>
78
+ This callbacks called for each `transfer`.
79
+
80
+ ```ruby
81
+ Transfer.configure do |c|
82
+ c.before do |klass, dataset|
83
+ #...
84
+ end
85
+ c.success do |model, row|
86
+ #...
87
+ end
88
+ c.failure do |model, row, exception|
89
+ #...
90
+ end
91
+ c.after do |klass, dataset|
92
+ #...
93
+ end
94
+ end
95
+ ```
96
+ Available global callbacks:
97
+
98
+ * `before` called before an transfer started. Parameters: `klass`, `dataset`.
99
+ * `success` called if save model is successfully. Parameters: `model`, `row`.
100
+ * `failure` called if save model is not successfully. Parameters: `row`, `exception`.
101
+ * `after` called after an transfer finished. Parameters: `klass`, `dataset`.
102
+
103
+ Description of parameters:
104
+
105
+ * `dataset` source table dataset, instance of [Sequel::Dataset](http://sequel.rubyforge.org/rdoc/classes/Sequel/Dataset.html).
106
+ * `klass` is destination class.
107
+ * `model` builded model, instance of `klass`.
108
+ * `row` of source table. Type: `Hash`.
109
+ * `exception` if save of model is not successfull.
110
+
111
+
112
+ ### Local transfer callbacks
113
+ You can specify callbacks in your `transfer` that are separate from the model callbacks. This callbacks called in model context, therefore `self` keyword points to model.
114
+
115
+ ```ruby
116
+ transfer :users => User do
117
+ before_save do |row|
118
+ self.messages << Message.build(:title => "Transfer", :description => "Welcome to new site, #{row[:fname]}!")
119
+ end
120
+ end
121
+ ```
122
+ Available callbacks:
123
+
124
+ * `before_save` called before save model. Paramaters: `row`.
125
+ * `after_save` called after successfully save model. Parameters: `row`.
126
+
127
+ where `row` is row of source table, type: `Hash`.
128
+
129
+
130
+ ### Filter columns
131
+ `only` filter passes source columns, specified in parameters.
132
+
133
+ ```ruby
134
+ transfer :users => User do
135
+ only :name
136
+ end
137
+ ```
138
+ `except` filter passes all source columns, except for those that are specified in the parameters:
139
+
140
+ ```ruby
141
+ transfer :users => User do
142
+ except :name
143
+ end
144
+ ```
145
+
146
+
147
+ ### Replace global options
148
+ Global options can be replaced global options, if it passed to `transfer`.
149
+
150
+ ```ruby
151
+ transfer :users => User, :validate => false, :failure_strategy => :rollback
152
+ ```
153
+ Available options for replace:
154
+
155
+ * `validate`
156
+ * `failure_strategy`
157
+
158
+
159
+ ### Logging
160
+ If you also want see progress of transfer in console, use e.g. [progressbar gem](https://github.com/peleteiro/progressbar) with global callbacks.
161
+
162
+ ```ruby
163
+ require 'progressbar'
164
+
165
+ Transfer.configure do |c|
166
+ c.before {|klass, dataset| @pbar = ProgressBar.new(klass, dataset.count) }
167
+ c.success { @pbar.inc }
168
+ c.after { @pbar.halt }
169
+ end
170
+
171
+ transfer :users => User
172
+ ```
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,34 @@
1
+ require 'sequel'
2
+
3
+ module Transfer
4
+ class Config
5
+ def initialize data = {}
6
+ data.each do |key, value|
7
+ send "#{key}=", value
8
+ end
9
+ end
10
+
11
+ def connection_options
12
+ @connection_options ||= {}
13
+ end
14
+
15
+ def process_options
16
+ @process_options ||= { :validate => true, :failure_strategy => :ignore }
17
+ end
18
+
19
+ def connection
20
+ @connection ||= Sequel.connect connection_options
21
+ end
22
+
23
+ def method_missing name, *args, &block
24
+ /^((validate|failure_strategy|failure|before|after|success)|\w+)(=)?$/.match name
25
+ opt = $2 ? process_options : connection_options
26
+ if block_given?
27
+ opt[$1.to_sym] = block
28
+ else
29
+ $3 ? opt[$1.to_sym] = args[0] : opt[name]
30
+ end
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,31 @@
1
+ class Transfer::Generators::ActiveRecord < Transfer::Generators::Base
2
+
3
+ def self.supports? klass
4
+ defined?(ActiveRecord) && klass.ancestors.include?(ActiveRecord::Base)
5
+ end
6
+
7
+ def column_present? name
8
+ super(name) || klass.column_names.include?(name.to_s)
9
+ end
10
+
11
+ def transaction &block
12
+ klass.transaction &block
13
+ end
14
+
15
+ def create attributes, row, options={}, callbacks={}
16
+ model = klass.new attributes, :without_protection => true
17
+
18
+ model.instance_exec(row, &callbacks[:before_save]) if callbacks[:before_save]
19
+ save_options = options.select{|key| key == :validate }
20
+
21
+ model.save! save_options
22
+ options[:success].call(model, row) if options[:success]
23
+ model.instance_exec(row, &callbacks[:after_save]) if callbacks[:after_save]
24
+ model
25
+ rescue Exception => e
26
+ options[:failure].call(model, row, e) if options[:failure]
27
+ raise ActiveRecord::Rollback if options[:failure_strategy] == :rollback
28
+ model
29
+ end
30
+
31
+ end
@@ -0,0 +1,26 @@
1
+ class Transfer::Generators::Base
2
+ attr_accessor :klass
3
+
4
+ def self.supports? klass
5
+ raise "Not Yet Implemented!"
6
+ end
7
+
8
+ def initialize klass
9
+ @klass = klass
10
+ end
11
+
12
+ def before
13
+ end
14
+
15
+ def after
16
+ end
17
+
18
+ def create attributes, row, options={}, callbacks={}
19
+ raise "Not Yet Implemented!"
20
+ end
21
+
22
+ def column_present? name
23
+ klass.method_defined? name
24
+ end
25
+
26
+ end
@@ -0,0 +1,28 @@
1
+ class Transfer::Generators::Mongoid < Transfer::Generators::Base
2
+
3
+ def self.supports? klass
4
+ defined?(Mongoid) && klass.ancestors.include?(Mongoid::Document)
5
+ end
6
+
7
+ def transaction
8
+ yield
9
+ rescue
10
+ klass.delete_all
11
+ end
12
+
13
+ def create attributes, row, options={}, callbacks={}
14
+ model = klass.new attributes, :without_protection => true
15
+ model.instance_exec(row, &callbacks[:before_save]) if callbacks[:before_save]
16
+ save_options = options.select{|key| key == :validate }
17
+ model.save! save_options
18
+
19
+ options[:success].call(model, row) if options[:success]
20
+ model.instance_exec(row, &callbacks[:after_save]) if callbacks[:after_save]
21
+ model
22
+ rescue Exception => e
23
+ options[:failure].call(model, row, e) if options[:failure]
24
+ raise if options[:failure_strategy] == :rollback
25
+ model
26
+ end
27
+
28
+ end
@@ -0,0 +1,47 @@
1
+ class Transfer::Generators::Sequel < Transfer::Generators::Base
2
+
3
+ def self.supports? klass
4
+ defined?(Sequel) && klass.ancestors.include?(Sequel::Model)
5
+ end
6
+
7
+ def before
8
+ klass.unrestrict_primary_key
9
+ @strict_param_setting = klass.strict_param_setting
10
+ klass.strict_param_setting = false
11
+ klass.set_restricted_columns if @restricted_columns = klass.restricted_columns
12
+ klass.set_allowed_columns if @allowed_columns = klass.allowed_columns
13
+ end
14
+
15
+ def after
16
+ klass.restrict_primary_key
17
+ klass.strict_param_setting = @strict_param_setting
18
+ klass.set_restricted_columns *@restricted_columns if @restricted_columns
19
+ klass.set_allowed_columns *@allowed_columns if @allowed_columns
20
+ end
21
+
22
+ def column_present? name
23
+ super name || klass.columns.include?(name.to_sym)
24
+ end
25
+
26
+ def transaction &block
27
+ klass.db.transaction :savepoint => true, &block
28
+ end
29
+
30
+ def create attributes, row, options={}, callbacks={}
31
+ model = klass.new attributes
32
+ model.instance_exec(row, &callbacks[:before_save]) if callbacks[:before_save]
33
+
34
+ save_options = options.select{|key| key == :validate }
35
+ save_options[:raise_on_failure] = true
36
+
37
+ model.save save_options
38
+ model.instance_exec(row, &callbacks[:after_save]) if callbacks[:after_save]
39
+ options[:success].call(model, row) if options[:success]
40
+ model
41
+ rescue Exception => e
42
+ options[:failure].call(model, row, e) if options[:failure]
43
+ raise Sequel::Rollback if options[:failure_strategy] == :rollback
44
+ model
45
+ end
46
+
47
+ end
@@ -0,0 +1,90 @@
1
+ class Transfer::Transferer
2
+ attr_reader :dataset, :klass
3
+
4
+ def initialize dataset, klass, &block
5
+ @dataset = dataset
6
+ @klass = klass
7
+ block.arity == 1 ? yield(self) : instance_eval(&block) if block_given?
8
+ end
9
+
10
+ def columns
11
+ @columns ||= dataset.columns.each_with_object({}){|i,hash| hash[i]=i if generator.column_present? i }
12
+ end
13
+
14
+ def build_attributes source
15
+ attrs = {}
16
+ columns.each do |name, value|
17
+ attrs[name] = case value
18
+ when Proc
19
+ value.call source
20
+ when Symbol
21
+ source[value]
22
+ else
23
+ value
24
+ end
25
+ end
26
+ attrs
27
+ end
28
+
29
+ def method_missing symbol, *args, &block
30
+ add_column symbol, block_given? ? block : args[0]
31
+ end
32
+
33
+ def process options = {}
34
+ generator.before
35
+ options[:before].call(klass, dataset) if options[:before]
36
+ generator.transaction do
37
+ dataset.each do |row|
38
+ attributes = build_attributes row
39
+ generator.create attributes, row, options, callbacks
40
+ end
41
+ end
42
+ options[:after].call(klass, dataset) if options[:after]
43
+ generator.after
44
+ end
45
+
46
+ def callbacks
47
+ @callbacks ||= {}
48
+ end
49
+
50
+ def before_save &block
51
+ callbacks[:before_save] = block
52
+ end
53
+
54
+ def after_save &block
55
+ callbacks[:after_save] = block
56
+ end
57
+
58
+ # def failure &block
59
+ # callbacks[:failure] = block
60
+ # end
61
+
62
+ def generator
63
+ @generator ||= GENERATORS.detect{|g| g.supports? klass}.new klass
64
+ end
65
+
66
+ private
67
+
68
+ GENERATORS = [
69
+ Transfer::Generators::Sequel,
70
+ Transfer::Generators::ActiveRecord,
71
+ Transfer::Generators::Mongoid,
72
+ Transfer::Generators::Base
73
+ ]
74
+
75
+
76
+ def add_column key, value
77
+ raise ArgumentError.new("method ##{key} in class #{klass} is not defined!") unless generator.column_present?(key)
78
+ raise ArgumentError.new("source column ##{value} not exists!") if value.instance_of?(Symbol) and !dataset.columns.include?(value)
79
+ columns[key] = value
80
+ end
81
+
82
+ def only *args
83
+ columns.delete_if {|key| !args.include?(key) }
84
+ end
85
+
86
+ def except *args
87
+ columns.delete_if {|key| args.include?(key) }
88
+ end
89
+
90
+ end
@@ -0,0 +1,3 @@
1
+ module Transfer
2
+ VERSION = "0.0.1"
3
+ end
data/lib/transfer.rb ADDED
@@ -0,0 +1,50 @@
1
+ require "transfer/version"
2
+
3
+
4
+ module Transfer
5
+ extend self
6
+ autoload :Config, 'transfer/config'
7
+ autoload :Transferer, 'transfer/transferer'
8
+
9
+ module Generators
10
+ autoload :Sequel, 'transfer/generators/sequel'
11
+ autoload :ActiveRecord, 'transfer/generators/active_record'
12
+ autoload :Mongoid, 'transfer/generators/mongoid'
13
+ autoload :Base, 'transfer/generators/base'
14
+ end
15
+
16
+ def configure name = :default, &block
17
+ config = Config.new
18
+ yield config if block_given?
19
+ configs[name] = config
20
+ end
21
+
22
+ def configs
23
+ @configs ||= Hash.new {|hash, key| raise "config #{key} not exists" }
24
+ end
25
+ end
26
+
27
+
28
+ def transfer *args, &block
29
+ raise ArgumentError if args.length == 0
30
+
31
+ case args[0]
32
+ when Symbol, String
33
+ args[0] = Transfer.configs[args[0].to_sym]
34
+ transfer *args, &block
35
+ when Hash
36
+ transfer :default, *args, &block
37
+ when Transfer::Config
38
+ raise ArgumentError.new("second argument should be Hash!") unless args[1].instance_of?(Hash)
39
+ config, options = args[0], args[1]
40
+ process_keys = [:validate, :failure_strategy]
41
+ process_options = config.process_options.merge options.select{|key| process_keys.include?(key) }
42
+ sources = options.select{|key| !process_keys.include?(key) }
43
+ sources.each do |key, value|
44
+ dataset = config.connection[key]
45
+ transferer = Transfer::Transferer.new dataset, value, &block
46
+ transferer.process process_options
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,9 @@
1
+ Fabricator :johnny_hollow, :from => :source_user do
2
+ fname "Johnny"
3
+ lname "Hollow"
4
+ end
5
+
6
+ Fabricator :olivia_lufkin, :from => :source_user do
7
+ fname "Olivia"
8
+ lname "Lufkin"
9
+ end
@@ -0,0 +1,45 @@
1
+ require 'rspec'
2
+ require 'rr'
3
+ require 'fabrication'
4
+ require 'transfer'
5
+ require 'database_cleaner'
6
+
7
+ Dir["./spec/support/**/*.rb"].each {|f| require f}
8
+
9
+ module TransfererHelper
10
+ def save_failure entity
11
+ if entity.kind_of?(Class)
12
+ instance_of(entity).save! { raise "force exception!" }
13
+ instance_of(entity).save { raise "force exception!" }
14
+ else
15
+ stub(entity).save! { raise "force exception!" }
16
+ stub(entity).save { raise "force exception!" }
17
+ end
18
+ end
19
+ end
20
+
21
+ RSpec.configure do |c|
22
+ c.mock_with :rr
23
+ c.include TransfererHelper
24
+
25
+ c.before :suite do
26
+ DatabaseCleaner[:mongoid].strategy = :truncation
27
+ DatabaseCleaner[:active_record].strategy = :transaction
28
+ end
29
+
30
+ c.around do |example|
31
+ Sequel.transaction Sequel::DATABASES, :rollback => :always do
32
+ example.run
33
+ end
34
+ end
35
+
36
+ c.before :each do
37
+ DatabaseCleaner.start
38
+ end
39
+
40
+ c.after :each do
41
+ DatabaseCleaner.clean
42
+ end
43
+ end
44
+
45
+
@@ -0,0 +1,42 @@
1
+ require 'active_record'
2
+
3
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
4
+ ActiveRecord::Migration.verbose = false
5
+
6
+
7
+ class ActiveRecordUserMigration < ActiveRecord::Migration
8
+ def self.up
9
+ create_table :active_record_users, :force => true do |t|
10
+ t.string :first_name
11
+ t.string :last_name
12
+ t.string :full_name
13
+ t.string :before_save_value
14
+ t.string :protected_value
15
+ end
16
+ end
17
+
18
+ def self.down
19
+ drop_table :active_record_users
20
+ end
21
+ end
22
+
23
+ ActiveRecordUserMigration.up
24
+
25
+
26
+ class ActiveRecordUser < ActiveRecord::Base
27
+ set_table_name "active_record_users"
28
+ attr_accessor :dynamic_value
29
+ attr_protected :protected_value
30
+ end
31
+
32
+ class ActiveRecordUserWithFalseValidation < ActiveRecord::Base
33
+ set_table_name "active_record_users"
34
+ attr_accessor :dynamic_value
35
+ attr_protected :protected_value
36
+
37
+ validate :fake
38
+
39
+ def fake
40
+ errors.add :fake, 'fake'
41
+ end
42
+ end
@@ -0,0 +1,38 @@
1
+ require 'mongoid'
2
+
3
+ Mongoid.configure do |config|
4
+ config.master = Mongo::Connection.new.db("godfather")
5
+ end
6
+
7
+ class MongoidUser
8
+ include Mongoid::Document
9
+
10
+ field :first_name, :type => String
11
+ field :last_name, :type => String
12
+ field :full_name, :type => String
13
+ field :before_save_value, :type => String
14
+ field :protected_value, :type => String
15
+
16
+ attr_accessor :dynamic_value
17
+ attr_protected :protected_value
18
+ end
19
+
20
+
21
+ class MongoidUserWithFalseValidation
22
+ include Mongoid::Document
23
+
24
+ field :first_name, :type => String
25
+ field :last_name, :type => String
26
+ field :full_name, :type => String
27
+ field :before_save_value, :type => String
28
+ field :protected_value, :type => String
29
+
30
+ attr_accessor :dynamic_value
31
+ attr_protected :protected_value
32
+
33
+ validate :fake
34
+
35
+ def fake
36
+ errors.add :fake, 'fake'
37
+ end
38
+ end
@@ -0,0 +1,45 @@
1
+ require 'sequel'
2
+
3
+ Sequel.extension :migration
4
+
5
+ DESTINATION_DB = Sequel.sqlite
6
+
7
+ class SequelUserMigration < Sequel::Migration
8
+ def up
9
+ create_table! :sequel_users do
10
+ primary_key :id
11
+ String :first_name
12
+ String :last_name
13
+ String :full_name
14
+ String :before_save_value
15
+ String :protected_value
16
+ end
17
+ end
18
+ def down
19
+ drop_table :sequel_users
20
+ end
21
+ end
22
+
23
+
24
+ SequelUserMigration.apply DESTINATION_DB, :up
25
+
26
+
27
+ class SequelUser < Sequel::Model
28
+ attr_accessor :dynamic_value
29
+ set_restricted_columns :protected_value
30
+ end
31
+
32
+
33
+ class SequelUserWithFalseValidation < Sequel::Model(:sequel_users)
34
+ attr_accessor :dynamic_value
35
+ set_restricted_columns :protected_value
36
+
37
+ def validate
38
+ super
39
+ errors.add(:fake, 'fake error')
40
+ end
41
+ end
42
+
43
+
44
+ SequelUser.db = DESTINATION_DB
45
+ SequelUserWithFalseValidation.db = DESTINATION_DB