abstract_importer 1.1.0 → 1.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 390699648f23aa027bf392f0f9095bf4b6801e3b
4
- data.tar.gz: 5f1d0ccb8c4db21649913fa3914b7f6a4f0ba22c
3
+ metadata.gz: df7b7a0811459f30502cf09d03741312919b9f54
4
+ data.tar.gz: 116840924377d0562121169b715b9fd64f49fde1
5
5
  SHA512:
6
- metadata.gz: 1081c7623c65fcbd0748385d6e30f139dcf0d33703ace0c172e9b3e2d9a07aac3cabff0de5f600dc40dc0de7d28af5de2d0865960252888bb5b101711c8ed252
7
- data.tar.gz: b59ab4abc7f8c9634e00c24840a01dfe0974f5b7f57e8f34b57129ad10e8d7f0c226972c3323b67107b3aeec6741b2f9e7aaf072ccc8515dbae308c59ca8072b
6
+ metadata.gz: a9c6b45e74533ba5a1ef07432d677834057649b66656c07c61594ea34e91bb449e5346167fb69cad7d7c464a1c48899e420f8f73a051a22b731afeecfab1db49
7
+ data.tar.gz: b64e4b346bbc2f566c5686b95edde07f3f49f86be9a971916ac870a3d583038ef31da07cad2ed4e7d58f9f976c6f4de92343dfd93c7449841523cafd1488176d
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # AbstractImporter
2
2
 
3
3
  [![Build Status](https://travis-ci.org/concordia-publishing-house/abstract_importer.png?branch=master)](https://travis-ci.org/concordia-publishing-house/abstract_importer)
4
-
5
4
  [![Code Climate](https://codeclimate.com/github/concordia-publishing-house/abstract_importer.png)](https://codeclimate.com/github/concordia-publishing-house/abstract_importer)
6
5
 
7
6
  AbstractImporter provides services for importing complex data from an arbitrary data source. It:
@@ -54,7 +53,7 @@ end
54
53
  `MyImporter`'s initializer takes two arguments: `parent` and `data_source`:
55
54
 
56
55
  * `parent` is any object that will respond to the names of your collections with an `ActiveRecord::Relation`.
57
- * `data_source` is any object that will respond to the names of your collections by yielding a hash of attributes once for every record you should import.
56
+ * `data_source` is any object that will respond to the names of your collections with an `Enumerator`.
58
57
 
59
58
  Here are reasonable classes for `parent` and `data_source`:
60
59
 
@@ -70,15 +69,19 @@ end
70
69
  # data source
71
70
  class Database
72
71
  def students
73
- yield id: 457, name: "Ron"
74
- yield id: 458, name: "Ginny"
75
- yield id: 459, name: "Fred"
76
- yield id: 460, name: "George"
72
+ Enumerator.new do |e|
73
+ e.yield id: 457, name: "Ron"
74
+ e.yield id: 458, name: "Ginny"
75
+ e.yield id: 459, name: "Fred"
76
+ e.yield id: 460, name: "George"
77
+ end
77
78
  end
78
79
 
79
80
  def parents
80
- yield id: 88, name: "Arthur"
81
- yield id: 89, name: "Molly"
81
+ Enumerator.new do |e|
82
+ e.yield id: 88, name: "Arthur"
83
+ e.yield id: 89, name: "Molly"
84
+ end
82
85
  end
83
86
  end
84
87
  ```
@@ -118,7 +121,7 @@ class MyImporter < AbstractImporter::Base
118
121
  import.students do |options|
119
122
  options.finder :find_student
120
123
  options.before_build { |attrs| attrs.merge(name: attrs[:name].capitalize) }
121
- options.on_complete :students_completed
124
+ options.after_all :students_completed
122
125
  end
123
126
  import.parents
124
127
  end
@@ -156,9 +159,13 @@ The complete list of callbacks is below.
156
159
 
157
160
  `after_create` is called with the original hash of attributes and the newly-saved record right after it is successfully saved.
158
161
 
159
- ##### on_complete
162
+ ##### before_all
163
+
164
+ `before_all` is called just before the records in a collection are been processed.
165
+
166
+ ##### after_all
160
167
 
161
- `on_complete` is called when all of the records in a collection have been processed.
168
+ `after_all` is called when all of the records in a collection have been processed.
162
169
 
163
170
 
164
171
 
@@ -15,7 +15,11 @@ module AbstractImporter
15
15
  yield @import_plan = ImportPlan.new
16
16
  end
17
17
 
18
- attr_reader :import_plan
18
+ def depends_on(*dependencies)
19
+ @dependencies = dependencies
20
+ end
21
+
22
+ attr_reader :import_plan, :dependencies
19
23
  end
20
24
 
21
25
 
@@ -31,11 +35,16 @@ module AbstractImporter
31
35
  @id_map = IdMap.new
32
36
  @results = {}
33
37
  @import_plan = self.class.import_plan.to_h
38
+ @atomic = options.fetch(:atomic, false)
34
39
  @collections = []
35
40
  end
36
41
 
37
42
  attr_reader :source, :parent, :reporter, :id_map, :results
38
43
 
44
+ def atomic?
45
+ @atomic
46
+ end
47
+
39
48
  def dry_run?
40
49
  @dry_run
41
50
  end
@@ -53,7 +62,9 @@ module AbstractImporter
53
62
  reporter.finish_setup(ms)
54
63
 
55
64
  ms = Benchmark.ms do
56
- collections.each &method(:import_collection)
65
+ with_transaction do
66
+ collections.each &method(:import_collection)
67
+ end
57
68
  end
58
69
 
59
70
  teardown
@@ -142,8 +153,19 @@ module AbstractImporter
142
153
  end
143
154
  end
144
155
 
156
+ def dependencies
157
+ @dependencies ||= Array(self.class.dependencies).map do |name|
158
+ reflection = parent.class.reflect_on_association(name)
159
+ model = reflection.klass
160
+ table_name = model.table_name
161
+ scope = parent.public_send(name)
162
+
163
+ Collection.new(name, model, table_name, scope, nil)
164
+ end
165
+ end
166
+
145
167
  def prepopulate_id_map!
146
- collections.each do |collection|
168
+ (collections + dependencies).each do |collection|
147
169
  query = collection.scope.where("#{collection.table_name}.legacy_id IS NOT NULL")
148
170
  map = values_of(query, :id, :legacy_id) \
149
171
  .each_with_object({}) { |(id, legacy_id), map| map[legacy_id] = id }
@@ -167,5 +189,15 @@ module AbstractImporter
167
189
  reporter.count_notice "#{plural}.#{foreign_key} will be nil: a #{depends_on.to_s.singularize} with the legacy id #{legacy_id} was not mapped."
168
190
  end
169
191
 
192
+
193
+
194
+ def with_transaction(&block)
195
+ if atomic?
196
+ ActiveRecord::Base.transaction(requires_new: true, &block)
197
+ else
198
+ block.call
199
+ end
200
+ end
201
+
170
202
  end
171
203
  end
@@ -30,11 +30,12 @@ module AbstractImporter
30
30
  reporter.start_collection(self)
31
31
  prepare!
32
32
 
33
+ invoke_callback(:before_all)
33
34
  summary.ms = Benchmark.ms do
34
35
  each_new_record &method(:process_record)
35
36
  end
37
+ invoke_callback(:after_all)
36
38
 
37
- invoke_callback(:on_complete)
38
39
  reporter.finish_collection(self, summary)
39
40
  summary
40
41
  end
@@ -54,43 +55,56 @@ module AbstractImporter
54
55
  # has foreign keys that refer to another
55
56
  next unless association.macro == :belongs_to
56
57
 
57
- # We don't at this time support polymorphic associations
58
- # which would require extending id_map to take the foreign
59
- # type fields into account.
60
- #
61
- # Rails can't return `association.table_name` so easily
62
- # because `table_name` comes from `klass` and `klass`
63
- # isn't predetermined.
64
- next if association.options[:polymorphic]
65
-
66
- depends_on = association.table_name.to_sym
67
- foreign_key = association.foreign_key.to_sym
68
-
69
58
  # We support skipping some mappings entirely. I believe
70
59
  # this is largely to cut down on verbosity in the log
71
60
  # files and should be refactored to another place in time.
61
+ foreign_key = association.foreign_key.to_sym
72
62
  next unless remap_foreign_key?(name, foreign_key)
73
63
 
74
- mappings << Proc.new do |hash|
75
- if hash.key?(foreign_key)
76
- hash[foreign_key] = map_foreign_key(hash[foreign_key], name, foreign_key, depends_on)
77
- else
78
- reporter.count_notice "#{name}.#{foreign_key} will not be mapped because it is not used"
79
- end
64
+ if association.options[:polymorphic]
65
+ mappings << prepare_polymorphic_mapping!(association)
66
+ else
67
+ mappings << prepare_mapping!(association)
80
68
  end
81
69
  end
82
70
  mappings
83
71
  end
84
72
 
73
+ def prepare_mapping!(association)
74
+ depends_on = association.table_name.to_sym
75
+ foreign_key = association.foreign_key.to_sym
76
+
77
+ Proc.new do |attrs|
78
+ if attrs.key?(foreign_key)
79
+ attrs[foreign_key] = map_foreign_key(attrs[foreign_key], name, foreign_key, depends_on)
80
+ else
81
+ reporter.count_notice "#{name}.#{foreign_key} will not be mapped because it is not used"
82
+ end
83
+ end
84
+ end
85
+
86
+ def prepare_polymorphic_mapping!(association)
87
+ foreign_key = association.foreign_key.to_sym
88
+ foreign_type = association.foreign_key.gsub(/_id$/, "_type").to_sym
89
+
90
+ Proc.new do |attrs|
91
+ if attrs.key?(foreign_key) && attrs.key?(foreign_type)
92
+ foreign_model = attrs[foreign_type]
93
+ depends_on = foreign_model.tableize.to_sym if foreign_model
94
+ attrs[foreign_key] = depends_on && map_foreign_key(attrs[foreign_key], name, foreign_key, depends_on)
95
+ else
96
+ reporter.count_notice "#{name}.#{foreign_key} will not be mapped because it is not used"
97
+ end
98
+ end
99
+ end
100
+
85
101
 
86
102
 
87
103
 
88
104
 
89
105
  def each_new_record
90
- source.public_send(name) do |hash_or_hashes|
91
- Array.wrap(hash_or_hashes).each do |hash|
92
- yield hash.dup
93
- end
106
+ source.public_send(name).each do |attrs|
107
+ yield attrs.dup
94
108
  end
95
109
  end
96
110
 
@@ -114,6 +128,8 @@ module AbstractImporter
114
128
  else
115
129
  summary.invalid += 1
116
130
  end
131
+ rescue ::AbstractImporter::Skip
132
+ summary.skipped += 1
117
133
  end
118
134
 
119
135
 
@@ -154,7 +170,7 @@ module AbstractImporter
154
170
  # rescue_callback has one shot to fix things
155
171
  invoke_callback(:rescue, record) unless record.valid?
156
172
 
157
- if record.save
173
+ if record.valid? && record.save
158
174
  invoke_callback(:after_create, hash, record)
159
175
  id_map << record
160
176
 
@@ -162,7 +178,7 @@ module AbstractImporter
162
178
  true
163
179
  else
164
180
 
165
- reporter.record_failed(record)
181
+ reporter.record_failed(record, hash)
166
182
  false
167
183
  end
168
184
  end
@@ -186,4 +202,7 @@ module AbstractImporter
186
202
  end
187
203
 
188
204
  end
205
+
206
+ class Skip < StandardError; end
207
+
189
208
  end
@@ -5,7 +5,8 @@ module AbstractImporter
5
5
  :before_build_callback,
6
6
  :before_create_callback,
7
7
  :after_create_callback,
8
- :on_complete_callback
8
+ :before_all_callback,
9
+ :after_all_callback
9
10
 
10
11
  def finder(sym=nil, &block)
11
12
  @finder_callback = sym || block
@@ -27,8 +28,12 @@ module AbstractImporter
27
28
  @rescue_callback = sym || block
28
29
  end
29
30
 
30
- def on_complete(sym=nil, &block)
31
- @on_complete_callback = sym || block
31
+ def before_all(sym=nil, &block)
32
+ @before_all_callback = sym || block
33
+ end
34
+
35
+ def after_all(sym=nil, &block)
36
+ @after_all_callback = sym || block
32
37
  end
33
38
 
34
39
  end
@@ -54,7 +54,7 @@ module AbstractImporter
54
54
  io.print "." unless production?
55
55
  end
56
56
 
57
- def record_failed(record)
57
+ def record_failed(record, hash)
58
58
  io.print "×" unless production?
59
59
 
60
60
  error_messages = invalid_params[record.class.name] ||= {}
@@ -111,8 +111,9 @@ module AbstractImporter
111
111
  stat "#{summary.already_imported} #{plural} were imported previously"
112
112
  stat "#{summary.redundant} #{plural} would create duplicates and will not be imported"
113
113
  stat "#{summary.invalid} #{plural} were invalid"
114
+ stat "#{summary.skipped} #{plural} were skipped"
114
115
  stat "#{summary.created} #{plural} were imported"
115
- stat "#{distance_of_time(summary.ms)} elapsed (#{(summary.ms / summary.total).to_i}ms each)"
116
+ stat "#{distance_of_time(summary.ms)} elapsed (#{summary.average_ms.to_i}ms each)"
116
117
  else
117
118
  stat "#{distance_of_time(summary.ms)} elapsed"
118
119
  end
@@ -1,8 +1,13 @@
1
1
  module AbstractImporter
2
- class Summary < Struct.new(:total, :redundant, :created, :already_imported, :invalid, :ms)
2
+ class Summary < Struct.new(:total, :redundant, :created, :already_imported, :invalid, :ms, :skipped)
3
3
 
4
4
  def initialize
5
- super(0,0,0,0,0,0)
5
+ super(0,0,0,0,0,0,0)
6
+ end
7
+
8
+ def average_ms
9
+ return nil if total == 0
10
+ ms / total
6
11
  end
7
12
 
8
13
  end
@@ -1,3 +1,3 @@
1
1
  module AbstractImporter
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0.rc1"
3
3
  end
@@ -18,6 +18,20 @@ class CallbackTest < ActiveSupport::TestCase
18
18
  import!
19
19
  assert_equal ["Harry", "Ron", "Hermione"], account.students.pluck(:name)
20
20
  end
21
+
22
+ should "allow you to skip certain records" do
23
+ plan do |import|
24
+ import.students do |options|
25
+ options.before_build do |attrs|
26
+ raise AbstractImporter::Skip if attrs[:name] == "Harry Potter"
27
+ end
28
+ end
29
+ end
30
+
31
+ import!
32
+ assert_equal ["Ron Weasley", "Hermione Granger"], account.students.pluck(:name)
33
+ assert_equal 1, results[:students].skipped
34
+ end
21
35
  end
22
36
 
23
37
 
@@ -75,11 +89,28 @@ class CallbackTest < ActiveSupport::TestCase
75
89
 
76
90
 
77
91
 
78
- context "on_complete" do
92
+ context "before_all" do
93
+ setup do
94
+ plan do |import|
95
+ import.students do |options|
96
+ options.before_all :callback
97
+ end
98
+ end
99
+ end
100
+
101
+ should "should be invoked before the collection has been imported" do
102
+ mock(importer).callback.once
103
+ import!
104
+ end
105
+ end
106
+
107
+
108
+
109
+ context "after_all" do
79
110
  setup do
80
111
  plan do |import|
81
112
  import.students do |options|
82
- options.on_complete :callback
113
+ options.after_all :callback
83
114
  end
84
115
  end
85
116
  end
@@ -45,7 +45,44 @@ class ImporterTest < ActiveSupport::TestCase
45
45
  assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
46
46
  end
47
47
 
48
- should "preserve mappings event when a record was previously imported" do
48
+ should "preserve mappings even when a record was previously imported" do
49
+ harry = account.students.create!(name: "Harry Potter", legacy_id: 456)
50
+ import!
51
+ assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
52
+ end
53
+
54
+ context "when {atomic: true}" do
55
+ should "rollback the whole import if an part fails" do
56
+ mock(importer).atomic? { true }
57
+ mock.instance_of(Parent).save { raise "hell" }
58
+ import! rescue
59
+ assert_equal 0, account.parents.count, "No parents should have been imported with the exception"
60
+ assert_equal 0, account.students.count, "Expected students to have been rolled back"
61
+ end
62
+ end
63
+
64
+ context "when {atomic: false}" do
65
+ should "not rollback the whole import if an part fails" do
66
+ mock(importer).atomic? { false }
67
+ mock.instance_of(Parent).save { raise "hell" }
68
+ import! rescue
69
+ assert_equal 0, account.parents.count, "No parents should have been imported with the exception"
70
+ assert_equal 3, account.students.count, "Expected students not to have been rolled back"
71
+ end
72
+ end
73
+ end
74
+
75
+
76
+
77
+ context "with a dependency" do
78
+ setup do
79
+ depends_on :students
80
+ plan do |import|
81
+ import.parents
82
+ end
83
+ end
84
+
85
+ should "preserve mappings when a dependency was imported by another importer" do
49
86
  harry = account.students.create!(name: "Harry Potter", legacy_id: 456)
50
87
  import!
51
88
  assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
@@ -103,4 +140,23 @@ class ImporterTest < ActiveSupport::TestCase
103
140
 
104
141
 
105
142
 
143
+ context "with polymorphic associations" do
144
+ setup do
145
+ plan do |import|
146
+ import.cats
147
+ import.owls
148
+ import.students
149
+ end
150
+ end
151
+
152
+ should "preserve mappings" do
153
+ import!
154
+ assert_equal 2, account.students.map(&:pet).compact.count, "Expected two students to still be linked to their pets upon import"
155
+ assert_kind_of Owl, account.students.find_by_name("Harry Potter").pet, "Expected Harry's pet to be an Owl"
156
+ assert_kind_of Cat, account.students.find_by_name("Hermione Granger").pet, "Expected Harry's pet to be a Cat"
157
+ end
158
+ end
159
+
160
+
161
+
106
162
  end
@@ -1,32 +1,56 @@
1
1
  class MockDataSource
2
2
 
3
+
3
4
  def students
4
- yield id: 456, name: "Harry Potter"
5
- yield id: 457, name: "Ron Weasley"
6
- yield id: 458, name: "Hermione Granger"
5
+ Enumerator.new do |e|
6
+ e.yield id: 456, name: "Harry Potter", pet_type: "Owl", pet_id: 901
7
+ e.yield id: 457, name: "Ron Weasley", pet_type: nil, pet_id: nil
8
+ e.yield id: 458, name: "Hermione Granger", pet_type: "Cat", pet_id: 901
9
+ end
7
10
  end
8
11
 
9
12
  def parents
10
- yield id: 88, name: "James Potter", student_id: 456
11
- yield id: 89, name: "Lily Potter", student_id: 456
13
+ Enumerator.new do |e|
14
+ e.yield id: 88, name: "James Potter", student_id: 456
15
+ e.yield id: 89, name: "Lily Potter", student_id: 456
16
+ end
12
17
  end
13
18
 
14
19
  def locations
15
- yield id: 5, slug: "godric's-hollow" # <-- invalid
16
- yield id: 6, slug: "azkaban"
20
+ Enumerator.new do |e|
21
+ e.yield id: 5, slug: "godric's-hollow" # <-- invalid
22
+ e.yield id: 6, slug: "azkaban"
23
+ end
17
24
  end
18
25
 
19
26
  def subjects
20
- yield id: 49, name: "Care of Magical Creatures", student_ids: [456]
21
- yield id: 50, name: "Advanced Potions", student_ids: [456, 457]
22
- yield id: 51, name: "History of Magic", student_ids: [457]
23
- yield id: 52, name: "Arithmancy", student_ids: [458]
24
- yield id: 53, name: "Study of Ancient Runes", student_ids: [458]
27
+ Enumerator.new do |e|
28
+ e.yield id: 49, name: "Care of Magical Creatures", student_ids: [456]
29
+ e.yield id: 50, name: "Advanced Potions", student_ids: [456, 457]
30
+ e.yield id: 51, name: "History of Magic", student_ids: [457]
31
+ e.yield id: 52, name: "Arithmancy", student_ids: [458]
32
+ e.yield id: 53, name: "Study of Ancient Runes", student_ids: [458]
33
+ end
25
34
  end
26
35
 
27
36
  def grades
28
- yield id: 500, subject_id: 50, student_id: 457, value: "Acceptable"
29
- yield id: 501, subject_id: 51, student_id: 457, value: "Troll"
37
+ Enumerator.new do |e|
38
+ e.yield id: 500, subject_id: 50, student_id: 457, value: "Acceptable"
39
+ e.yield id: 501, subject_id: 51, student_id: 457, value: "Troll"
40
+ end
41
+ end
42
+
43
+ def cats
44
+ Enumerator.new do |e|
45
+ e.yield id: 901, name: "Crookshanks"
46
+ end
30
47
  end
31
48
 
49
+ def owls
50
+ Enumerator.new do |e|
51
+ e.yield id: 901, name: "Hedwig"
52
+ end
53
+ end
54
+
55
+
32
56
  end
@@ -2,6 +2,7 @@ class Student < ActiveRecord::Base
2
2
  has_and_belongs_to_many :subjects
3
3
  has_many :grades
4
4
  has_many :parents
5
+ belongs_to :pet, polymorphic: true
5
6
 
6
7
  def report_card
7
8
  subjects.map do |subject|
@@ -34,4 +35,14 @@ class Account < ActiveRecord::Base
34
35
  has_many :subjects
35
36
  has_many :grades
36
37
  has_many :locations
38
+ has_many :cats
39
+ has_many :owls
40
+ end
41
+
42
+ class Cat < ActiveRecord::Base
43
+ has_one :student, as: :pet
44
+ end
45
+
46
+ class Owl < ActiveRecord::Base
47
+ has_one :student, as: :pet
37
48
  end
@@ -8,6 +8,8 @@ ActiveRecord::Schema.define(:version => 1) do
8
8
  t.integer "legacy_id"
9
9
  t.string "name"
10
10
  t.string "house"
11
+ t.string "pet_type"
12
+ t.integer "pet_id"
11
13
  end
12
14
 
13
15
  create_table "parents", :force => true do |t|
@@ -42,4 +44,16 @@ ActiveRecord::Schema.define(:version => 1) do
42
44
  t.string "value"
43
45
  end
44
46
 
47
+ create_table "owls", :force => true do |t|
48
+ t.integer "account_id"
49
+ t.integer "legacy_id"
50
+ t.string "name"
51
+ end
52
+
53
+ create_table "cats", :force => true do |t|
54
+ t.integer "account_id"
55
+ t.integer "legacy_id"
56
+ t.string "name"
57
+ end
58
+
45
59
  end
@@ -50,12 +50,16 @@ class ActiveSupport::TestCase
50
50
 
51
51
  protected
52
52
 
53
- attr_reader :account, :results
53
+ attr_reader :account, :results, :data_source
54
54
 
55
55
  def plan(&block)
56
56
  @klass.import(&block)
57
57
  end
58
58
 
59
+ def depends_on(*args)
60
+ @klass.depends_on(*args)
61
+ end
62
+
59
63
  def import!
60
64
  @results = importer.perform!
61
65
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abstract_importer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Lail
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-19 00:00:00.000000000 Z
11
+ date: 2014-04-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -209,12 +209,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
209
209
  version: '0'
210
210
  required_rubygems_version: !ruby/object:Gem::Requirement
211
211
  requirements:
212
- - - '>='
212
+ - - '>'
213
213
  - !ruby/object:Gem::Version
214
- version: '0'
214
+ version: 1.3.1
215
215
  requirements: []
216
216
  rubyforge_project:
217
- rubygems_version: 2.0.3
217
+ rubygems_version: 2.2.1
218
218
  signing_key:
219
219
  specification_version: 4
220
220
  summary: Provides services for the mass-import of complex relational data