abstract_importer 1.2.1 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6545777a4f5b26c7f43754894478c1ee2b38db44
4
- data.tar.gz: defa4ee62445d06bfdadaf9453b2c54f19829c90
3
+ metadata.gz: aef74ea6f4a839ec7a9fc349cf07cb44a53001e4
4
+ data.tar.gz: 8c2a8da38743766e795050d5fe67b1102ec44f60
5
5
  SHA512:
6
- metadata.gz: 607c5f129b8ac2742ad76f8c4a964b3ee24f5304b7d1701409bc5e3b0a5bf3eca0dda5d00d5f8556383d7ec32ba3786259946d55d3eee9f88709b83028c595fc
7
- data.tar.gz: edaecfc589c1844af50cf53239940ebfc17052be632fa2bf563fc8bcd74d8e33521b157b9caa73c3285c3685d4e724bda514749cefa9f3fc51ba85d787b4810e
6
+ metadata.gz: 5215f833089ae23c768c3b9763438e72d43030cb87c8e5d99eb85c61e1736d837c4f4d356a7b7b8708ac3de8136b18f4fb3b9bc2653c7e6c981bebc015b35bfa
7
+ data.tar.gz: 62ab5e5d9c9b8d984a0bfa098bea336238491ec6546783ae41757dc9922d6f5c7c491a1a3c537fce0bd46d95fb29fc7a8eaa9900ad647f8c66307c8fcbd9e856
@@ -0,0 +1 @@
1
+ 2.1.2
@@ -1,8 +1,10 @@
1
1
  # .travis.yml
2
2
  language: ruby
3
3
  rvm:
4
- - 2.0.0
5
- env:
6
- - DB=sqlite
4
+ - 2.1.2
5
+ before_install:
6
+ - sudo apt-add-repository -y ppa:travis-ci/sqlite3
7
+ - sudo apt-get -y update
8
+ - sudo apt-get install sqlite3=3.7.15.1-1~travis1
7
9
  script:
8
10
  - bundle exec rake test
@@ -17,7 +17,8 @@ Gem::Specification.new do |spec|
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.add_dependency "activerecord"
20
+ spec.add_dependency "activerecord", ">= 4.0"
21
+ spec.add_dependency "activerecord-insert_many", "~> 0.1.1"
21
22
 
22
23
  spec.add_development_dependency "bundler", "~> 1.3"
23
24
  spec.add_development_dependency "minitest", "~> 4.7"
@@ -9,29 +9,29 @@ require 'abstract_importer/summary'
9
9
 
10
10
  module AbstractImporter
11
11
  class Base
12
-
12
+
13
13
  class << self
14
14
  def import
15
15
  yield @import_plan = ImportPlan.new
16
16
  end
17
-
17
+
18
18
  def depends_on(*dependencies)
19
19
  @dependencies = dependencies
20
20
  end
21
-
21
+
22
22
  attr_reader :import_plan, :dependencies
23
23
  end
24
-
25
-
26
-
24
+
25
+
26
+
27
27
  def initialize(parent, source, options={})
28
28
  @source = source
29
29
  @parent = parent
30
-
30
+
31
31
  io = options.fetch(:io, $stderr)
32
32
  @reporter = default_reporter(io)
33
33
  @dry_run = options.fetch(:dry_run, false)
34
-
34
+
35
35
  @id_map = IdMap.new
36
36
  @results = {}
37
37
  @import_plan = self.class.import_plan.to_h
@@ -41,171 +41,159 @@ module AbstractImporter
41
41
  @only = Array(options[:only]) if options.key?(:only)
42
42
  @collections = []
43
43
  end
44
-
44
+
45
45
  attr_reader :source, :parent, :reporter, :id_map, :results
46
-
46
+
47
47
  def atomic?
48
48
  @atomic
49
49
  end
50
-
50
+
51
51
  def dry_run?
52
52
  @dry_run
53
53
  end
54
-
55
-
56
-
57
-
58
-
54
+
55
+
56
+
57
+
58
+
59
59
  def perform!
60
60
  reporter.start_all(self)
61
-
61
+
62
62
  ms = Benchmark.ms do
63
63
  setup
64
64
  end
65
65
  reporter.finish_setup(ms)
66
-
66
+
67
67
  ms = Benchmark.ms do
68
68
  with_transaction do
69
69
  collections.each &method(:import_collection)
70
70
  end
71
71
  end
72
-
72
+
73
73
  teardown
74
74
  reporter.finish_all(self, ms)
75
75
  results
76
76
  end
77
-
77
+
78
78
  def setup
79
79
  verify_source!
80
80
  verify_parent!
81
81
  instantiate_collections!
82
82
  prepopulate_id_map!
83
83
  end
84
-
84
+
85
85
  def import_collection(collection)
86
86
  return if skip? collection
87
87
  results[collection.name] = CollectionImporter.new(self, collection).perform!
88
88
  end
89
-
89
+
90
90
  def teardown
91
91
  end
92
-
92
+
93
93
  def skip?(collection)
94
94
  return true if skip.member?(collection.name)
95
95
  return true if only && !only.member?(collection.name)
96
96
  false
97
97
  end
98
-
98
+
99
99
  def strategy_for(collection)
100
100
  strategy_name = @strategies.fetch collection.name, :default
101
101
  AbstractImporter::Strategies.const_get :"#{strategy_name.capitalize}Strategy"
102
102
  end
103
-
104
-
105
-
106
-
107
-
103
+
104
+
105
+
106
+
107
+
108
108
  def describe_source
109
109
  source.to_s
110
110
  end
111
-
111
+
112
112
  def describe_destination
113
113
  parent.to_s
114
114
  end
115
-
116
-
117
-
118
-
119
-
115
+
116
+
117
+
118
+
119
+
120
120
  def remap_foreign_key?(plural, foreign_key)
121
121
  true
122
122
  end
123
-
123
+
124
124
  def map_foreign_key(legacy_id, plural, foreign_key, depends_on)
125
125
  id_map.apply!(legacy_id, depends_on)
126
126
  rescue IdMap::IdNotMappedError
127
127
  record_no_id_in_map_error(legacy_id, plural, foreign_key, depends_on)
128
128
  nil
129
129
  end
130
-
131
-
132
-
133
-
134
-
130
+
131
+
132
+
133
+
134
+
135
135
  private
136
-
136
+
137
137
  attr_reader :collections, :import_plan, :skip, :only
138
-
138
+
139
139
  def verify_source!
140
140
  import_plan.keys.each do |collection|
141
141
  next if source.respond_to?(collection)
142
-
142
+
143
143
  raise "#{source.class} does not respond to `#{collection}`; " <<
144
- "but #{self.class} plans to import records with that name"
144
+ "but #{self.class} plans to import records with that name"
145
145
  end
146
146
  end
147
-
147
+
148
148
  def verify_parent!
149
149
  import_plan.keys.each do |collection|
150
150
  next if parent.respond_to?(collection)
151
-
151
+
152
152
  raise "#{parent.class} does not have a collection named `#{collection}`; " <<
153
153
  "but #{self.class} plans to import records with that name"
154
154
  end
155
155
  end
156
-
156
+
157
157
  def instantiate_collections!
158
158
  @collections = import_plan.map do |name, block|
159
159
  reflection = parent.class.reflect_on_association(name)
160
160
  model = reflection.klass
161
161
  table_name = model.table_name
162
162
  scope = parent.public_send(name)
163
-
163
+
164
164
  options = ImportOptions.new
165
165
  instance_exec(options, &block) if block
166
-
166
+
167
167
  Collection.new(name, model, table_name, scope, options)
168
168
  end
169
169
  end
170
-
170
+
171
171
  def dependencies
172
172
  @dependencies ||= Array(self.class.dependencies).map do |name|
173
173
  reflection = parent.class.reflect_on_association(name)
174
174
  model = reflection.klass
175
175
  table_name = model.table_name
176
176
  scope = parent.public_send(name)
177
-
177
+
178
178
  Collection.new(name, model, table_name, scope, nil)
179
179
  end
180
180
  end
181
-
181
+
182
182
  def prepopulate_id_map!
183
183
  (collections + dependencies).each do |collection|
184
- query = collection.scope.where("#{collection.table_name}.legacy_id IS NOT NULL")
185
- map = values_of(query, :id, :legacy_id) \
186
- .each_with_object({}) { |(id, legacy_id), map| map[legacy_id] = id }
187
-
188
- id_map.init collection.table_name, map
189
- end
190
- end
191
-
192
- def values_of(query, *columns)
193
- if Rails.version < "4.0.0"
194
- query = query.select(columns.map { |column| "#{query.table_name}.#{column}" }.join(", "))
195
- ActiveRecord::Base.connection.select_rows(query.to_sql)
196
- else
197
- query.pluck(*columns)
184
+ id_map.init collection.table_name, collection.scope
185
+ .where("#{collection.table_name}.legacy_id IS NOT NULL")
198
186
  end
199
187
  end
200
-
201
-
202
-
188
+
189
+
190
+
203
191
  def record_no_id_in_map_error(legacy_id, plural, foreign_key, depends_on)
204
192
  reporter.count_notice "#{plural}.#{foreign_key} will be nil: a #{depends_on.to_s.singularize} with the legacy id #{legacy_id} was not mapped."
205
193
  end
206
-
207
-
208
-
194
+
195
+
196
+
209
197
  def with_transaction(&block)
210
198
  if atomic?
211
199
  ActiveRecord::Base.transaction(requires_new: true, &block)
@@ -213,7 +201,7 @@ module AbstractImporter
213
201
  block.call
214
202
  end
215
203
  end
216
-
204
+
217
205
  def default_reporter(io)
218
206
  case ENV["IMPORT_REPORTER"].to_s.downcase
219
207
  when "none" then Reporters::NullReporter.new(io)
@@ -221,6 +209,6 @@ module AbstractImporter
221
209
  else Reporters::DebugReporter.new(io)
222
210
  end
223
211
  end
224
-
212
+
225
213
  end
226
214
  end
@@ -2,15 +2,15 @@ require "abstract_importer/strategies"
2
2
 
3
3
  module AbstractImporter
4
4
  class CollectionImporter
5
-
5
+
6
6
  def initialize(importer, collection)
7
7
  @importer = importer
8
8
  @collection = collection
9
9
  @strategy = importer.strategy_for(collection).new(self)
10
10
  end
11
-
11
+
12
12
  attr_reader :importer, :collection, :summary, :strategy
13
-
13
+
14
14
  delegate :name,
15
15
  :table_name,
16
16
  :model,
@@ -18,7 +18,7 @@ module AbstractImporter
18
18
  :options,
19
19
  :association_attrs,
20
20
  :to => :collection
21
-
21
+
22
22
  delegate :dry_run?,
23
23
  :parent,
24
24
  :source,
@@ -27,46 +27,47 @@ module AbstractImporter
27
27
  :id_map,
28
28
  :map_foreign_key,
29
29
  :to => :importer
30
-
31
-
32
-
30
+
31
+
32
+
33
33
  def perform!
34
34
  reporter.start_collection(self)
35
35
  prepare!
36
-
36
+
37
37
  invoke_callback(:before_all)
38
38
  summary.ms = Benchmark.ms do
39
39
  each_new_record do |attributes|
40
40
  strategy.process_record(attributes)
41
41
  end
42
42
  end
43
+ strategy.flush
43
44
  invoke_callback(:after_all)
44
-
45
+
45
46
  reporter.finish_collection(self, summary)
46
47
  summary
47
48
  end
48
-
49
-
50
-
49
+
50
+
51
+
51
52
  def prepare!
52
53
  @summary = Summary.new
53
54
  @mappings = prepare_mappings!
54
55
  end
55
-
56
+
56
57
  def prepare_mappings!
57
58
  mappings = []
58
59
  model.reflect_on_all_associations.each do |association|
59
-
60
+
60
61
  # We only want the associations where this record
61
62
  # has foreign keys that refer to another
62
63
  next unless association.macro == :belongs_to
63
-
64
+
64
65
  # We support skipping some mappings entirely. I believe
65
66
  # this is largely to cut down on verbosity in the log
66
67
  # files and should be refactored to another place in time.
67
68
  foreign_key = association.foreign_key.to_sym
68
69
  next unless remap_foreign_key?(name, foreign_key)
69
-
70
+
70
71
  if association.options[:polymorphic]
71
72
  mappings << prepare_polymorphic_mapping!(association)
72
73
  else
@@ -75,11 +76,11 @@ module AbstractImporter
75
76
  end
76
77
  mappings
77
78
  end
78
-
79
+
79
80
  def prepare_mapping!(association)
80
81
  depends_on = association.table_name.to_sym
81
82
  foreign_key = association.foreign_key.to_sym
82
-
83
+
83
84
  Proc.new do |attrs|
84
85
  if attrs.key?(foreign_key)
85
86
  attrs[foreign_key] = map_foreign_key(attrs[foreign_key], name, foreign_key, depends_on)
@@ -88,11 +89,11 @@ module AbstractImporter
88
89
  end
89
90
  end
90
91
  end
91
-
92
+
92
93
  def prepare_polymorphic_mapping!(association)
93
94
  foreign_key = association.foreign_key.to_sym
94
95
  foreign_type = association.foreign_key.gsub(/_id$/, "_type").to_sym
95
-
96
+
96
97
  Proc.new do |attrs|
97
98
  if attrs.key?(foreign_key) && attrs.key?(foreign_type)
98
99
  foreign_model = attrs[foreign_type]
@@ -103,23 +104,23 @@ module AbstractImporter
103
104
  end
104
105
  end
105
106
  end
106
-
107
-
108
-
107
+
108
+
109
+
109
110
  def each_new_record
110
111
  source.public_send(name).each do |attrs|
111
112
  yield attrs.dup
112
113
  end
113
114
  end
114
-
115
-
116
-
115
+
116
+
117
+
117
118
  def remap_foreign_keys!(hash)
118
119
  @mappings.each do |proc|
119
120
  proc.call(hash)
120
121
  end
121
122
  end
122
-
123
+
123
124
  def redundant_record?(hash)
124
125
  existing_record = invoke_callback(:finder, hash)
125
126
  if existing_record
@@ -129,9 +130,9 @@ module AbstractImporter
129
130
  false
130
131
  end
131
132
  end
132
-
133
-
134
-
133
+
134
+
135
+
135
136
  def invoke_callback(callback, *args)
136
137
  callback_name = :"#{callback}_callback"
137
138
  callback = options.public_send(callback_name)
@@ -139,9 +140,9 @@ module AbstractImporter
139
140
  callback = importer.method(callback) if callback.is_a?(Symbol)
140
141
  callback.call(*args)
141
142
  end
142
-
143
+
143
144
  end
144
-
145
+
145
146
  class Skip < StandardError; end
146
-
147
+
147
148
  end