importer 0.3.2 → 0.4.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.
- data/README.rdoc +10 -14
- data/Rakefile +6 -8
- data/VERSION +1 -1
- data/importer.gemspec +26 -39
- data/lib/importer.rb +11 -70
- data/lib/importer/adapters/active_record_adapter.rb +102 -0
- data/lib/importer/adapters/mongo_mapper_adapter.rb +104 -0
- data/lib/importer/import.rb +39 -1
- data/lib/importer/imported_object.rb +29 -2
- data/lib/importer/parser/base.rb +4 -0
- data/lib/importer/parser/csv.rb +2 -2
- data/lib/importer/parser/xml.rb +2 -2
- data/test/factories.rb +0 -9
- data/test/helper.rb +14 -48
- data/test/importer/adapters/active_record_adapter_test.rb +114 -0
- data/test/importer/adapters/mongo_mapper_adapter_test.rb +115 -0
- data/test/importer/import_test.rb +45 -0
- data/test/importer/imported_object_test.rb +18 -0
- metadata +20 -47
- data/lib/importer/import/active_record.rb +0 -41
- data/lib/importer/import/simple.rb +0 -61
- data/lib/importer/imported_object/active_record.rb +0 -41
- data/lib/importer/imported_object/simple.rb +0 -25
- data/rails_generators/importer/importer_generator.rb +0 -18
- data/rails_generators/importer/templates/imported_objects_migration.rb +0 -19
- data/rails_generators/importer/templates/imports_migration.rb +0 -17
- data/test/importer/import/active_record_test.rb +0 -36
- data/test/importer/import/simple_test.rb +0 -36
- data/test/importer/imported_object/active_record_test.rb +0 -37
- data/test/importer/imported_object/simple_test.rb +0 -35
- data/test/importer_test.rb +0 -50
data/lib/importer/import.rb
CHANGED
@@ -1,5 +1,43 @@
|
|
1
1
|
module Importer
|
2
|
-
|
2
|
+
# Summary of import.
|
3
|
+
# Contains detailed information about import process, all imported objects,
|
4
|
+
# no matter if they were imported successfully or not.
|
5
|
+
#
|
6
|
+
# The importer builds it's Import instance by adding imported objects via
|
7
|
+
# +add_object+.
|
8
|
+
#
|
9
|
+
# If you want to have your own implementation of Import summary
|
10
|
+
# (f.e. activerecord-based), you can force the importer to use it with:
|
11
|
+
#
|
12
|
+
# Product.import(path_to_xml_or_csv_file, :import => CustomImportClass)
|
13
|
+
#
|
14
|
+
# Just be sure to implement +add_object+ and +build_imported_object+
|
15
|
+
# methods in your custom class.
|
16
|
+
class Import
|
17
|
+
attr_reader :imported_objects
|
3
18
|
|
19
|
+
def initialize
|
20
|
+
@imported_objects = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_object(imported_object)
|
24
|
+
imported_objects << imported_object
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_imported_object
|
28
|
+
ImportedObject.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def new_imported_objects
|
32
|
+
imported_objects.select { |object| object.state == 'new_object' }
|
33
|
+
end
|
34
|
+
|
35
|
+
def existing_imported_objects
|
36
|
+
imported_objects.select { |object| object.state == 'existing_object' }
|
37
|
+
end
|
38
|
+
|
39
|
+
def invalid_imported_objects
|
40
|
+
imported_objects.select { |object| object.state == 'invalid_object' }
|
41
|
+
end
|
4
42
|
end
|
5
43
|
end
|
@@ -1,5 +1,32 @@
|
|
1
1
|
module Importer
|
2
|
-
|
2
|
+
# Instances of this class are created during import process and contain
|
3
|
+
# detailed information about imported objects.
|
4
|
+
#
|
5
|
+
# Attributes:
|
6
|
+
# * +state+ - an imported object can be in one of three states:
|
7
|
+
# * new_object - new object was detected and successfully imported
|
8
|
+
# * existing_object - already existing object was detected and successfully
|
9
|
+
# imported
|
10
|
+
# * invalid_object - detected object could not have been imported because of
|
11
|
+
# validation errors
|
12
|
+
# * +object+ - pointer to actual object created or updated during import
|
13
|
+
# * +data+ - detected object's attributes hash
|
14
|
+
# * +validation_errors+ - list of validation errors for invalid object
|
15
|
+
#
|
16
|
+
# Instances of this class are built via Import's +build_imported_object+ method.
|
17
|
+
#
|
18
|
+
# If you need you can implement your own version of ImportedObject class
|
19
|
+
# (f.e. activerecord-based). To use it you must also implement custom version of
|
20
|
+
# +Import+ class that builds instances of CustomImportedObject with it's
|
21
|
+
# +build_imported_object+ method.
|
22
|
+
class ImportedObject
|
23
|
+
attr_accessor :state, :object, :data, :validation_errors
|
3
24
|
|
25
|
+
def initialize(attributes = {})
|
26
|
+
@state = attributes[:state]
|
27
|
+
@object = attributes[:object]
|
28
|
+
@data = attributes[:data]
|
29
|
+
@validation_errors = attributes[:validation_errors]
|
30
|
+
end
|
4
31
|
end
|
5
|
-
end
|
32
|
+
end
|
data/lib/importer/parser/base.rb
CHANGED
@@ -2,6 +2,10 @@ module Importer
|
|
2
2
|
module Parser
|
3
3
|
# Extend this class if you want to provide a custom parser.
|
4
4
|
# You only need to implement +run+ instance method in subclasses.
|
5
|
+
#
|
6
|
+
# To force the importer to use your custom parser use:
|
7
|
+
#
|
8
|
+
# Product.import(file, :parser => CustomParserClass)
|
5
9
|
class Base
|
6
10
|
|
7
11
|
class << self
|
data/lib/importer/parser/csv.rb
CHANGED
data/lib/importer/parser/xml.rb
CHANGED
data/test/factories.rb
CHANGED
@@ -1,11 +1,2 @@
|
|
1
|
-
Factory.define :active_record_import, :class => Importer::Import::ActiveRecord do |f|
|
2
|
-
end
|
3
|
-
|
4
|
-
Factory.define :active_record_imported_object, :class => Importer::ImportedObject::ActiveRecord do |f|
|
5
|
-
end
|
6
|
-
|
7
|
-
Factory.define :simple_import, :class => Importer::Import::Simple do |f|
|
8
|
-
end
|
9
|
-
|
10
1
|
Factory.define :product do |f|
|
11
2
|
end
|
data/test/helper.rb
CHANGED
@@ -1,46 +1,23 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'test/unit'
|
3
3
|
require 'shoulda'
|
4
|
+
require 'rr'
|
5
|
+
require 'active_record'
|
6
|
+
require 'mongo_mapper'
|
4
7
|
|
5
8
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
6
9
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
10
|
+
|
7
11
|
require 'importer'
|
8
12
|
|
9
13
|
config = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml')))
|
10
14
|
ActiveRecord::Base.establish_connection(config['test'])
|
11
15
|
|
12
|
-
|
13
|
-
|
16
|
+
MongoMapper.connection = Mongo::Connection.new('127.0.0.1', 27017)
|
17
|
+
MongoMapper.database = 'importer-test'
|
14
18
|
|
15
19
|
class Test::Unit::TestCase
|
16
|
-
|
17
|
-
reset_tables
|
18
|
-
end
|
19
|
-
|
20
|
-
def reset_tables
|
21
|
-
ActiveRecord::Base.connection.create_table :products, { :force => true } do |t|
|
22
|
-
t.string :customid
|
23
|
-
t.string :name
|
24
|
-
t.string :description
|
25
|
-
t.decimal :price
|
26
|
-
end
|
27
|
-
|
28
|
-
ActiveRecord::Base.connection.create_table :imports, { :force => true } do |t|
|
29
|
-
t.integer :new_objects_count, :default => 0
|
30
|
-
t.integer :existing_objects_count, :default => 0
|
31
|
-
t.integer :invalid_objects_count, :default => 0
|
32
|
-
t.string :workflow_state, :default => "ready"
|
33
|
-
end
|
34
|
-
|
35
|
-
ActiveRecord::Base.connection.create_table :imported_objects, { :force => true } do |t|
|
36
|
-
t.integer :import_id
|
37
|
-
t.string :object_type
|
38
|
-
t.integer :object_id
|
39
|
-
t.string :data
|
40
|
-
t.string :validation_errors
|
41
|
-
t.string :state
|
42
|
-
end
|
43
|
-
end
|
20
|
+
include RR::Adapters::TestUnit
|
44
21
|
|
45
22
|
# return path to a fixture file from test/fixtures dir
|
46
23
|
def fixture_file(file)
|
@@ -48,24 +25,13 @@ class Test::Unit::TestCase
|
|
48
25
|
end
|
49
26
|
end
|
50
27
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
def self.find_on_import(import, attributes)
|
57
|
-
find_by_customid(attributes["customid"])
|
58
|
-
end
|
28
|
+
def def_class(class_name, parent_class = Object, &blk)
|
29
|
+
undef_class(class_name)
|
30
|
+
klass = Object.const_set(class_name, Class.new(parent_class))
|
31
|
+
klass.module_eval(&blk)
|
32
|
+
klass
|
59
33
|
end
|
60
34
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
def self.find_on_import(import, attributes)
|
65
|
-
if attributes["customid"] == "3"
|
66
|
-
raise ::Exception.new("An error occured.")
|
67
|
-
else
|
68
|
-
super
|
69
|
-
end
|
70
|
-
end
|
35
|
+
def undef_class(class_name)
|
36
|
+
Object.send(:remove_const, class_name) rescue nil
|
71
37
|
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class Importer::Adapters::ActiveRecordAdapterTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
ActiveRecord::Base.connection.create_table :products, { :force => true } do |t|
|
6
|
+
t.string :customid
|
7
|
+
t.string :name
|
8
|
+
t.string :description
|
9
|
+
t.decimal :price
|
10
|
+
end
|
11
|
+
|
12
|
+
def_class("Product", ActiveRecord::Base) do
|
13
|
+
include Importer
|
14
|
+
|
15
|
+
validates_numericality_of :price
|
16
|
+
|
17
|
+
def self.find_on_import(import, attributes)
|
18
|
+
find_by_customid(attributes["customid"])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def teardown
|
24
|
+
undef_class("Product")
|
25
|
+
end
|
26
|
+
|
27
|
+
context "" do
|
28
|
+
setup do
|
29
|
+
@product = Product.create(:customid => "1", :name => "A pink ball", :description => "Round glass ball.", :price => 86)
|
30
|
+
end
|
31
|
+
|
32
|
+
context "importing objects" do
|
33
|
+
setup do
|
34
|
+
@new_object = { "name" => "A red hat", "customid"=>"2", "price" => "114.00", "description" => "Party hat." }
|
35
|
+
@existing_object = { "name" => "A black ball", "customid"=>"1", "price" => "86.00", "description" => "Round glass ball." }
|
36
|
+
@invalid_object = { "name" => "A white ribbon", "customid"=>"3", "price" => "oops", "description" => "A really long one." }
|
37
|
+
|
38
|
+
data = [ @new_object, @existing_object, @invalid_object ]
|
39
|
+
|
40
|
+
stub(Importer::Parser::Xml).run(fixture_file("products.xml")) { data }
|
41
|
+
|
42
|
+
@import = Product.import(fixture_file("products.xml"))
|
43
|
+
end
|
44
|
+
|
45
|
+
should_change("product's name", :from => "A pink ball", :to => "A black ball") { @product.reload.name }
|
46
|
+
|
47
|
+
should_change("products count", :by => 1) { Product.count }
|
48
|
+
should "correctly create new product" do
|
49
|
+
product = Product.last
|
50
|
+
|
51
|
+
assert_equal "A red hat", product.name
|
52
|
+
assert_equal "Party hat.", product.description
|
53
|
+
assert_equal 114, product.price
|
54
|
+
assert_equal "2", product.customid
|
55
|
+
end
|
56
|
+
|
57
|
+
should "correctly summarize the import process" do
|
58
|
+
assert_equal 1, @import.new_imported_objects.size
|
59
|
+
assert_equal 1, @import.existing_imported_objects.size
|
60
|
+
assert_equal 1, @import.invalid_imported_objects.size
|
61
|
+
end
|
62
|
+
|
63
|
+
should "correctly build imported objects" do
|
64
|
+
new_object = @import.new_imported_objects.first
|
65
|
+
existing_object = @import.existing_imported_objects.first
|
66
|
+
invalid_object = @import.invalid_imported_objects.first
|
67
|
+
|
68
|
+
assert_equal @new_object, new_object.data
|
69
|
+
assert_equal 'new_object', new_object.state
|
70
|
+
assert_equal Product.last, new_object.object
|
71
|
+
|
72
|
+
assert_equal @existing_object, existing_object.data
|
73
|
+
assert_equal 'existing_object', existing_object.state
|
74
|
+
assert_equal @product, existing_object.object
|
75
|
+
|
76
|
+
assert_equal @invalid_object, invalid_object.data
|
77
|
+
assert_equal 'invalid_object', invalid_object.state
|
78
|
+
assert_equal ["Price is not a number"], invalid_object.validation_errors
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "when there is exception during import process" do
|
83
|
+
setup do
|
84
|
+
def_class("InvalidProduct", Product) do
|
85
|
+
set_table_name "products"
|
86
|
+
|
87
|
+
def self.find_on_import(import, attributes)
|
88
|
+
if attributes["customid"] == "3"
|
89
|
+
raise ::Exception.new("An error occured.")
|
90
|
+
else
|
91
|
+
super
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
begin
|
97
|
+
InvalidProduct.import(fixture_file("products.xml"))
|
98
|
+
rescue ::Exception => e
|
99
|
+
@exception = e
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def teardown
|
104
|
+
undef_class("InvalidProduct")
|
105
|
+
end
|
106
|
+
|
107
|
+
should_not_change("product's name") { @product.reload.name }
|
108
|
+
should_not_change("products count") { InvalidProduct.count }
|
109
|
+
should "propagate exception" do
|
110
|
+
assert_equal "An error occured.", @exception.message
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class Importer::Adapters::MongoMapperAdapterTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
def_class("Product") do
|
6
|
+
include MongoMapper::Document
|
7
|
+
include Importer
|
8
|
+
|
9
|
+
key :customid, String
|
10
|
+
key :name, String
|
11
|
+
key :description, String
|
12
|
+
key :price, Float
|
13
|
+
|
14
|
+
validates_numericality_of :price
|
15
|
+
|
16
|
+
def self.find_on_import(import, attributes)
|
17
|
+
find_by_customid(attributes["customid"])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def teardown
|
23
|
+
MongoMapper.database.drop_collection("products")
|
24
|
+
undef_class("Product")
|
25
|
+
end
|
26
|
+
|
27
|
+
context "" do
|
28
|
+
setup do
|
29
|
+
@product = Product.create(:customid => "1", :name => "A pink ball", :description => "Round glass ball.", :price => 86)
|
30
|
+
end
|
31
|
+
|
32
|
+
context "importing objects" do
|
33
|
+
setup do
|
34
|
+
@new_object = { "name" => "A red hat", "customid"=>"2", "price" => "114.00", "description" => "Party hat." }
|
35
|
+
@existing_object = { "name" => "A black ball", "customid"=>"1", "price" => "86.00", "description" => "Round glass ball." }
|
36
|
+
@invalid_object = { "name" => "A white ribbon", "customid"=>"3", "price" => "oops", "description" => "A really long one." }
|
37
|
+
|
38
|
+
data = [ @new_object, @existing_object, @invalid_object ]
|
39
|
+
|
40
|
+
stub(Importer::Parser::Xml).run(fixture_file("products.xml")) { data }
|
41
|
+
|
42
|
+
@import = Product.import(fixture_file("products.xml"))
|
43
|
+
end
|
44
|
+
|
45
|
+
should_change("product's name", :from => "A pink ball", :to => "A black ball") { @product.reload.name }
|
46
|
+
|
47
|
+
should_change("products count", :by => 1) { Product.count }
|
48
|
+
should "correctly create new product" do
|
49
|
+
product = Product.find_by_customid("2")
|
50
|
+
|
51
|
+
assert_equal "A red hat", product.name
|
52
|
+
assert_equal "Party hat.", product.description
|
53
|
+
assert_equal 114, product.price
|
54
|
+
assert_equal "2", product.customid
|
55
|
+
end
|
56
|
+
|
57
|
+
should "correctly summarize the import process" do
|
58
|
+
assert_equal 1, @import.new_imported_objects.size
|
59
|
+
assert_equal 1, @import.existing_imported_objects.size
|
60
|
+
assert_equal 1, @import.invalid_imported_objects.size
|
61
|
+
end
|
62
|
+
|
63
|
+
should "correctly build imported objects" do
|
64
|
+
new_object = @import.new_imported_objects.first
|
65
|
+
existing_object = @import.existing_imported_objects.first
|
66
|
+
invalid_object = @import.invalid_imported_objects.first
|
67
|
+
|
68
|
+
assert_equal @new_object, new_object.data
|
69
|
+
assert_equal 'new_object', new_object.state
|
70
|
+
assert_equal Product.find_by_customid("2"), new_object.object
|
71
|
+
|
72
|
+
assert_equal @existing_object, existing_object.data
|
73
|
+
assert_equal 'existing_object', existing_object.state
|
74
|
+
assert_equal @product, existing_object.object
|
75
|
+
|
76
|
+
assert_equal @invalid_object, invalid_object.data
|
77
|
+
assert_equal 'invalid_object', invalid_object.state
|
78
|
+
assert_equal ["Price must be a number"], invalid_object.validation_errors
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "when there is exception during import process" do
|
83
|
+
setup do
|
84
|
+
def_class("InvalidProduct", Product) do
|
85
|
+
include MongoMapper::Document
|
86
|
+
include Importer
|
87
|
+
|
88
|
+
set_collection_name "products"
|
89
|
+
|
90
|
+
def self.find_on_import(import, attributes)
|
91
|
+
if attributes["customid"] == "3"
|
92
|
+
raise ::Exception.new("An error occured.")
|
93
|
+
else
|
94
|
+
super
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
begin
|
100
|
+
InvalidProduct.import(fixture_file("products.xml"))
|
101
|
+
rescue ::Exception => e
|
102
|
+
@exception = e
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def teardown
|
107
|
+
undef_class("InvalidProduct")
|
108
|
+
end
|
109
|
+
|
110
|
+
should "propagate exception" do
|
111
|
+
assert_equal "An error occured.", @exception.message
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class Importer::ImportTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@import = Importer::Import.new
|
6
|
+
end
|
7
|
+
|
8
|
+
context "when sent #build_imported_object" do
|
9
|
+
setup do
|
10
|
+
@imported_object = @import.build_imported_object
|
11
|
+
end
|
12
|
+
|
13
|
+
should "correctly return instance of Importer::ImportedObject" do
|
14
|
+
assert @imported_object.is_a?(Importer::ImportedObject)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "import with some imported_object" do
|
19
|
+
setup do
|
20
|
+
@new_object = Importer::ImportedObject.new(:state => 'new_object')
|
21
|
+
@existing_object = Importer::ImportedObject.new(:state => 'existing_object')
|
22
|
+
@invalid_object = Importer::ImportedObject.new(:state => 'invalid_object')
|
23
|
+
|
24
|
+
@import.add_object(@new_object)
|
25
|
+
@import.add_object(@existing_object)
|
26
|
+
@import.add_object(@invalid_object)
|
27
|
+
end
|
28
|
+
|
29
|
+
should "correctly return all imported objects" do
|
30
|
+
assert_same_elements [@new_object, @existing_object, @invalid_object], @import.imported_objects
|
31
|
+
end
|
32
|
+
|
33
|
+
should "correctly return new_imported_objects" do
|
34
|
+
assert_same_elements [@new_object], @import.new_imported_objects
|
35
|
+
end
|
36
|
+
|
37
|
+
should "correctly return existing_imported_objects" do
|
38
|
+
assert_same_elements [@existing_object], @import.existing_imported_objects
|
39
|
+
end
|
40
|
+
|
41
|
+
should "correctly return invalid_imported_objects" do
|
42
|
+
assert_same_elements [@invalid_object], @import.invalid_imported_objects
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|