transfer 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.
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