importer 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,7 +12,6 @@ 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
- * can save reports to the database, along with errors for later inspection
16
15
 
17
16
  == Installation
18
17
 
@@ -24,11 +23,6 @@ Or the plugin
24
23
 
25
24
  script/plugin install git://github.com/szajbus/importer.git
26
25
 
27
- Generate the migrations and run them (this step may not be necessary, read below)
28
-
29
- script/generate importer import
30
- rake db:migrate
31
-
32
26
  Add to your model
33
27
 
34
28
  class Product < ActiveRecord::Base
@@ -39,21 +33,17 @@ And start importing
39
33
 
40
34
  Product.import(path_to_xml_or_csv_file)
41
35
 
42
- This will parse the file and import all products there are defined in it. Import summary will be saved in imports table, it will tell you how many products were created, modified or invalid. Exact information about each product (detected attributes and errors) will be saved in imported_objects table.
43
-
44
- If you don't want to save summaries to database you can force the importer to do a simple import with:
45
-
46
- Product.import(path_to_xml_or_csv_file, :import => Importer::Import::Simple.create)
36
+ This will parse the file and import all products there are defined in it. This will return import summary which will tell you how many products were created, modified or invalid. Exact information about each product (detected attributes and errors) will be available in summary too.
47
37
 
48
38
  == Customization
49
39
 
50
40
  You can create your own parser to import from sources other than XML or CSV files. Check the implementation of one of existing parsers to find out how to write your own. Then just pass parser class to import method:
51
41
 
52
- Product.import(path_to_xml_or_csv_file, :parser => YourCustomParser)
42
+ Product.import(path_to_file, :parser => CustomParserClass)
53
43
 
54
- You can also create your custom Import and ImportedObject classes if you need the import summaries in other way than those currently offered (ActiveRecord which stores summaries in database and Simple which does not store them at all). Again check the current implementations to get the idea. To force the importer to use it:
44
+ You can also create your custom versions of Import and ImportedObject classes. A possible alternative version could be ActiveRecord Import and ImportedObject models that would save import summary to database for later inspection. Check the rdocs for these classes for more information. You force the importer to use a custom Import class with:
55
45
 
56
- Product.import(path_to_xml_or_csv_file, :import => YourCustomImport.new)
46
+ Product.import(path_to_file, :import => CustomImportClass)
57
47
 
58
48
  == Note on Patches/Pull Requests
59
49
 
@@ -65,6 +55,12 @@ You can also create your custom Import and ImportedObject classes if you need th
65
55
  (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
66
56
  * Send me a pull request. Bonus points for topic branches.
67
57
 
58
+ == Disclaimer
59
+
60
+ Importer gem/plugin was extracted from an actual Ruby on Rails application. It probably lacks some features or needs some polishing. Feel free to contribute.
61
+
62
+ The gem is still under development, backward compatibility can not be guaranteed (at least until it reaches 1.0 stable version).
63
+
68
64
  == Copyright
69
65
 
70
66
  Copyright (c) 2010 Michal Szajbe. See LICENSE for details.
data/Rakefile CHANGED
@@ -5,18 +5,16 @@ begin
5
5
  require 'jeweler'
6
6
  Jeweler::Tasks.new do |gem|
7
7
  gem.name = "importer"
8
- gem.summary = %Q{Import objects from XML files}
9
- gem.description = %Q{Define new objects or modifications of existing ones in XML file and import them to your application.}
8
+ gem.summary = %Q{Import objects from external files}
9
+ gem.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.}
10
10
  gem.email = "michal.szajbe@gmail.com"
11
11
  gem.homepage = "http://github.com/szajbus/importer"
12
12
  gem.authors = ["Michał Szajbe"]
13
- gem.add_dependency "crack", ">= 0"
14
- gem.add_dependency "fastercsv", ">= 0"
15
- gem.add_dependency "activerecord", ">= 0"
16
- gem.add_dependency "activesupport", ">= 0"
17
- gem.add_dependency "workflow", ">= 0"
13
+ gem.add_dependency "crack", ">= 0.1.6"
14
+ gem.add_dependency "fastercsv", ">= 1.5.0"
15
+ gem.add_development_dependency "activerecord", ">= 0"
16
+ gem.add_development_dependency "mongo_mapper", ">= 0.7.0"
18
17
  gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
19
- gem.add_development_dependency "factory_girl", ">= 0"
20
18
  gem.add_development_dependency 'sqlite3-ruby'
21
19
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
22
20
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.2
1
+ 0.4.0
@@ -5,12 +5,12 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{importer}
8
- s.version = "0.3.2"
8
+ s.version = "0.4.0"
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-03-25}
13
- s.description = %q{Define new objects or modifications of existing ones in XML file and import them to your application.}
12
+ s.date = %q{2010-04-07}
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 = [
16
16
  "LICENSE",
@@ -25,20 +25,15 @@ Gem::Specification.new do |s|
25
25
  "VERSION",
26
26
  "importer.gemspec",
27
27
  "lib/importer.rb",
28
+ "lib/importer/adapters/active_record_adapter.rb",
29
+ "lib/importer/adapters/mongo_mapper_adapter.rb",
28
30
  "lib/importer/import.rb",
29
- "lib/importer/import/active_record.rb",
30
- "lib/importer/import/simple.rb",
31
31
  "lib/importer/imported_object.rb",
32
- "lib/importer/imported_object/active_record.rb",
33
- "lib/importer/imported_object/simple.rb",
34
32
  "lib/importer/parser.rb",
35
33
  "lib/importer/parser/base.rb",
36
34
  "lib/importer/parser/csv.rb",
37
35
  "lib/importer/parser/xml.rb",
38
36
  "rails/init.rb",
39
- "rails_generators/importer/importer_generator.rb",
40
- "rails_generators/importer/templates/imported_objects_migration.rb",
41
- "rails_generators/importer/templates/imports_migration.rb",
42
37
  "test/database.yml",
43
38
  "test/factories.rb",
44
39
  "test/fixtures/empty.csv",
@@ -48,31 +43,29 @@ Gem::Specification.new do |s|
48
43
  "test/fixtures/products.csv",
49
44
  "test/fixtures/products.xml",
50
45
  "test/helper.rb",
51
- "test/importer/import/active_record_test.rb",
52
- "test/importer/import/simple_test.rb",
53
- "test/importer/imported_object/active_record_test.rb",
54
- "test/importer/imported_object/simple_test.rb",
46
+ "test/importer/adapters/active_record_adapter_test.rb",
47
+ "test/importer/adapters/mongo_mapper_adapter_test.rb",
48
+ "test/importer/import_test.rb",
49
+ "test/importer/imported_object_test.rb",
55
50
  "test/importer/parser/csv_test.rb",
56
51
  "test/importer/parser/xml_test.rb",
57
- "test/importer/parser_test.rb",
58
- "test/importer_test.rb"
52
+ "test/importer/parser_test.rb"
59
53
  ]
60
54
  s.homepage = %q{http://github.com/szajbus/importer}
61
55
  s.rdoc_options = ["--charset=UTF-8"]
62
56
  s.require_paths = ["lib"]
63
57
  s.rubygems_version = %q{1.3.5}
64
- s.summary = %q{Import objects from XML files}
58
+ s.summary = %q{Import objects from external files}
65
59
  s.test_files = [
66
60
  "test/factories.rb",
67
61
  "test/helper.rb",
68
- "test/importer/import/active_record_test.rb",
69
- "test/importer/import/simple_test.rb",
70
- "test/importer/imported_object/active_record_test.rb",
71
- "test/importer/imported_object/simple_test.rb",
62
+ "test/importer/adapters/active_record_adapter_test.rb",
63
+ "test/importer/adapters/mongo_mapper_adapter_test.rb",
64
+ "test/importer/import_test.rb",
65
+ "test/importer/imported_object_test.rb",
72
66
  "test/importer/parser/csv_test.rb",
73
67
  "test/importer/parser/xml_test.rb",
74
- "test/importer/parser_test.rb",
75
- "test/importer_test.rb"
68
+ "test/importer/parser_test.rb"
76
69
  ]
77
70
 
78
71
  if s.respond_to? :specification_version then
@@ -80,32 +73,26 @@ Gem::Specification.new do |s|
80
73
  s.specification_version = 3
81
74
 
82
75
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
83
- s.add_runtime_dependency(%q<crack>, [">= 0"])
84
- s.add_runtime_dependency(%q<fastercsv>, [">= 0"])
85
- s.add_runtime_dependency(%q<activerecord>, [">= 0"])
86
- s.add_runtime_dependency(%q<activesupport>, [">= 0"])
87
- s.add_runtime_dependency(%q<workflow>, [">= 0"])
76
+ s.add_runtime_dependency(%q<crack>, [">= 0.1.6"])
77
+ s.add_runtime_dependency(%q<fastercsv>, [">= 1.5.0"])
78
+ s.add_development_dependency(%q<activerecord>, [">= 0"])
79
+ s.add_development_dependency(%q<mongo_mapper>, [">= 0.7.0"])
88
80
  s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
89
- s.add_development_dependency(%q<factory_girl>, [">= 0"])
90
81
  s.add_development_dependency(%q<sqlite3-ruby>, [">= 0"])
91
82
  else
92
- s.add_dependency(%q<crack>, [">= 0"])
93
- s.add_dependency(%q<fastercsv>, [">= 0"])
83
+ s.add_dependency(%q<crack>, [">= 0.1.6"])
84
+ s.add_dependency(%q<fastercsv>, [">= 1.5.0"])
94
85
  s.add_dependency(%q<activerecord>, [">= 0"])
95
- s.add_dependency(%q<activesupport>, [">= 0"])
96
- s.add_dependency(%q<workflow>, [">= 0"])
86
+ s.add_dependency(%q<mongo_mapper>, [">= 0.7.0"])
97
87
  s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
98
- s.add_dependency(%q<factory_girl>, [">= 0"])
99
88
  s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
100
89
  end
101
90
  else
102
- s.add_dependency(%q<crack>, [">= 0"])
103
- s.add_dependency(%q<fastercsv>, [">= 0"])
91
+ s.add_dependency(%q<crack>, [">= 0.1.6"])
92
+ s.add_dependency(%q<fastercsv>, [">= 1.5.0"])
104
93
  s.add_dependency(%q<activerecord>, [">= 0"])
105
- s.add_dependency(%q<activesupport>, [">= 0"])
106
- s.add_dependency(%q<workflow>, [">= 0"])
94
+ s.add_dependency(%q<mongo_mapper>, [">= 0.7.0"])
107
95
  s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
108
- s.add_dependency(%q<factory_girl>, [">= 0"])
109
96
  s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
110
97
  end
111
98
  end
@@ -1,17 +1,14 @@
1
- require 'active_support'
2
-
1
+ require 'importer/adapters/active_record_adapter'
2
+ require 'importer/adapters/mongo_mapper_adapter'
3
3
  require 'importer/import'
4
4
  require 'importer/imported_object'
5
- require 'importer/import/active_record'
6
- require 'importer/import/simple'
7
- require 'importer/imported_object/active_record'
8
- require 'importer/imported_object/simple'
9
5
  require 'importer/parser'
10
6
  require 'importer/parser/base'
11
7
  require 'importer/parser/csv'
12
8
  require 'importer/parser/xml'
13
9
 
14
- # Importer is a tool to help you with importing objects to your database from various sources.
10
+ # Importer module provides your models with flexible API that makes it easier
11
+ # to import data from external sources.
15
12
  #
16
13
  # Usage:
17
14
  #
@@ -27,74 +24,18 @@ require 'importer/parser/xml'
27
24
  # Copyright:: Copyright (c) 2010 Michal Szajbe
28
25
  # License:: check the LICENCE file
29
26
  module Importer
27
+ class AdapterError < ::Exception; end
30
28
 
31
29
  class << self
32
30
  def included(base)
33
- base.send(:include, Importer::InstanceMethods)
34
- base.send(:extend, Importer::ClassMethods)
35
- end
36
- end
37
-
38
- module ClassMethods
39
- # The import process is wrapped in a transaction, so if anything goes wrong there is no
40
- # harm done.
41
- # * +file+ - path to an XML or CVS file, you can also import from other data formats,
42
- # but you also need to provide a custom parser to read it
43
- # Possible options:
44
- # * +parser+ - by default the parser is determined from file extension, but you can force
45
- # the imported to use another one by passing it's class here
46
- # * +import+ - by default importer tries to store import summary in database, so it uses
47
- # ActiveRecord import, to use other import type pass it's instance here
48
- def import(file, options = {})
49
- import = options[:import] || Importer::Import::ActiveRecord.create
50
- parser = options[:parser] || Importer::Parser.get_klass(file)
51
- data = parser.run(file)
52
-
53
- transaction do
54
- import.start!
55
-
56
- data.each do |attributes|
57
- imported_object = import.build_imported_object
58
-
59
- if object = find_on_import(import, attributes)
60
- imported_object.state = "existing_object"
61
- else
62
- object = new
63
- imported_object.state = "new_object"
64
- end
65
-
66
- imported_object.data = attributes
67
- object.merge_attributes_on_import(import, attributes)
68
-
69
- unless object.save
70
- imported_object.state = "invalid_object"
71
- imported_object.validation_errors = object.errors.full_messages
72
- end
73
-
74
- imported_object.object = object
75
- imported_object.save
76
- end
77
-
78
- import.finish!
31
+ if base.respond_to?(:descends_from_active_record?) && base.descends_from_active_record?
32
+ base.send(:include, Importer::Adapters::ActiveRecordAdapter)
33
+ elsif defined?(MongoMapper) && (base.include?(MongoMapper::Document) || base.include?(MongoMapper::EmbeddedDocument))
34
+ base.send(:include, Importer::Adapters::MongoMapperAdapter)
35
+ else
36
+ raise AdapterError.new("Can't determine adapter for #{base.class} class.")
79
37
  end
80
-
81
- import
82
- end
83
-
84
- # Overload +find_on_import+ method to find existing objects while importing an object.
85
- # This is used to determine wheter an imported should add a new object or just modify
86
- # an existing one. By default it searches records by id.
87
- def find_on_import(import, attributes)
88
- find_by_id(attributes["id"])
89
38
  end
90
39
  end
91
40
 
92
- module InstanceMethods
93
- # Overload +merge_attributes_on_import+ method if you need the detected attributes
94
- # merged to an object in some specific way. By default detected attributes are literally
95
- # assigned to an object.
96
- def merge_attributes_on_import(import, attributes)
97
- self.attributes = attributes
98
- end
99
- end
100
41
  end
@@ -0,0 +1,102 @@
1
+ module Importer
2
+ module Adapters
3
+ # Adapter for ActiveRecord models
4
+ #
5
+ # Usage:
6
+ #
7
+ # class Product < ActiveRecord::Base
8
+ # include Importer
9
+ # end
10
+ #
11
+ # Product.import(path_to_xml_or_csv_file)
12
+ #
13
+ # It sends the given file to a parser and then imports detected objects.
14
+ # Instead of simply inserting all detected objects to database, the importer
15
+ # tries to determine wheter a detected object already exists. If so, the object
16
+ # is only updated, otherwise a new object is created.
17
+ #
18
+ # To change the way how importer checks for existing objects (or to turn off this
19
+ # behavior completely) override +find_on_import+ method. The default behavior now
20
+ # is to try to find existing object by detected object's id.
21
+ #
22
+ # By default the detected object's attributes hash is literally assigned to a
23
+ # soon-to-be-saved object. If there is a need for more sophisticated behavior,
24
+ # simply override +merge_attributes_on_import+ method.
25
+ module ActiveRecordAdapter
26
+
27
+ class << self
28
+ def included(base)
29
+ base.send(:include, InstanceMethods)
30
+ base.send(:extend, ClassMethods)
31
+ end
32
+ end
33
+
34
+ module ClassMethods
35
+ # The import process is wrapped in a transaction, so if anything goes wrong there is no
36
+ # harm done.
37
+ # * +file+ - path to an XML or CVS file, you can also import from other data formats,
38
+ # but you also need to provide a custom parser to read it
39
+ # Possible options:
40
+ # * +parser+ - by default the parser is determined from file extension, but you can force
41
+ # the imported to use another one by passing it's class here
42
+ # * +import+ - by default importer returns instance of +Import+ class that contains
43
+ # detailed report of import process, you can implement your own Import class and force
44
+ # the importer to use it by passing it's class here
45
+ def import(file, options = {})
46
+ import = (options[:import] || Importer::Import).new
47
+ parser = options[:parser] || Importer::Parser.get_klass(file)
48
+ data = parser.run(file)
49
+
50
+ transaction do
51
+ data.each do |attributes|
52
+ imported_object = import.build_imported_object
53
+
54
+ if object = find_on_import(import, attributes)
55
+ imported_object.state = "existing_object"
56
+ else
57
+ object = new
58
+ imported_object.state = "new_object"
59
+ end
60
+
61
+ imported_object.data = attributes
62
+ object.merge_attributes_on_import(import, attributes)
63
+
64
+ unless object.save
65
+ imported_object.state = "invalid_object"
66
+ imported_object.validation_errors = object.errors.full_messages
67
+ end
68
+
69
+ imported_object.object = object
70
+
71
+ import.add_object(imported_object)
72
+ end
73
+ end
74
+
75
+ import
76
+ end
77
+
78
+ # Determines whether a detected object already exists in database.
79
+ # By default it tries to find an existing objects by id of the detected one.
80
+ # Returns the object or nil if it's not found.
81
+ # Override this method in your model to change that default behavior.
82
+ # * +import+ - current import
83
+ # * +attributes+ - detected object's attributes hash
84
+ def find_on_import(import, attributes)
85
+ find_by_id(attributes["id"])
86
+ end
87
+ end
88
+
89
+ module InstanceMethods
90
+ # Merges attributes of a detected object with current object's ones.
91
+ # By default it simply assigns detected attributes to the object.
92
+ # Override this method in your model to provide some more sophisticated
93
+ # behavior.
94
+ # * +import+ - current import
95
+ # * +attributes+ - detected object's attributes hash
96
+ def merge_attributes_on_import(import, attributes)
97
+ self.attributes = attributes
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,104 @@
1
+ module Importer
2
+ module Adapters
3
+ # Adapter for MongoMapper models
4
+ #
5
+ # Usage:
6
+ #
7
+ # class Product
8
+ # include MongoMapper::Document
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 MongoMapperAdapter
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
+ # Performs actual import
37
+ #
38
+ # Note: unlike with ActiveRecord adapter, import process is not wrapped in a transaction
39
+ # since mongodb does not support them.
40
+ #
41
+ # * +file+ - path to an XML or CVS file, you can also import from other data formats,
42
+ # but you also need to provide a custom parser to read it
43
+ # Possible options:
44
+ # * +parser+ - by default the parser is determined from file extension, but you can force
45
+ # the imported to use another one by passing it's class here
46
+ # * +import+ - by default importer returns instance of +Import+ class that contains
47
+ # detailed report of import process, you can implement your own Import class and force
48
+ # the importer to use it by passing it's class here
49
+ def import(file, options = {})
50
+ import = (options[:import] || Importer::Import).new
51
+ parser = options[:parser] || Importer::Parser.get_klass(file)
52
+ data = parser.run(file)
53
+
54
+ data.each do |attributes|
55
+ imported_object = import.build_imported_object
56
+
57
+ if object = find_on_import(import, attributes)
58
+ imported_object.state = "existing_object"
59
+ else
60
+ object = new
61
+ imported_object.state = "new_object"
62
+ end
63
+
64
+ imported_object.data = attributes
65
+ object.merge_attributes_on_import(import, attributes)
66
+
67
+ unless object.save
68
+ imported_object.state = "invalid_object"
69
+ imported_object.validation_errors = object.errors.full_messages
70
+ end
71
+
72
+ imported_object.object = object
73
+
74
+ import.add_object(imported_object)
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
+ find_by_id(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