importu 0.1.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/.gitignore +3 -0
- data/Gemfile +4 -0
- data/README.md +163 -0
- data/importu.gemspec +29 -0
- data/lib/importu.rb +12 -0
- data/lib/importu/converters.rb +82 -0
- data/lib/importu/core_ext.rb +3 -0
- data/lib/importu/core_ext/array/deep_freeze.rb +7 -0
- data/lib/importu/core_ext/deep_freeze.rb +3 -0
- data/lib/importu/core_ext/hash/deep_freeze.rb +7 -0
- data/lib/importu/core_ext/object/deep_freeze.rb +6 -0
- data/lib/importu/dsl.rb +127 -0
- data/lib/importu/exceptions.rb +34 -0
- data/lib/importu/importer.rb +119 -0
- data/lib/importu/importer/csv.rb +52 -0
- data/lib/importu/importer/json.rb +45 -0
- data/lib/importu/importer/xml.rb +55 -0
- data/lib/importu/record.rb +124 -0
- data/lib/importu/version.rb +3 -0
- data/spec/factories/importer.rb +12 -0
- data/spec/factories/importer_record.rb +13 -0
- data/spec/factories/json_importer.rb +14 -0
- data/spec/factories/xml_importer.rb +12 -0
- data/spec/lib/importu/converters_spec.rb +276 -0
- data/spec/lib/importu/dsl_spec.rb +26 -0
- data/spec/lib/importu/exceptions_spec.rb +96 -0
- data/spec/lib/importu/importer/json_spec.rb +37 -0
- data/spec/lib/importu/importer/xml_spec.rb +14 -0
- data/spec/lib/importu/record_spec.rb +123 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/matchers/delegate_matcher.rb +42 -0
- metadata +218 -0
@@ -0,0 +1,13 @@
|
|
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
|
@@ -0,0 +1,14 @@
|
|
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
|
@@ -0,0 +1,276 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'active_support/core_ext/time/calculations'
|
4
|
+
|
5
|
+
describe Importu::Importer do
|
6
|
+
subject(:record) { build(:importer_record) }
|
7
|
+
|
8
|
+
describe ":raw converter" do
|
9
|
+
it "uses definition's label as key when looking up data" do
|
10
|
+
record.stub(:definitions => { :field1 => { :label => "field1", :required => true } })
|
11
|
+
record.stub(:data => { "field1" => " value1 ", "field2" => "value2" })
|
12
|
+
record.convert(:field1, :raw).should eq " value1 "
|
13
|
+
end
|
14
|
+
|
15
|
+
it "raises an exception if field definition is not defined" do
|
16
|
+
expect { record.convert(:field1, :raw) }.to raise_error
|
17
|
+
end
|
18
|
+
|
19
|
+
it "raises MissingField if field data not defined" do
|
20
|
+
record.stub(:definitions => { :field1 => { :required => true } })
|
21
|
+
expect { record.convert(:field1, :raw) }.to raise_error(Importu::MissingField)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe ":clean converter" do
|
26
|
+
it "returns nil if raw data is nil" do
|
27
|
+
record.converters[:raw] = proc { nil }
|
28
|
+
record.convert(:field1, :clean).should be nil
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns nil if a blank string" do
|
32
|
+
record.converters[:raw] = proc { "" }
|
33
|
+
record.convert(:field1, :clean).should be nil
|
34
|
+
end
|
35
|
+
|
36
|
+
it "returns stripped value if a string" do
|
37
|
+
record.converters[:raw] = proc { " abc 123 " }
|
38
|
+
record.convert(:field1, :clean).should eq "abc 123"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "returns original value if not a string" do
|
42
|
+
raw_val = Time.now
|
43
|
+
record.converters[:raw] = proc { raw_val }
|
44
|
+
record.convert(:field1, :clean).should eq raw_val
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe ":string converter" do
|
49
|
+
it "returns nil if clean value is nil" do
|
50
|
+
record.converters[:clean] = proc { nil }
|
51
|
+
record.convert(:field1, :string).should be nil
|
52
|
+
end
|
53
|
+
|
54
|
+
it "returns string if clean value is a string" do
|
55
|
+
record.converters[:clean] = proc { "six pence" }
|
56
|
+
record.convert(:field1, :string).should eq "six pence"
|
57
|
+
end
|
58
|
+
|
59
|
+
it "converts clean value to string if not a string" do
|
60
|
+
clean_val = Time.now
|
61
|
+
record.converters[:clean] = proc { clean_val }
|
62
|
+
record.convert(:field1, :string).should eq clean_val.to_s
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe ":integer converter" do
|
67
|
+
it "returns nil if clean value is nil" do
|
68
|
+
record.converters[:clean] = proc { nil }
|
69
|
+
record.convert(:field1, :integer).should be nil
|
70
|
+
end
|
71
|
+
|
72
|
+
it "returns integer if clean value returns an integer" do
|
73
|
+
record.converters[:clean] = proc { 92 }
|
74
|
+
record.convert(:field1, :integer).should eq 92
|
75
|
+
end
|
76
|
+
|
77
|
+
it "converts clean value to integer if not an integer type" do
|
78
|
+
record.converters[:clean] = proc { "29" }
|
79
|
+
record.convert(:field1, :integer).should eq 29
|
80
|
+
end
|
81
|
+
|
82
|
+
it "raises an exception if clean value is not a valid integer" do
|
83
|
+
record.converters[:clean] = proc { "4.25" }
|
84
|
+
expect { record.convert(:field1, :integer) }.to raise_error(Importu::FieldParseError)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe ":float converter" do
|
89
|
+
it "returns nil if clean value is nil" do
|
90
|
+
record.converters[:clean] = proc { nil }
|
91
|
+
record.convert(:field1, :float).should be nil
|
92
|
+
end
|
93
|
+
|
94
|
+
it "returns float if clean value returns an float" do
|
95
|
+
record.converters[:clean] = proc { 92.25 }
|
96
|
+
record.convert(:field1, :float).should eq 92.25
|
97
|
+
end
|
98
|
+
|
99
|
+
it "converts clean value to float if not an float type" do
|
100
|
+
record.converters[:clean] = proc { "29.4" }
|
101
|
+
record.convert(:field1, :float).should eq 29.4
|
102
|
+
end
|
103
|
+
|
104
|
+
it "converts whole values to float" do
|
105
|
+
record.converters[:clean] = proc { "77" }
|
106
|
+
record.convert(:field1, :float).should eq 77.0
|
107
|
+
end
|
108
|
+
|
109
|
+
it "raises an exception if clean value is not a valid float" do
|
110
|
+
record.converters[:clean] = proc { "4d6point3" }
|
111
|
+
expect { record.convert(:field1, :float) }.to raise_error(Importu::FieldParseError)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe ":decimal converter" do
|
116
|
+
it "returns nil if clean value is nil" do
|
117
|
+
record.converters[:clean] = proc { nil }
|
118
|
+
record.convert(:field1, :decimal).should be nil
|
119
|
+
end
|
120
|
+
|
121
|
+
it "returns decimal if clean value returns an decimal" do
|
122
|
+
clean_val = BigDecimal("92.25")
|
123
|
+
record.converters[:clean] = proc { clean_val }
|
124
|
+
record.convert(:field1, :decimal).should eq clean_val
|
125
|
+
end
|
126
|
+
|
127
|
+
it "converts clean value to decimal if not an decimal type" do
|
128
|
+
record.converters[:clean] = proc { "29.4" }
|
129
|
+
record.convert(:field1, :decimal).should eq BigDecimal("29.4")
|
130
|
+
end
|
131
|
+
|
132
|
+
it "converts whole values to decimal" do
|
133
|
+
record.converters[:clean] = proc { "77" }
|
134
|
+
record.convert(:field1, :decimal).should eq BigDecimal("77.0")
|
135
|
+
end
|
136
|
+
|
137
|
+
it "raises an exception if clean value is not a valid decimal" do
|
138
|
+
record.converters[:clean] = proc { "4d6point3" }
|
139
|
+
expect { record.convert(:field1, :decimal) }.to raise_error(Importu::FieldParseError)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe ":boolean converter" do
|
144
|
+
it "returns nil if clean value is nil" do
|
145
|
+
record.converters[:clean] = proc { nil }
|
146
|
+
record.convert(:field1, :boolean).should be nil
|
147
|
+
end
|
148
|
+
|
149
|
+
it "returns true if clean value is true" do
|
150
|
+
record.converters[:clean] = proc { true }
|
151
|
+
record.convert(:field1, :boolean).should eq true
|
152
|
+
end
|
153
|
+
|
154
|
+
it "returns true if clean value is 'true'" do
|
155
|
+
record.converters[:clean] = proc { "true" }
|
156
|
+
record.convert(:field1, :boolean).should eq true
|
157
|
+
end
|
158
|
+
|
159
|
+
it "returns true if clean value is 'yes'" do
|
160
|
+
record.converters[:clean] = proc { "yes" }
|
161
|
+
record.convert(:field1, :boolean).should eq true
|
162
|
+
end
|
163
|
+
|
164
|
+
it "returns true if clean value is '1'" do
|
165
|
+
record.converters[:clean] = proc { "1" }
|
166
|
+
record.convert(:field1, :boolean).should eq true
|
167
|
+
end
|
168
|
+
|
169
|
+
it "returns true if clean value is 1" do
|
170
|
+
record.converters[:clean] = proc { 1 }
|
171
|
+
record.convert(:field1, :boolean).should eq true
|
172
|
+
end
|
173
|
+
|
174
|
+
it "returns false if clean value is false" do
|
175
|
+
record.converters[:clean] = proc { false }
|
176
|
+
record.convert(:field1, :boolean).should eq false
|
177
|
+
end
|
178
|
+
|
179
|
+
it "returns false if clean value is 'false'" do
|
180
|
+
record.converters[:clean] = proc { "false" }
|
181
|
+
record.convert(:field1, :boolean).should eq false
|
182
|
+
end
|
183
|
+
|
184
|
+
it "returns false if clean value is 'no'" do
|
185
|
+
record.converters[:clean] = proc { "no" }
|
186
|
+
record.convert(:field1, :boolean).should eq false
|
187
|
+
end
|
188
|
+
|
189
|
+
it "returns false if clean value is '0'" do
|
190
|
+
record.converters[:clean] = proc { "0" }
|
191
|
+
record.convert(:field1, :boolean).should eq false
|
192
|
+
end
|
193
|
+
|
194
|
+
it "returns false if clean value is 0" do
|
195
|
+
record.converters[:clean] = proc { 0 }
|
196
|
+
record.convert(:field1, :boolean).should eq false
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
describe ":date converter" do
|
201
|
+
it "returns nil if clean value is nil" do
|
202
|
+
record.converters[:clean] = proc { nil }
|
203
|
+
record.convert(:field1, :date).should be nil
|
204
|
+
end
|
205
|
+
|
206
|
+
context "when a format is not specified" do
|
207
|
+
it "tries to guess the date format (DD/MM/YYYY is default?)" do
|
208
|
+
expected = Date.new(2012, 10, 3)
|
209
|
+
record.converters[:clean] = proc { "03/10/2012" }
|
210
|
+
record.convert(:field1, :date).should eq expected
|
211
|
+
end
|
212
|
+
|
213
|
+
it "raises an exception if the date is invaild" do
|
214
|
+
record.converters[:clean] = proc { "2012-04-32" }
|
215
|
+
expect { record.convert(:field1, :date) }.to raise_error(Importu::FieldParseError)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
context "when a format is specified" do
|
220
|
+
it "parses dates that match the format" do
|
221
|
+
expected = Date.new(2012, 4, 18)
|
222
|
+
record.converters[:clean] = proc { "04/18/2012" }
|
223
|
+
record.convert(:field1, :date, :format => "%m/%d/%Y").should eq expected
|
224
|
+
end
|
225
|
+
|
226
|
+
it "raises exception if date doesn't match the format" do
|
227
|
+
record.converters[:clean] = proc { "04-18-2012" }
|
228
|
+
expect { record.convert(:field1, :date) }.to raise_error(Importu::FieldParseError)
|
229
|
+
end
|
230
|
+
|
231
|
+
it "raises an exception if the date is invalid" do
|
232
|
+
record.converters[:clean] = proc { "04/32/2012" }
|
233
|
+
expect { record.convert(:field1, :date) }.to raise_error(Importu::FieldParseError)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe ":datetime converter" do
|
239
|
+
it "returns nil if clean value is nil" do
|
240
|
+
record.converters[:clean] = proc { nil }
|
241
|
+
record.convert(:field1, :datetime).should be nil
|
242
|
+
end
|
243
|
+
|
244
|
+
context "when a format is not specified" do
|
245
|
+
it "tries to guess the date format (DD/MM/YYYY is default?)" do
|
246
|
+
expected = DateTime.parse("2012-10-03 04:37:29")
|
247
|
+
record.converters[:clean] = proc { "03/10/2012 04:37:29" }
|
248
|
+
record.convert(:field1, :datetime).should eq expected
|
249
|
+
end
|
250
|
+
|
251
|
+
it "raises an exception if the datetime is invaild" do
|
252
|
+
record.converters[:clean] = proc { "2012-04-32 15:41:22" }
|
253
|
+
expect { record.convert(:field1, :datetime) }.to raise_error(Importu::FieldParseError)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
context "when a format is specified" do
|
258
|
+
it "parses datetimes that match the format" do
|
259
|
+
expected = DateTime.parse("2012-04-18 16:37:00")
|
260
|
+
record.converters[:clean] = proc { "04/18/2012 16:37" }
|
261
|
+
record.convert(:field1, :datetime, :format => "%m/%d/%Y %H:%M").should eq expected
|
262
|
+
end
|
263
|
+
|
264
|
+
it "raises exception if datetime doesn't match the format" do
|
265
|
+
record.converters[:clean] = proc { "04-18-2012 15:22:19" }
|
266
|
+
expect { record.convert(:field1, :datetime) }.to raise_error(Importu::FieldParseError)
|
267
|
+
end
|
268
|
+
|
269
|
+
it "raises an exception if the datetime is invalid" do
|
270
|
+
record.converters[:clean] = proc { "04/32/2012 00:00:00" }
|
271
|
+
expect { record.convert(:field1, :datetime) }.to raise_error(Importu::FieldParseError)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
end
|
@@ -0,0 +1,26 @@
|
|
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
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Importu::ImportuException do
|
4
|
+
subject(:exception) { Importu::ImportuException.new }
|
5
|
+
|
6
|
+
it "#name should return 'ImportuException" do
|
7
|
+
exception.name.should eq "ImportuException"
|
8
|
+
end
|
9
|
+
|
10
|
+
describe Importu::InvalidInput do
|
11
|
+
subject(:exception) { Importu::InvalidInput.new }
|
12
|
+
|
13
|
+
it "should be a subclass of Importu::ImportuException" do
|
14
|
+
exception.class.ancestors.include?(Importu::ImportuException)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "#name should return 'InvalidInput'" do
|
18
|
+
exception.name.should eq "InvalidInput"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe Importu::InvalidRecord do
|
23
|
+
subject(:exception) { Importu::InvalidRecord.new }
|
24
|
+
|
25
|
+
it "should be a subclass of Importu::ImportuException" do
|
26
|
+
exception.class.ancestors.include?(Importu::ImportuException)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "#name should return 'InvalidRecord'" do
|
30
|
+
exception.name.should eq "InvalidRecord"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe Importu::FieldParseError do
|
35
|
+
subject(:exception) { Importu::FieldParseError.new }
|
36
|
+
|
37
|
+
it "should be a subclass of Importu::InvalidRecord" do
|
38
|
+
exception.class.ancestors.include?(Importu::InvalidRecord)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "#name should return 'FieldParseError'" do
|
42
|
+
exception.name.should eq "FieldParseError"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe Importu::MissingField do
|
47
|
+
let(:definition) { { :name => "foo_field_1", :label => "Field 1" } }
|
48
|
+
subject(:exception) { Importu::MissingField.new(definition) }
|
49
|
+
|
50
|
+
it "should be a subclass of Importu::InvalidRecord" do
|
51
|
+
exception.class.ancestors.include?(Importu::InvalidRecord)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "#name should return 'MissingField'" do
|
55
|
+
exception.name.should eq "MissingField"
|
56
|
+
end
|
57
|
+
|
58
|
+
it "#definition should return the definition passed during construction" do
|
59
|
+
exception.definition.should eq definition
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#message" do
|
63
|
+
it "should mention a missing field" do
|
64
|
+
exception.message.should match(/missing field/i)
|
65
|
+
end
|
66
|
+
|
67
|
+
context "field definition has a label" do
|
68
|
+
let(:definition) { { :label => "Field 2" } }
|
69
|
+
it "mentions missing field's label" do
|
70
|
+
exception.message.should match(/Field 2/)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context "field definition is missing a label" do
|
75
|
+
let(:definition) { { :name => "foo_field_2" } }
|
76
|
+
|
77
|
+
it "mentions missing field's name" do
|
78
|
+
exception.message.should match(/foo_field_2/)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe Importu::DuplicateRecord do
|
85
|
+
subject(:exception) { Importu::DuplicateRecord.new }
|
86
|
+
|
87
|
+
it "should be a subclass of Importu::InvalidRecord" do
|
88
|
+
exception.class.ancestors.include?(Importu::InvalidRecord)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "#name should return 'DuplicateRecord'" do
|
92
|
+
exception.name.should eq "DuplicateRecord"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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
|