importer 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -12,6 +12,7 @@ Docs: http://rdoc.info/projects/szajbus/importer
12
12
 
13
13
  * can import from XML and CSV formats, but it's possible to add custom parsers
14
14
  * reports how many new objects got imported, how many objects was modified and how many objects were invalid
15
+ * includes ActiveRecord, DataMapper and MongoMapper adapters
15
16
 
16
17
  == Installation
17
18
 
@@ -36,6 +37,12 @@ Add to your model
36
37
  include Importer
37
38
  end
38
39
 
40
+ # DataMapper
41
+ class Product
42
+ include DataMapper::Resource
43
+ include Importer
44
+ end
45
+
39
46
  And start importing
40
47
 
41
48
  Product.import(path_to_xml_or_csv_file)
data/Rakefile CHANGED
@@ -14,6 +14,10 @@ begin
14
14
  gem.add_dependency "fastercsv", ">= 1.5.0"
15
15
  gem.add_development_dependency "activerecord", ">= 0"
16
16
  gem.add_development_dependency "mongo_mapper", ">= 0.7.0"
17
+ gem.add_development_dependency "dm-core", ">= 0.10.2"
18
+ gem.add_development_dependency "dm-validations", ">= 0.10.2"
19
+ gem.add_development_dependency "dm-aggregates", ">= 0.10.2"
20
+ gem.add_development_dependency "do_sqlite3", ">= 0.10.1.1"
17
21
  gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
18
22
  gem.add_development_dependency 'sqlite3-ruby'
19
23
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.1
1
+ 0.4.2
data/importer.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{importer}
8
- s.version = "0.4.1"
8
+ s.version = "0.4.2"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Micha\305\202 Szajbe"]
12
- s.date = %q{2010-04-16}
12
+ s.date = %q{2010-05-17}
13
13
  s.description = %q{Define new objects or modifications of existing ones in external file (xml, csv, etc) and import them to your application. Importer will not only import all the objects but also will give you detailed summary of the import process.}
14
14
  s.email = %q{michal.szajbe@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -26,6 +26,7 @@ Gem::Specification.new do |s|
26
26
  "importer.gemspec",
27
27
  "lib/importer.rb",
28
28
  "lib/importer/adapters/active_record_adapter.rb",
29
+ "lib/importer/adapters/data_mapper_adapter.rb",
29
30
  "lib/importer/adapters/mongo_mapper_adapter.rb",
30
31
  "lib/importer/import.rb",
31
32
  "lib/importer/imported_object.rb",
@@ -44,6 +45,7 @@ Gem::Specification.new do |s|
44
45
  "test/fixtures/products.xml",
45
46
  "test/helper.rb",
46
47
  "test/importer/adapters/active_record_adapter_test.rb",
48
+ "test/importer/adapters/data_mapper_adapter_test.rb",
47
49
  "test/importer/adapters/mongo_mapper_adapter_test.rb",
48
50
  "test/importer/import_test.rb",
49
51
  "test/importer/imported_object_test.rb",
@@ -60,6 +62,7 @@ Gem::Specification.new do |s|
60
62
  "test/factories.rb",
61
63
  "test/helper.rb",
62
64
  "test/importer/adapters/active_record_adapter_test.rb",
65
+ "test/importer/adapters/data_mapper_adapter_test.rb",
63
66
  "test/importer/adapters/mongo_mapper_adapter_test.rb",
64
67
  "test/importer/import_test.rb",
65
68
  "test/importer/imported_object_test.rb",
@@ -77,6 +80,10 @@ Gem::Specification.new do |s|
77
80
  s.add_runtime_dependency(%q<fastercsv>, [">= 1.5.0"])
78
81
  s.add_development_dependency(%q<activerecord>, [">= 0"])
79
82
  s.add_development_dependency(%q<mongo_mapper>, [">= 0.7.0"])
83
+ s.add_development_dependency(%q<dm-core>, [">= 0.10.2"])
84
+ s.add_development_dependency(%q<dm-validations>, [">= 0.10.2"])
85
+ s.add_development_dependency(%q<dm-aggregates>, [">= 0.10.2"])
86
+ s.add_development_dependency(%q<do_sqlite3>, [">= 0.10.1.1"])
80
87
  s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
81
88
  s.add_development_dependency(%q<sqlite3-ruby>, [">= 0"])
82
89
  else
@@ -84,6 +91,10 @@ Gem::Specification.new do |s|
84
91
  s.add_dependency(%q<fastercsv>, [">= 1.5.0"])
85
92
  s.add_dependency(%q<activerecord>, [">= 0"])
86
93
  s.add_dependency(%q<mongo_mapper>, [">= 0.7.0"])
94
+ s.add_dependency(%q<dm-core>, [">= 0.10.2"])
95
+ s.add_dependency(%q<dm-validations>, [">= 0.10.2"])
96
+ s.add_dependency(%q<dm-aggregates>, [">= 0.10.2"])
97
+ s.add_dependency(%q<do_sqlite3>, [">= 0.10.1.1"])
87
98
  s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
88
99
  s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
89
100
  end
@@ -92,6 +103,10 @@ Gem::Specification.new do |s|
92
103
  s.add_dependency(%q<fastercsv>, [">= 1.5.0"])
93
104
  s.add_dependency(%q<activerecord>, [">= 0"])
94
105
  s.add_dependency(%q<mongo_mapper>, [">= 0.7.0"])
106
+ s.add_dependency(%q<dm-core>, [">= 0.10.2"])
107
+ s.add_dependency(%q<dm-validations>, [">= 0.10.2"])
108
+ s.add_dependency(%q<dm-aggregates>, [">= 0.10.2"])
109
+ s.add_dependency(%q<do_sqlite3>, [">= 0.10.1.1"])
95
110
  s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
96
111
  s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
97
112
  end
data/lib/importer.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'importer/adapters/active_record_adapter'
2
+ require 'importer/adapters/data_mapper_adapter'
2
3
  require 'importer/adapters/mongo_mapper_adapter'
3
4
  require 'importer/import'
4
5
  require 'importer/imported_object'
@@ -32,6 +33,8 @@ module Importer
32
33
  base.send(:include, Importer::Adapters::ActiveRecordAdapter)
33
34
  elsif defined?(MongoMapper) && (base.include?(MongoMapper::Document) || base.include?(MongoMapper::EmbeddedDocument))
34
35
  base.send(:include, Importer::Adapters::MongoMapperAdapter)
36
+ elsif defined?(DataMapper) && (base.include?(DataMapper::Resource))
37
+ base.send(:include, Importer::Adapters::DataMapperAdapter)
35
38
  else
36
39
  raise AdapterError.new("Can't determine adapter for #{base.class} class.")
37
40
  end
@@ -64,7 +64,7 @@ module Importer
64
64
 
65
65
  unless object.save
66
66
  imported_object.state = "invalid_object"
67
- imported_object.validation_errors = object.errors.full_messages
67
+ imported_object.validation_errors = object.errors.full_messages.uniq
68
68
  end
69
69
 
70
70
  imported_object.object = object
@@ -0,0 +1,104 @@
1
+ module Importer
2
+ module Adapters
3
+ # Adapter for DataMapper models
4
+ #
5
+ # Usage:
6
+ #
7
+ # class Product
8
+ # include DataMapper::Resource
9
+ # include Importer
10
+ # end
11
+ #
12
+ # Product.import(path_to_xml_or_csv_file)
13
+ #
14
+ # It sends the given file to a parser and then imports detected objects.
15
+ # Instead of simply inserting all detected objects to database, the importer
16
+ # tries to determine wheter a detected object already exists. If so, the object
17
+ # is only updated, otherwise a new object is created.
18
+ #
19
+ # To change the way how importer checks for existing objects (or to turn off this
20
+ # behavior completely) override +find_on_import+ method. The default behavior now
21
+ # is to try to find existing object by detected object's id.
22
+ #
23
+ # By default the detected object's attributes hash is literally assigned to a
24
+ # soon-to-be-saved object. If there is a need for more sophisticated behavior,
25
+ # simply override +merge_attributes_on_import+ method.
26
+ module DataMapperAdapter
27
+
28
+ class << self
29
+ def included(base)
30
+ base.send(:include, InstanceMethods)
31
+ base.send(:extend, ClassMethods)
32
+ end
33
+ end
34
+
35
+ module ClassMethods
36
+ # The import process is wrapped in a transaction, so if anything goes wrong there is no
37
+ # harm done.
38
+ # * +file+ - path to an XML or CVS file, you can also import from other data formats,
39
+ # but you also need to provide a custom parser to read it
40
+ # Possible options:
41
+ # * +parser+ - by default the parser is determined from file extension, but you can force
42
+ # the imported to use another one by passing it's class here
43
+ # * +import+ - by default importer returns instance of +Import+ class that contains
44
+ # detailed report of import process, you can implement your own Import class and force
45
+ # the importer to use it by passing it's class here
46
+ # * +import_options+ - options passed to Import instance on it's initialization
47
+ def import(file, options = {})
48
+ import = (options[:import] || Importer::Import).new(options[:import_options])
49
+ parser = options[:parser] || Importer::Parser.get_klass(file)
50
+ data = parser.run(file)
51
+
52
+ transaction do
53
+ data.each do |attributes|
54
+ imported_object = import.build_imported_object
55
+
56
+ if object = find_on_import(import, attributes)
57
+ imported_object.state = "existing_object"
58
+ else
59
+ object = new
60
+ imported_object.state = "new_object"
61
+ end
62
+
63
+ imported_object.data = attributes
64
+ object.merge_attributes_on_import(import, attributes)
65
+
66
+ unless object.save
67
+ imported_object.state = "invalid_object"
68
+ imported_object.validation_errors = object.errors.full_messages.uniq
69
+ end
70
+
71
+ imported_object.object = object
72
+
73
+ import.add_object(imported_object)
74
+ end
75
+ end
76
+
77
+ import
78
+ end
79
+
80
+ # Determines whether a detected object already exists in database.
81
+ # By default it tries to find an existing objects by id of the detected one.
82
+ # Returns the object or nil if it's not found.
83
+ # Override this method in your model to change that default behavior.
84
+ # * +import+ - current import
85
+ # * +attributes+ - detected object's attributes hash
86
+ def find_on_import(import, attributes)
87
+ get(attributes["id"])
88
+ end
89
+ end
90
+
91
+ module InstanceMethods
92
+ # Merges attributes of a detected object with current object's ones.
93
+ # By default it simply assigns detected attributes to the object.
94
+ # Override this method in your model to provide some more sophisticated
95
+ # behavior.
96
+ # * +import+ - current import
97
+ # * +attributes+ - detected object's attributes hash
98
+ def merge_attributes_on_import(import, attributes)
99
+ self.attributes = attributes
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -67,7 +67,7 @@ module Importer
67
67
 
68
68
  unless object.save
69
69
  imported_object.state = "invalid_object"
70
- imported_object.validation_errors = object.errors.full_messages
70
+ imported_object.validation_errors = object.errors.full_messages.uniq
71
71
  end
72
72
 
73
73
  imported_object.object = object
data/test/helper.rb CHANGED
@@ -4,6 +4,9 @@ require 'shoulda'
4
4
  require 'rr'
5
5
  require 'active_record'
6
6
  require 'mongo_mapper'
7
+ require 'dm-core'
8
+ require 'dm-validations'
9
+ require 'dm-aggregates'
7
10
 
8
11
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
12
  $LOAD_PATH.unshift(File.dirname(__FILE__))
@@ -12,6 +15,7 @@ require 'importer'
12
15
 
13
16
  config = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml')))
14
17
  ActiveRecord::Base.establish_connection(config['test'])
18
+ DataMapper.setup(:default, config['test'])
15
19
 
16
20
  MongoMapper.connection = Mongo::Connection.new('127.0.0.1', 27017)
17
21
  MongoMapper.database = 'importer-test'
@@ -0,0 +1,129 @@
1
+ require 'helper'
2
+
3
+ class Importer::Adapters::DataMapperAdapterTest < Test::Unit::TestCase
4
+ def setup
5
+ def_class("Product") do
6
+ include DataMapper::Resource
7
+ include Importer
8
+
9
+ property :id, DataMapper::Types::Serial
10
+ property :customid, String
11
+ property :name, String
12
+ property :description, String
13
+ property :price, Float
14
+
15
+ validates_is_number :price
16
+
17
+ def self.find_on_import(import, attributes)
18
+ first(:customid => attributes["customid"])
19
+ end
20
+ end
21
+
22
+ DataMapper.auto_migrate!
23
+ end
24
+
25
+ def teardown
26
+ undef_class("Product")
27
+ end
28
+
29
+ context "" do
30
+ setup do
31
+ @product = Product.create(:customid => "1", :name => "A pink ball", :description => "Round glass ball.", :price => 86)
32
+ end
33
+
34
+ context "importing objects" do
35
+ setup do
36
+ @new_object = { "name" => "A red hat", "customid"=>"2", "price" => "114.00", "description" => "Party hat." }
37
+ @existing_object = { "name" => "A black ball", "customid"=>"1", "price" => "86.00", "description" => "Round glass ball." }
38
+ @invalid_object = { "name" => "A white ribbon", "customid"=>"3", "price" => "oops", "description" => "A really long one." }
39
+
40
+ data = [ @new_object, @existing_object, @invalid_object ]
41
+
42
+ stub(Importer::Parser::Xml).run(fixture_file("products.xml")) { data }
43
+
44
+ @import = Product.import(fixture_file("products.xml"))
45
+ end
46
+
47
+ should_change("product's name", :from => "A pink ball", :to => "A black ball") { @product.reload.name }
48
+
49
+ should_change("products count", :by => 1) { Product.count }
50
+ should "correctly create new product" do
51
+ product = Product.last
52
+
53
+ assert_equal "A red hat", product.name
54
+ assert_equal "Party hat.", product.description
55
+ assert_equal 114, product.price
56
+ assert_equal "2", product.customid
57
+ end
58
+
59
+ should "correctly summarize the import process" do
60
+ assert_equal 1, @import.new_imported_objects.size
61
+ assert_equal 1, @import.existing_imported_objects.size
62
+ assert_equal 1, @import.invalid_imported_objects.size
63
+ end
64
+
65
+ should "correctly build imported objects" do
66
+ new_object = @import.new_imported_objects.first
67
+ existing_object = @import.existing_imported_objects.first
68
+ invalid_object = @import.invalid_imported_objects.first
69
+
70
+ assert_equal @new_object, new_object.data
71
+ assert_equal 'new_object', new_object.state
72
+ assert_equal Product.last, new_object.object
73
+
74
+ assert_equal @existing_object, existing_object.data
75
+ assert_equal 'existing_object', existing_object.state
76
+ assert_equal @product, existing_object.object
77
+
78
+ assert_equal @invalid_object, invalid_object.data
79
+ assert_equal 'invalid_object', invalid_object.state
80
+ assert_equal ["Price must be a number"], invalid_object.validation_errors
81
+ end
82
+ end
83
+
84
+ context "when there is exception during import process" do
85
+ setup do
86
+ def_class("InvalidProduct", Product) do
87
+ storage_names[:default] = "products"
88
+
89
+ def self.find_on_import(import, attributes)
90
+ if attributes["customid"] == "3"
91
+ raise ::Exception.new("An error occured.")
92
+ else
93
+ super
94
+ end
95
+ end
96
+ end
97
+
98
+ begin
99
+ InvalidProduct.import(fixture_file("products.xml"))
100
+ rescue ::Exception => e
101
+ @exception = e
102
+ end
103
+ end
104
+
105
+ def teardown
106
+ undef_class("InvalidProduct")
107
+ end
108
+
109
+ should_not_change("product's name") { @product.reload.name }
110
+ should_not_change("products count") { InvalidProduct.count }
111
+ should "propagate exception" do
112
+ assert_equal "An error occured.", @exception.message
113
+ end
114
+ end
115
+ end
116
+
117
+ context "passing import_options to #import method" do
118
+ setup do
119
+ stub(Importer::Parser::Xml).run(fixture_file("empty.xml")) { [] }
120
+
121
+ @options = { :key => 'value' }
122
+ @import = Product.import(fixture_file("empty.xml"), :import_options => @options)
123
+ end
124
+
125
+ should "pass the options to Import instance" do
126
+ assert_equal @options, @import.options
127
+ end
128
+ end
129
+ end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 4
8
- - 1
9
- version: 0.4.1
8
+ - 2
9
+ version: 0.4.2
10
10
  platform: ruby
11
11
  authors:
12
12
  - "Micha\xC5\x82 Szajbe"
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-04-16 00:00:00 +02:00
17
+ date: 2010-05-17 00:00:00 +02:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -72,7 +72,7 @@ dependencies:
72
72
  type: :development
73
73
  version_requirements: *id004
74
74
  - !ruby/object:Gem::Dependency
75
- name: thoughtbot-shoulda
75
+ name: dm-core
76
76
  prerelease: false
77
77
  requirement: &id005 !ruby/object:Gem::Requirement
78
78
  requirements:
@@ -80,11 +80,13 @@ dependencies:
80
80
  - !ruby/object:Gem::Version
81
81
  segments:
82
82
  - 0
83
- version: "0"
83
+ - 10
84
+ - 2
85
+ version: 0.10.2
84
86
  type: :development
85
87
  version_requirements: *id005
86
88
  - !ruby/object:Gem::Dependency
87
- name: sqlite3-ruby
89
+ name: dm-validations
88
90
  prerelease: false
89
91
  requirement: &id006 !ruby/object:Gem::Requirement
90
92
  requirements:
@@ -92,9 +94,64 @@ dependencies:
92
94
  - !ruby/object:Gem::Version
93
95
  segments:
94
96
  - 0
95
- version: "0"
97
+ - 10
98
+ - 2
99
+ version: 0.10.2
96
100
  type: :development
97
101
  version_requirements: *id006
102
+ - !ruby/object:Gem::Dependency
103
+ name: dm-aggregates
104
+ prerelease: false
105
+ requirement: &id007 !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ segments:
110
+ - 0
111
+ - 10
112
+ - 2
113
+ version: 0.10.2
114
+ type: :development
115
+ version_requirements: *id007
116
+ - !ruby/object:Gem::Dependency
117
+ name: do_sqlite3
118
+ prerelease: false
119
+ requirement: &id008 !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ segments:
124
+ - 0
125
+ - 10
126
+ - 1
127
+ - 1
128
+ version: 0.10.1.1
129
+ type: :development
130
+ version_requirements: *id008
131
+ - !ruby/object:Gem::Dependency
132
+ name: thoughtbot-shoulda
133
+ prerelease: false
134
+ requirement: &id009 !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ segments:
139
+ - 0
140
+ version: "0"
141
+ type: :development
142
+ version_requirements: *id009
143
+ - !ruby/object:Gem::Dependency
144
+ name: sqlite3-ruby
145
+ prerelease: false
146
+ requirement: &id010 !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ segments:
151
+ - 0
152
+ version: "0"
153
+ type: :development
154
+ version_requirements: *id010
98
155
  description: Define new objects or modifications of existing ones in external file (xml, csv, etc) and import them to your application. Importer will not only import all the objects but also will give you detailed summary of the import process.
99
156
  email: michal.szajbe@gmail.com
100
157
  executables: []
@@ -114,6 +171,7 @@ files:
114
171
  - importer.gemspec
115
172
  - lib/importer.rb
116
173
  - lib/importer/adapters/active_record_adapter.rb
174
+ - lib/importer/adapters/data_mapper_adapter.rb
117
175
  - lib/importer/adapters/mongo_mapper_adapter.rb
118
176
  - lib/importer/import.rb
119
177
  - lib/importer/imported_object.rb
@@ -132,6 +190,7 @@ files:
132
190
  - test/fixtures/products.xml
133
191
  - test/helper.rb
134
192
  - test/importer/adapters/active_record_adapter_test.rb
193
+ - test/importer/adapters/data_mapper_adapter_test.rb
135
194
  - test/importer/adapters/mongo_mapper_adapter_test.rb
136
195
  - test/importer/import_test.rb
137
196
  - test/importer/imported_object_test.rb
@@ -172,6 +231,7 @@ test_files:
172
231
  - test/factories.rb
173
232
  - test/helper.rb
174
233
  - test/importer/adapters/active_record_adapter_test.rb
234
+ - test/importer/adapters/data_mapper_adapter_test.rb
175
235
  - test/importer/adapters/mongo_mapper_adapter_test.rb
176
236
  - test/importer/import_test.rb
177
237
  - test/importer/imported_object_test.rb