importu 0.1.0 → 0.2.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 +7 -0
- data/.editorconfig +15 -0
- data/.github/workflows/ci.yml +48 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.rubocop.yml +311 -0
- data/.simplecov +14 -0
- data/.yardstick.yml +36 -0
- data/Appraisals +22 -0
- data/CHANGELOG.md +51 -0
- data/CONTRIBUTING.md +86 -0
- data/Gemfile +5 -1
- data/LICENSE +21 -0
- data/README.md +435 -52
- data/Rakefile +71 -0
- data/UPGRADING.md +188 -0
- data/gemfiles/rails_7_2.gemfile +11 -0
- data/gemfiles/rails_7_2.gemfile.lock +268 -0
- data/gemfiles/rails_8_0.gemfile +11 -0
- data/gemfiles/rails_8_0.gemfile.lock +271 -0
- data/gemfiles/rails_8_1.gemfile +11 -0
- data/gemfiles/rails_8_1.gemfile.lock +269 -0
- data/gemfiles/standalone.gemfile +8 -0
- data/gemfiles/standalone.gemfile.lock +197 -0
- data/importu.gemspec +41 -22
- data/lib/importu/backends/active_record.rb +171 -0
- data/lib/importu/backends/middleware/duplicate_manager_proxy.rb +41 -0
- data/lib/importu/backends/middleware/enforce_allowed_actions.rb +52 -0
- data/lib/importu/backends/middleware.rb +11 -0
- data/lib/importu/backends.rb +103 -0
- data/lib/importu/config_dsl.rb +381 -0
- data/lib/importu/converter_context.rb +94 -0
- data/lib/importu/converters.rb +119 -64
- data/lib/importu/definition.rb +23 -0
- data/lib/importu/duplicate_manager.rb +88 -0
- data/lib/importu/exceptions.rb +135 -4
- data/lib/importu/importer.rb +183 -96
- data/lib/importu/record.rb +138 -102
- data/lib/importu/sources/csv.rb +122 -0
- data/lib/importu/sources/json.rb +106 -0
- data/lib/importu/sources/ruby.rb +46 -0
- data/lib/importu/sources/xml.rb +133 -0
- data/lib/importu/sources.rb +13 -0
- data/lib/importu/summary.rb +277 -0
- data/lib/importu/version.rb +3 -1
- data/lib/importu.rb +45 -9
- data/spec/fixtures/books-duplicates/README.md +7 -0
- data/spec/fixtures/books-duplicates/infile.csv +7 -0
- data/spec/fixtures/books-duplicates/model.json +23 -0
- data/spec/fixtures/books-duplicates/summary.json +10 -0
- data/spec/fixtures/books-valid/README.md +13 -0
- data/spec/fixtures/books-valid/infile.csv +4 -0
- data/spec/fixtures/books-valid/infile.json +23 -0
- data/spec/fixtures/books-valid/infile.xml +21 -0
- data/spec/fixtures/books-valid/model.json +23 -0
- data/spec/fixtures/books-valid/record.json +26 -0
- data/spec/fixtures/books-valid/summary.json +8 -0
- data/spec/fixtures/source-empty-file/infile.csv +0 -0
- data/spec/fixtures/source-empty-file/infile.json +0 -0
- data/spec/fixtures/source-empty-file/infile.xml +0 -0
- data/spec/fixtures/source-empty-records/infile.csv +3 -0
- data/spec/fixtures/source-empty-records/infile.json +1 -0
- data/spec/fixtures/source-empty-records/infile.xml +6 -0
- data/spec/fixtures/source-malformed/infile.csv +1 -0
- data/spec/fixtures/source-malformed/infile.json +1 -0
- data/spec/fixtures/source-malformed/infile.xml +3 -0
- data/spec/fixtures/source-no-records/infile.csv +1 -0
- data/spec/fixtures/source-no-records/infile.json +1 -0
- data/spec/fixtures/source-no-records/infile.xml +3 -0
- data/spec/lib/importu/backends/active_record_spec.rb +150 -0
- data/spec/lib/importu/backends/middleware/duplicate_manager_proxy_spec.rb +70 -0
- data/spec/lib/importu/backends/middleware/enforce_allowed_actions_spec.rb +70 -0
- data/spec/lib/importu/backends_spec.rb +170 -0
- data/spec/lib/importu/converters_spec.rb +184 -141
- data/spec/lib/importu/definition_spec.rb +248 -0
- data/spec/lib/importu/duplicate_manager_spec.rb +92 -0
- data/spec/lib/importu/exceptions_spec.rb +69 -16
- data/spec/lib/importu/import_context_spec.rb +199 -0
- data/spec/lib/importu/importer_spec.rb +95 -0
- data/spec/lib/importu/integration_spec.rb +221 -0
- data/spec/lib/importu/record_spec.rb +130 -80
- data/spec/lib/importu/sources/csv_spec.rb +29 -0
- data/spec/lib/importu/sources/importer_source_examples.rb +175 -0
- data/spec/lib/importu/sources/json_spec.rb +29 -0
- data/spec/lib/importu/sources/ruby_spec.rb +102 -0
- data/spec/lib/importu/sources/xml_spec.rb +70 -0
- data/spec/lib/importu/summary_spec.rb +186 -0
- data/spec/spec_helper.rb +91 -7
- data/spec/support/active_record.rb +20 -0
- data/spec/support/book_importer.rb +31 -0
- data/spec/support/dummy_backend.rb +50 -0
- data/spec/support/fixtures_helper.rb +43 -0
- data/spec/support/matchers/delegate_matcher.rb +14 -8
- metadata +173 -100
- data/lib/importu/core_ext/array/deep_freeze.rb +0 -7
- data/lib/importu/core_ext/deep_freeze.rb +0 -3
- data/lib/importu/core_ext/hash/deep_freeze.rb +0 -7
- data/lib/importu/core_ext/object/deep_freeze.rb +0 -6
- data/lib/importu/core_ext.rb +0 -3
- data/lib/importu/dsl.rb +0 -127
- data/lib/importu/importer/csv.rb +0 -52
- data/lib/importu/importer/json.rb +0 -45
- data/lib/importu/importer/xml.rb +0 -55
- data/spec/factories/importer.rb +0 -12
- data/spec/factories/importer_record.rb +0 -13
- data/spec/factories/json_importer.rb +0 -14
- data/spec/factories/xml_importer.rb +0 -12
- data/spec/lib/importu/dsl_spec.rb +0 -26
- data/spec/lib/importu/importer/json_spec.rb +0 -37
- data/spec/lib/importu/importer/xml_spec.rb +0 -14
data/lib/importu/dsl.rb
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
require 'active_support/core_ext/module/delegation'
|
|
2
|
-
require 'active_support/core_ext/hash/deep_dup'
|
|
3
|
-
require 'active_support/core_ext/hash/keys'
|
|
4
|
-
require 'active_support/concern'
|
|
5
|
-
|
|
6
|
-
require 'importu/core_ext/deep_freeze'
|
|
7
|
-
|
|
8
|
-
# importer definition examples:
|
|
9
|
-
# allow_actions :create
|
|
10
|
-
# allow_actions :create, :update
|
|
11
|
-
#
|
|
12
|
-
# find_by :id # match against a single field, :id (default)
|
|
13
|
-
# find_by [:name, :date] # match against multiple fields
|
|
14
|
-
# find_by :id, [:name, :date] # try name/date combo if no id match
|
|
15
|
-
# find_by nil # never try to look up records, assume :create
|
|
16
|
-
# find_by do |record|
|
|
17
|
-
# scoped.where(:foo => record[:name].downcase)
|
|
18
|
-
# end
|
|
19
|
-
#
|
|
20
|
-
# field :field1, :label => 'Field 1'
|
|
21
|
-
# fields :field1, :field2, :field3
|
|
22
|
-
# fields :field1, :field2, convert_to(:integer)
|
|
23
|
-
# fields :field1, :field2 do |data,definition|
|
|
24
|
-
# Time.strptime(data[definition[:label]], '%d/%m/%Y')
|
|
25
|
-
# end
|
|
26
|
-
#
|
|
27
|
-
# allow actions:
|
|
28
|
-
# :create - if an existing record can't be found, we can create it
|
|
29
|
-
# :update - if an existing record found, update its attributes
|
|
30
|
-
#
|
|
31
|
-
# field(s) definition options:
|
|
32
|
-
# :label - header/label/key/element name used in input file (default: field name)
|
|
33
|
-
# :required - must be present in input file (values can be blank, default: true)
|
|
34
|
-
|
|
35
|
-
require 'active_support/concern'
|
|
36
|
-
|
|
37
|
-
module Importu::Dsl
|
|
38
|
-
extend ActiveSupport::Concern
|
|
39
|
-
|
|
40
|
-
included do
|
|
41
|
-
config_dsl :record_class, :default => Importu::Record
|
|
42
|
-
config_dsl :model, :description
|
|
43
|
-
config_dsl :allowed_actions, :default => [:create]
|
|
44
|
-
config_dsl :finder_fields, :default => [[:id]]
|
|
45
|
-
config_dsl :definitions, :default => {}
|
|
46
|
-
config_dsl :preprocessor, :postprocessor
|
|
47
|
-
config_dsl :converters, :default => {}
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
module ClassMethods
|
|
51
|
-
def allow_actions(*actions)
|
|
52
|
-
@allowed_actions = actions
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def find_by(*field_groups, &block)
|
|
56
|
-
@finder_fields = block ? [block] : field_groups.map {|g|g&&[*g]}.compact
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def fields(*fields, &block)
|
|
60
|
-
block = fields.pop if fields.last.kind_of?(Proc)
|
|
61
|
-
options = fields.extract_options!.symbolize_keys!
|
|
62
|
-
|
|
63
|
-
@definitions ||= definitions.deep_dup
|
|
64
|
-
fields.compact.each do |field_name|
|
|
65
|
-
definition = (@definitions[field_name]||{}).merge(options)
|
|
66
|
-
|
|
67
|
-
definition[:name] = field_name
|
|
68
|
-
definition[:label] ||= (options['label'] || field_name).to_s
|
|
69
|
-
definition[:required] = true unless definition.key?(:required)
|
|
70
|
-
definition[:create] = true unless definition.key?(:create)
|
|
71
|
-
definition[:update] = true unless definition.key?(:update)
|
|
72
|
-
|
|
73
|
-
definition[:converter] = block if block
|
|
74
|
-
definition[:converter] ||= converters[:clean]
|
|
75
|
-
|
|
76
|
-
@definitions[field_name] = definition
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
return
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
alias_method :field, :fields
|
|
83
|
-
|
|
84
|
-
def preprocess(&block)
|
|
85
|
-
# gets executed just before record converted to object
|
|
86
|
-
@preprocessor = block
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def postprocess(&block)
|
|
90
|
-
# gets executed just after record converted to object
|
|
91
|
-
@postprocessor = block
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def converter(name, &block)
|
|
95
|
-
@converters = converters.merge(name => block)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def convert_to(type, options = {})
|
|
99
|
-
converters[type] # FIXME: raise error if not found?
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def config_dsl(*methods)
|
|
103
|
-
options = methods.extract_options!
|
|
104
|
-
options.assert_valid_keys(:default)
|
|
105
|
-
default = (options[:default] || nil).deep_freeze
|
|
106
|
-
|
|
107
|
-
methods.each do |m|
|
|
108
|
-
instance_variable_set("@#{m}", default)
|
|
109
|
-
|
|
110
|
-
singleton_class.send(:define_method, m) do |*args,&block|
|
|
111
|
-
if block || !args.empty?
|
|
112
|
-
val = (block ? instance_eval(&block) : args[0])
|
|
113
|
-
instance_variable_set("@#{m}", val.deep_freeze)
|
|
114
|
-
else
|
|
115
|
-
instance_variable_defined?("@#{m}") \
|
|
116
|
-
? instance_variable_get("@#{m}")
|
|
117
|
-
: superclass.send(m)
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# make dsl methods available to importer instances
|
|
123
|
-
delegate *methods, :to => :singleton_class
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
end
|
data/lib/importu/importer/csv.rb
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
require 'csv'
|
|
2
|
-
|
|
3
|
-
class Importu::Importer::Csv < Importu::Importer
|
|
4
|
-
def initialize(infile, options = {})
|
|
5
|
-
super
|
|
6
|
-
|
|
7
|
-
@csv_options = {
|
|
8
|
-
:headers => true,
|
|
9
|
-
:return_headers => true,
|
|
10
|
-
:write_headers => true,
|
|
11
|
-
:skip_blanks => true,
|
|
12
|
-
}.merge(options[:csv_options]||{})
|
|
13
|
-
|
|
14
|
-
@reader = ::CSV.new(@infile, @csv_options)
|
|
15
|
-
@header = @reader.readline
|
|
16
|
-
@data_pos = @infile.pos
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def records
|
|
20
|
-
@infile.pos = @data_pos
|
|
21
|
-
Enumerator.new do |yielder|
|
|
22
|
-
@reader.each do |row|
|
|
23
|
-
yielder.yield record_class.new(self, row.to_hash, row)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def import_record(record, finder_scope, &block)
|
|
29
|
-
begin
|
|
30
|
-
super
|
|
31
|
-
rescue Importu::MissingField => e
|
|
32
|
-
# if one record missing field, all are, major error
|
|
33
|
-
raise Importu::InvalidInput, "missing required field: #{e.message}"
|
|
34
|
-
rescue Importu::InvalidRecord => e
|
|
35
|
-
write_error(record.raw_data, e.message)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def write_error(data, msg)
|
|
43
|
-
unless @writer
|
|
44
|
-
@writer = ::CSV.new(outfile, @csv_options)
|
|
45
|
-
@header['_errors'] = '_errors'
|
|
46
|
-
@writer << @header
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
data['_errors'] = msg
|
|
50
|
-
@writer << data
|
|
51
|
-
end
|
|
52
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
require 'multi_json'
|
|
2
|
-
|
|
3
|
-
class Importu::Importer::Json < Importu::Importer
|
|
4
|
-
def initialize(infile, options = {})
|
|
5
|
-
super
|
|
6
|
-
|
|
7
|
-
begin
|
|
8
|
-
infile.rewind
|
|
9
|
-
@reader = MultiJson.load(infile.read)
|
|
10
|
-
rescue MultiJson::DecodeError => e
|
|
11
|
-
raise Importu::InvalidInput, e.message
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def import!(finder_scope = nil, &block)
|
|
16
|
-
result = super
|
|
17
|
-
outfile.write(JSON.pretty_generate(@error_records)) if @invalid > 0
|
|
18
|
-
result
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def records(&block)
|
|
22
|
-
enum = Enumerator.new do |yielder|
|
|
23
|
-
@reader.each_with_index do |data,idx|
|
|
24
|
-
yielder.yield record_class.new(self, data, data)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def import_record(record, finder_scope, &block)
|
|
30
|
-
begin
|
|
31
|
-
super
|
|
32
|
-
rescue Importu::InvalidRecord => e
|
|
33
|
-
write_error(record.raw_data, e.message)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
def write_error(data, msg)
|
|
41
|
-
@error_records ||= []
|
|
42
|
-
@error_records << data.merge('_errors' => msg)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
end
|
data/lib/importu/importer/xml.rb
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
require 'nokogiri'
|
|
2
|
-
|
|
3
|
-
class Importu::Importer::Xml < Importu::Importer
|
|
4
|
-
config_dsl :records_xpath
|
|
5
|
-
|
|
6
|
-
def initialize(infile, options = {})
|
|
7
|
-
super
|
|
8
|
-
|
|
9
|
-
xml_options = {}.merge(options[:xml_options]||{})
|
|
10
|
-
if reader.errors.any?
|
|
11
|
-
raise Importu::InvalidInput, reader.errors.join("\n")
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def reader
|
|
16
|
-
@reader ||= Nokogiri::XML(infile)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def import!(finder_scope = nil, &block)
|
|
20
|
-
reader.xpath('//_errors').remove
|
|
21
|
-
result = super
|
|
22
|
-
outfile.write(reader) if @invalid > 0
|
|
23
|
-
result
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def records
|
|
27
|
-
Enumerator.new do |yielder|
|
|
28
|
-
reader.xpath(records_xpath).each do |xml|
|
|
29
|
-
data = Hash[xml.elements.map {|e| [e.name, e.content]}]
|
|
30
|
-
yielder.yield record_class.new(self, data, xml)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def import_record(record, finder_scope, &block)
|
|
36
|
-
begin
|
|
37
|
-
super
|
|
38
|
-
record.raw_data.remove
|
|
39
|
-
rescue Importu::InvalidRecord => e
|
|
40
|
-
add_xml_record_error(record.raw_data, e.message)
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
def add_xml_record_error(xml, text)
|
|
48
|
-
unless node = xml.xpath('./_errors').first
|
|
49
|
-
node = Nokogiri::XML::Node.new '_errors', reader
|
|
50
|
-
xml.add_child(node)
|
|
51
|
-
end
|
|
52
|
-
node.content = text + ','
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
end
|
data/spec/factories/importer.rb
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
FactoryGirl.define do
|
|
2
|
-
factory :importer_record, :class => Importu::Record do
|
|
3
|
-
initialize_with do
|
|
4
|
-
Importu::Record.new(importer, data, raw_data)
|
|
5
|
-
end
|
|
6
|
-
|
|
7
|
-
ignore do
|
|
8
|
-
importer { build(:importer) }
|
|
9
|
-
data { Hash.new }
|
|
10
|
-
raw_data { Hash.new }
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
FactoryGirl.define do
|
|
2
|
-
factory :json_importer, :class => Importu::Importer::Json do
|
|
3
|
-
initialize_with do
|
|
4
|
-
infile = StringIO.new(data) if data
|
|
5
|
-
Importu::Importer::Json.new(infile, options)
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
ignore do
|
|
9
|
-
data nil # string version of input file
|
|
10
|
-
infile { StringIO.new("[]") }
|
|
11
|
-
options { Hash.new }
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
require 'spec_helper'
|
|
2
|
-
|
|
3
|
-
describe Importu::Importer do
|
|
4
|
-
describe "::record_class" do
|
|
5
|
-
it "returns Importu::Record by default" do
|
|
6
|
-
Importu::Importer.record_class.should eq Importu::Record
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
it "can be overridden globally" do
|
|
10
|
-
custom_record_class = Class.new(Importu::Record)
|
|
11
|
-
orig = Importu::Importer.record_class
|
|
12
|
-
Importu::Importer.record_class custom_record_class
|
|
13
|
-
Importu::Importer.record_class.should eq custom_record_class
|
|
14
|
-
Importu::Importer.record_class orig
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
it "can be overridden in a subclass" do
|
|
18
|
-
custom_record_class = Class.new(Importu::Record)
|
|
19
|
-
klass = Class.new(Importu::Importer) do
|
|
20
|
-
record_class custom_record_class
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
klass.record_class.should eq custom_record_class
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
require 'spec_helper'
|
|
2
|
-
|
|
3
|
-
describe Importu::Importer::Json do
|
|
4
|
-
subject(:importer) { build(:json_importer, :data => data) }
|
|
5
|
-
|
|
6
|
-
context "input file is blank" do
|
|
7
|
-
let(:data) { "" }
|
|
8
|
-
|
|
9
|
-
it "raises an InvalidInput exception" do
|
|
10
|
-
expect { importer }.to raise_error Importu::InvalidInput
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
context "non-array root elements" do
|
|
15
|
-
%w({}, "foo", 3, 3.7, false, nil).each do |data|
|
|
16
|
-
it "raises InvalidInput exception if root is #{data}" do
|
|
17
|
-
expect { build(:json_importer, :data => "") }.to raise_error Importu::InvalidInput
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
context "input file is []" do
|
|
23
|
-
let(:data) { "[]" }
|
|
24
|
-
|
|
25
|
-
it "treats file as having 0 records" do
|
|
26
|
-
importer.records.should have(0).items
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
context "input file is [{},{}]" do
|
|
31
|
-
let(:data) { "[{},{}]" }
|
|
32
|
-
|
|
33
|
-
it "treats file as having 2 records" do
|
|
34
|
-
importer.records.should have(2).items
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
require 'spec_helper'
|
|
2
|
-
|
|
3
|
-
describe Importu::Importer::Xml do
|
|
4
|
-
subject(:importer) { build(:xml_importer, :infile => infile) }
|
|
5
|
-
|
|
6
|
-
context "input file is blank" do
|
|
7
|
-
let(:infile) { StringIO.new }
|
|
8
|
-
|
|
9
|
-
it "raises an InvalidInput exception" do
|
|
10
|
-
expect { importer }.to raise_error Importu::InvalidInput
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
end
|