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
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
3
|
+
|
|
4
|
+
require "importu/definition"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Importu::Definition do
|
|
7
|
+
subject(:definition) { Class.new(ancestor) }
|
|
8
|
+
let(:ancestor) { Class.new(Importu::Definition) }
|
|
9
|
+
|
|
10
|
+
describe "#allow_actions" do
|
|
11
|
+
it "updates the [:backend][:allowed_actions] config" do
|
|
12
|
+
expect { definition.allow_actions(:create, :update) }
|
|
13
|
+
.to change { definition.config[:backend][:allowed_actions] }
|
|
14
|
+
.to([:create, :update])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "inherits config from ancestor" do
|
|
18
|
+
ancestor.allow_actions(:update)
|
|
19
|
+
expect(definition.config[:backend][:allowed_actions]).to eq [:update]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "does not affect ancestor config" do
|
|
23
|
+
ancestor.allow_actions(:update)
|
|
24
|
+
expect { definition.allow_actions(:create, :update) }
|
|
25
|
+
.to_not change { ancestor.config }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe "#before_save" do
|
|
30
|
+
let(:foo_block) { Proc.new {} }
|
|
31
|
+
|
|
32
|
+
it "updates the [:backend][:before_save] config" do
|
|
33
|
+
expect { definition.before_save(&foo_block) }
|
|
34
|
+
.to change { definition.config[:backend][:before_save] }
|
|
35
|
+
.to(foo_block)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "inherits config from ancestor" do
|
|
39
|
+
ancestor.before_save(&foo_block)
|
|
40
|
+
expect(definition.config[:backend][:before_save]).to eq foo_block
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "does not affect ancestor config" do
|
|
44
|
+
ancestor.before_save(&foo_block)
|
|
45
|
+
expect { definition.before_save {} }
|
|
46
|
+
.to_not change { ancestor.config }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe "#config" do
|
|
51
|
+
it "returns a config hash of the definition" do
|
|
52
|
+
expect(definition.config).to include(:converters, :fields)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "[:backend][:allowed_actions] defaults to only allow :create" do
|
|
56
|
+
expect(definition.config[:backend][:allowed_actions]).to eq [:create]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "[:backend][:finder_fields] defaults to no fields" do
|
|
60
|
+
expect(definition.config[:backend][:finder_fields]).to eq []
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe "#convert_to" do
|
|
65
|
+
let!(:converter) do
|
|
66
|
+
definition.converter :foo do |name, **options|
|
|
67
|
+
"foo(#{name}, {#{options.map {|k, v| "#{k}:#{v}"}.join(",")}})"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "returns a converter stub representing the converter" do
|
|
72
|
+
stub = definition.convert_to(:foo)
|
|
73
|
+
expect(stub.type).to eq :foo
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "saves converter options from definition" do
|
|
77
|
+
expect(definition.convert_to(:foo, a: 7).options).to eq({a: 7})
|
|
78
|
+
expect(definition.convert_to(:foo).options).to eq({})
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "raises an exception if converter cannot be found" do
|
|
82
|
+
expect { definition.convert_to(:bar) }.to raise_error(KeyError)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe "#converter" do
|
|
87
|
+
let(:foo_block) { Proc.new {} }
|
|
88
|
+
|
|
89
|
+
it "updates the :converters config" do
|
|
90
|
+
expect { definition.converter(:foo, &foo_block) }
|
|
91
|
+
.to change { definition.config[:converters][:foo] }
|
|
92
|
+
.to(foo_block)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "inherits config from ancestor" do
|
|
96
|
+
ancestor.converter(:foo, &foo_block)
|
|
97
|
+
expect(definition.config[:converters][:foo]).to eq foo_block
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "does not affect ancestor config" do
|
|
101
|
+
ancestor.converter(:foo, &foo_block)
|
|
102
|
+
expect { definition.converter(:foo) {} }
|
|
103
|
+
.to_not change { ancestor.config }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe "#field" do
|
|
108
|
+
let(:converter) { Proc.new {} }
|
|
109
|
+
|
|
110
|
+
it "updates the :fields config and presets defaults" do
|
|
111
|
+
definition.field(:foo, required: false)
|
|
112
|
+
expect(definition.config[:fields][:foo])
|
|
113
|
+
.to eq definition.field_defaults(:foo).merge(required: false)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "inherits config from ancestors" do
|
|
117
|
+
ancestor.field(:foo, required: false, &converter)
|
|
118
|
+
expect(definition.config[:fields][:foo]).to include(
|
|
119
|
+
required: false,
|
|
120
|
+
converter: converter,
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "does not affect ancestor config" do
|
|
125
|
+
ancestor.field(:foo, required: false, &converter)
|
|
126
|
+
expect { definition.field(:foo, abstract: true, label: "baaa") }
|
|
127
|
+
.to_not change { ancestor.config }
|
|
128
|
+
expect(definition.config[:fields][:foo]).to include(
|
|
129
|
+
required: false,
|
|
130
|
+
abstract: true,
|
|
131
|
+
label: "baaa"
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "allows partially updating the field config" do
|
|
136
|
+
ancestor.field(:foo, required: false, &converter)
|
|
137
|
+
definition.field(:foo, abstract: true, label: "baaa")
|
|
138
|
+
definition.field(:foo, create: false)
|
|
139
|
+
expect(definition.config[:fields][:foo]).to include(
|
|
140
|
+
required: false,
|
|
141
|
+
abstract: true,
|
|
142
|
+
create: false,
|
|
143
|
+
label: "baaa",
|
|
144
|
+
converter: converter,
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "defaults to using the :default converter" do
|
|
149
|
+
definition.field(:foo)
|
|
150
|
+
field_definition = definition.config[:fields][:foo]
|
|
151
|
+
expect(field_definition[:converter].type).to eq :default
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe "#fields" do
|
|
156
|
+
it "configures each field with the same properties" do
|
|
157
|
+
converter = Proc.new {}
|
|
158
|
+
expect(definition).to receive(:field).with(:foo, required: false, &converter)
|
|
159
|
+
expect(definition).to receive(:field).with(:bar, required: false, &converter)
|
|
160
|
+
expect(definition).to receive(:field).with(:baz, required: false, &converter)
|
|
161
|
+
definition.fields(:foo, :bar, :baz, required: false, &converter)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
describe "#find_by" do
|
|
166
|
+
it "updates the [:backend][:finder_fields] config" do
|
|
167
|
+
expect { definition.find_by(:foo, [:bar, :baz]) }
|
|
168
|
+
.to change { definition.config[:backend][:finder_fields] }
|
|
169
|
+
.to([[:foo], [:bar, :baz]])
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "inherits config from ancestor" do
|
|
173
|
+
ancestor.find_by(:foo, [:bar, :baz])
|
|
174
|
+
expect(definition.config[:backend][:finder_fields])
|
|
175
|
+
.to eq [[:foo], [:bar, :baz]]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "does not affect ancestor config" do
|
|
179
|
+
ancestor.find_by(:foo, [:bar, :baz])
|
|
180
|
+
expect { definition.find_by(:bar) }
|
|
181
|
+
.to_not change { ancestor.config }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "allows setting to a block" do
|
|
185
|
+
foo_block = Proc.new {}
|
|
186
|
+
expect { definition.find_by(&foo_block) }
|
|
187
|
+
.to change { definition.config[:backend][:finder_fields] }
|
|
188
|
+
.to([foo_block])
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it "allows clearing all finder fields" do
|
|
192
|
+
ancestor.find_by(:foo, [:bar, :baz])
|
|
193
|
+
expect { definition.find_by(nil) }
|
|
194
|
+
.to change { definition.config[:backend][:finder_fields] }
|
|
195
|
+
.to([])
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
describe "#model" do
|
|
200
|
+
it "updates the :backend config" do
|
|
201
|
+
expect { definition.model("Foo") }
|
|
202
|
+
.to change { definition.config[:backend][:model] }
|
|
203
|
+
.to("Foo")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "inherits config from ancestor" do
|
|
207
|
+
ancestor.model("Foo")
|
|
208
|
+
expect(definition.config[:backend][:model]).to eq "Foo"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "does not affect ancestor config" do
|
|
212
|
+
ancestor.model("Foo")
|
|
213
|
+
expect { definition.model("Bar") }
|
|
214
|
+
.to_not change { ancestor.config }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "allows specifying a backend property" do
|
|
218
|
+
expect { definition.model("Foo", backend: :oven) }
|
|
219
|
+
.to change { definition.config[:backend][:name] }
|
|
220
|
+
.to(:oven)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
describe "#source" do
|
|
225
|
+
let(:converter) { Proc.new {} }
|
|
226
|
+
|
|
227
|
+
it "updates the :sources config" do
|
|
228
|
+
definition.source(:xml, records_xpath: "//people")
|
|
229
|
+
expect(definition.config[:sources][:xml])
|
|
230
|
+
.to include(records_xpath: "//people")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it "inherits config from ancestors" do
|
|
234
|
+
ancestor.source(:xml, records_xpath: "//animals")
|
|
235
|
+
expect(definition.config[:sources][:xml])
|
|
236
|
+
.to include(records_xpath: "//animals")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it "does not affect ancestor config" do
|
|
240
|
+
ancestor.source(:xml, records_xpath: "//animals")
|
|
241
|
+
expect { definition.source(:xml, records_xpath: "//people") }
|
|
242
|
+
.to_not change { ancestor.config }
|
|
243
|
+
expect(definition.config[:sources][:xml])
|
|
244
|
+
.to include(records_xpath: "//people")
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
3
|
+
|
|
4
|
+
require "importu/exceptions"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Importu::DuplicateManager do
|
|
7
|
+
subject(:duplicates) { described_class.new(finder_fields: finder_fields) }
|
|
8
|
+
|
|
9
|
+
let(:finder_fields) { [] }
|
|
10
|
+
|
|
11
|
+
describe "#check_object!" do
|
|
12
|
+
it "allows different unique ids to be set without any errors" do
|
|
13
|
+
duplicates.check_object!(9164)
|
|
14
|
+
expect { duplicates.check_object!(9165) }.to_not raise_error
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "raises a DuplicateRecord when unique on subsequent encounters" do
|
|
18
|
+
duplicates.check_object!(9164)
|
|
19
|
+
expect { duplicates.check_object!(9164) }
|
|
20
|
+
.to raise_error(Importu::DuplicateRecord)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "#check_record!" do
|
|
25
|
+
context "when no finder fields are defined" do
|
|
26
|
+
let(:finder_fields) { [] }
|
|
27
|
+
|
|
28
|
+
it "never considers records to be duplicates" do
|
|
29
|
+
duplicates.check_record!(foo: 3, bar: 4)
|
|
30
|
+
expect { duplicates.check_record!(foo: 3, bar: 4) }.to_not raise_error
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context "when a finder field is defined" do
|
|
35
|
+
let(:finder_fields) { [[:foo]] }
|
|
36
|
+
|
|
37
|
+
it "allows different values to be set without any errors" do
|
|
38
|
+
duplicates.check_record!(foo: 3, bar: 4)
|
|
39
|
+
expect { duplicates.check_record!(foo: 4, bar: 4) }.to_not raise_error
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
context "when multiple finder field groups are defined" do
|
|
44
|
+
let(:finder_fields) { [[:foo, :bar], [:bar, :baz], [:qux]] }
|
|
45
|
+
|
|
46
|
+
it "records and matches against all field groups" do
|
|
47
|
+
duplicates.check_record!(foo: 1, bar: 1, baz: 1, qux: 1)
|
|
48
|
+
|
|
49
|
+
# Each field group has a unique set of values from previous check
|
|
50
|
+
duplicates.check_record!(foo: 1, bar: 2, baz: 1, qux: 2)
|
|
51
|
+
|
|
52
|
+
# The second field group conflicts with values from previous check
|
|
53
|
+
expect { duplicates.check_record!(foo: 2, bar: 2, baz: 1, qux: 3) }
|
|
54
|
+
.to raise_error(Importu::DuplicateRecord)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "adds all non-duplicate field groups, even if exception is raised" do
|
|
58
|
+
duplicates.check_record!(foo: 1, bar: 1, baz: 1, qux: 1)
|
|
59
|
+
|
|
60
|
+
# Only first field group is duplicate, but others should still get added
|
|
61
|
+
expect { duplicates.check_record!(foo: 1, bar: 1, baz: 2, qux: 2) }
|
|
62
|
+
.to raise_error(Importu::DuplicateRecord)
|
|
63
|
+
|
|
64
|
+
expect { duplicates.check_record!(bar: 1, baz: 2) }
|
|
65
|
+
.to raise_error(Importu::DuplicateRecord)
|
|
66
|
+
expect { duplicates.check_record!(qux: 2) }
|
|
67
|
+
.to raise_error(Importu::DuplicateRecord)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context "when a proc-based finder field is defined" do
|
|
72
|
+
let(:finder_fields) { [Proc.new { raise :bleep }, [:foo]] }
|
|
73
|
+
|
|
74
|
+
it "ignores proc-based field groups" do
|
|
75
|
+
duplicates.check_record!(foo: 3, bar: 4)
|
|
76
|
+
duplicates.check_record!(foo: 4, bar: 4)
|
|
77
|
+
expect { duplicates.check_record!(foo: 3, bar: 5) }
|
|
78
|
+
.to raise_error(Importu::DuplicateRecord)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
context "when a field in a finder field group is does not exist on the record" do
|
|
83
|
+
let(:finder_fields) { [[:foo, :baz]] }
|
|
84
|
+
|
|
85
|
+
it "ignores the field group when doing duplicate checking" do
|
|
86
|
+
duplicates.check_record!(foo: 3, bar: 4)
|
|
87
|
+
expect { duplicates.check_record!(foo: 3, bar: 4) }.to_not raise_error
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
require "importu/exceptions"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Importu::ImportuException do
|
|
4
7
|
subject(:exception) { Importu::ImportuException.new }
|
|
5
8
|
|
|
6
9
|
it "#name should return 'ImportuException" do
|
|
7
|
-
exception.name.
|
|
10
|
+
expect(exception.name).to eq "ImportuException"
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
describe Importu::InvalidInput do
|
|
@@ -15,7 +18,7 @@ describe Importu::ImportuException do
|
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
it "#name should return 'InvalidInput'" do
|
|
18
|
-
exception.name.
|
|
21
|
+
expect(exception.name).to eq "InvalidInput"
|
|
19
22
|
end
|
|
20
23
|
end
|
|
21
24
|
|
|
@@ -27,24 +30,48 @@ describe Importu::ImportuException do
|
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
it "#name should return 'InvalidRecord'" do
|
|
30
|
-
exception.name.
|
|
33
|
+
expect(exception.name).to eq "InvalidRecord"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe "#normalized_message" do
|
|
37
|
+
context "when not provided" do
|
|
38
|
+
it "returns nil" do
|
|
39
|
+
expect(exception.normalized_message).to be_nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
context "when provided" do
|
|
44
|
+
subject(:exception) do
|
|
45
|
+
Importu::InvalidRecord.new(
|
|
46
|
+
"isbn too short (was: abc)", nil, normalized_message: "isbn too short"
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "returns the normalized message" do
|
|
51
|
+
expect(exception.normalized_message).to eq "isbn too short"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "preserves the full message" do
|
|
55
|
+
expect(exception.message).to eq "isbn too short (was: abc)"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
31
58
|
end
|
|
32
59
|
end
|
|
33
60
|
|
|
34
61
|
describe Importu::FieldParseError do
|
|
35
|
-
subject(:exception) { Importu::FieldParseError.new }
|
|
62
|
+
subject(:exception) { Importu::FieldParseError.new(:foo, "is invalid") }
|
|
36
63
|
|
|
37
64
|
it "should be a subclass of Importu::InvalidRecord" do
|
|
38
65
|
exception.class.ancestors.include?(Importu::InvalidRecord)
|
|
39
66
|
end
|
|
40
67
|
|
|
41
68
|
it "#name should return 'FieldParseError'" do
|
|
42
|
-
exception.name.
|
|
69
|
+
expect(exception.name).to eq "FieldParseError"
|
|
43
70
|
end
|
|
44
71
|
end
|
|
45
72
|
|
|
46
73
|
describe Importu::MissingField do
|
|
47
|
-
let(:definition) { { :
|
|
74
|
+
let(:definition) { { name: "foo_field_1", label: "Field 1" } }
|
|
48
75
|
subject(:exception) { Importu::MissingField.new(definition) }
|
|
49
76
|
|
|
50
77
|
it "should be a subclass of Importu::InvalidRecord" do
|
|
@@ -52,30 +79,56 @@ describe Importu::ImportuException do
|
|
|
52
79
|
end
|
|
53
80
|
|
|
54
81
|
it "#name should return 'MissingField'" do
|
|
55
|
-
exception.name.
|
|
82
|
+
expect(exception.name).to eq "MissingField"
|
|
56
83
|
end
|
|
57
84
|
|
|
58
85
|
it "#definition should return the definition passed during construction" do
|
|
59
|
-
exception.definition.
|
|
86
|
+
expect(exception.definition).to eq definition
|
|
60
87
|
end
|
|
61
88
|
|
|
62
89
|
describe "#message" do
|
|
63
90
|
it "should mention a missing field" do
|
|
64
|
-
exception.message.
|
|
91
|
+
expect(exception.message).to match(/missing field/i)
|
|
65
92
|
end
|
|
66
93
|
|
|
67
94
|
context "field definition has a label" do
|
|
68
|
-
let(:definition) { { :
|
|
95
|
+
let(:definition) { { label: "Field 2" } }
|
|
69
96
|
it "mentions missing field's label" do
|
|
70
|
-
exception.message.
|
|
97
|
+
expect(exception.message).to match(/Field 2/)
|
|
71
98
|
end
|
|
72
99
|
end
|
|
73
100
|
|
|
74
101
|
context "field definition is missing a label" do
|
|
75
|
-
let(:definition) { { :
|
|
102
|
+
let(:definition) { { name: "foo_field_2" } }
|
|
76
103
|
|
|
77
104
|
it "mentions missing field's name" do
|
|
78
|
-
exception.message.
|
|
105
|
+
expect(exception.message).to match(/foo_field_2/)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
context "when available_fields is provided" do
|
|
110
|
+
subject(:exception) do
|
|
111
|
+
Importu::MissingField.new(definition, available_fields: ["name", "email", "phone"])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "lists available fields" do
|
|
115
|
+
expect(exception.message).to match(/available fields: name, email, phone/)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
context "when available_fields is empty" do
|
|
120
|
+
subject(:exception) do
|
|
121
|
+
Importu::MissingField.new(definition, available_fields: [])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "does not include available fields section" do
|
|
125
|
+
expect(exception.message).not_to match(/available fields/)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
context "when available_fields is not provided" do
|
|
130
|
+
it "does not include available fields section" do
|
|
131
|
+
expect(exception.message).not_to match(/available fields/)
|
|
79
132
|
end
|
|
80
133
|
end
|
|
81
134
|
end
|
|
@@ -89,7 +142,7 @@ describe Importu::ImportuException do
|
|
|
89
142
|
end
|
|
90
143
|
|
|
91
144
|
it "#name should return 'DuplicateRecord'" do
|
|
92
|
-
exception.name.
|
|
145
|
+
expect(exception.name).to eq "DuplicateRecord"
|
|
93
146
|
end
|
|
94
147
|
end
|
|
95
148
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
3
|
+
|
|
4
|
+
require "importu/converter_context"
|
|
5
|
+
require "importu/definition"
|
|
6
|
+
require "importu/exceptions"
|
|
7
|
+
|
|
8
|
+
RSpec.describe Importu::ConverterContext do
|
|
9
|
+
subject(:context) do
|
|
10
|
+
described_class.with_config(**definition.config).new(data)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
let(:definition) { Class.new(Importu::Definition) }
|
|
14
|
+
|
|
15
|
+
let(:data) { {} }
|
|
16
|
+
|
|
17
|
+
describe "#field_value" do
|
|
18
|
+
let(:definition) do
|
|
19
|
+
Class.new(super()) do
|
|
20
|
+
field :field1, required: true, &convert_to(:integer)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
let(:data) { { "field1" => "73" } }
|
|
25
|
+
|
|
26
|
+
it "returns the field value after applying the converter" do
|
|
27
|
+
expect(context.field_value(:field1)).to eq 73
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context "when a definition doesn't exist for the field" do
|
|
31
|
+
it "raises an InvalidDefinition error" do
|
|
32
|
+
expect { context.field_value(:nonexistent_field) }
|
|
33
|
+
.to raise_error(Importu::InvalidDefinition)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context "when the converted value is nil" do
|
|
38
|
+
let(:data) { { "field1" => nil } }
|
|
39
|
+
|
|
40
|
+
context "and the field is required" do
|
|
41
|
+
let(:definition) { Class.new(super()) { field :field1, required: true } }
|
|
42
|
+
|
|
43
|
+
it "raises a MissingField error" do
|
|
44
|
+
expect { context.field_value(:field1) }
|
|
45
|
+
.to raise_error(Importu::MissingField)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context "and a default is defined on the field" do
|
|
49
|
+
let(:definition) { Class.new(super()) { field :field1, default: :beep } }
|
|
50
|
+
|
|
51
|
+
it "raises a MissingField error" do
|
|
52
|
+
expect { context.field_value(:field1) }
|
|
53
|
+
.to raise_error(Importu::MissingField)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
context "and the field is not required" do
|
|
59
|
+
let(:definition) { Class.new(super()) { field :field1, required: false } }
|
|
60
|
+
|
|
61
|
+
it "returns nil" do
|
|
62
|
+
expect(context.field_value(:field1)).to be nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
context "annd a default is defined on the field" do
|
|
66
|
+
let(:definition) { Class.new(super()) { field :field1, default: :beep } }
|
|
67
|
+
|
|
68
|
+
it "returns the default value" do
|
|
69
|
+
expect(context.field_value(:field1)).to eq :beep
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context "when converter raises an ArgumentError" do
|
|
76
|
+
let(:definition) do
|
|
77
|
+
Class.new(super()) do
|
|
78
|
+
converter(:ash) {|*| raise ArgumentError, "sawdust" }
|
|
79
|
+
field :field1, &convert_to(:ash)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "raises a FieldParseError error" do
|
|
84
|
+
expect { context.field_value(:field1) }
|
|
85
|
+
.to raise_error(Importu::FieldParseError)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "includes the original error message" do
|
|
89
|
+
expect { context.field_value(:field1) }
|
|
90
|
+
.to raise_error(Importu::FieldParseError)
|
|
91
|
+
.with_message(/sawdust/)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
context "when converter raises an unexpected error" do
|
|
96
|
+
let(:definition) do
|
|
97
|
+
Class.new(super()) do
|
|
98
|
+
converter(:rubble) {|*| raise StandardError, "you did what?!" }
|
|
99
|
+
field :field1, &convert_to(:rubble)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "raises the unexpected exception" do
|
|
104
|
+
expect { context.field_value(:field1) }
|
|
105
|
+
.to raise_error(StandardError)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe "field cross-references" do
|
|
111
|
+
context "when a field converter references another field" do
|
|
112
|
+
let(:definition) do
|
|
113
|
+
Class.new(super()) do
|
|
114
|
+
field :first_name
|
|
115
|
+
field :last_name
|
|
116
|
+
field :full_name, abstract: true do
|
|
117
|
+
"#{field_value(:first_name)} #{field_value(:last_name)}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
let(:data) { { "first_name" => "Arthur", "last_name" => "Dent" } }
|
|
123
|
+
|
|
124
|
+
it "can access other field values" do
|
|
125
|
+
expect(context.field_value(:full_name)).to eq "Arthur Dent"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
context "when a field references a field with a converter" do
|
|
130
|
+
let(:definition) do
|
|
131
|
+
Class.new(super()) do
|
|
132
|
+
field :price, &convert_to(:decimal)
|
|
133
|
+
field :quantity, &convert_to(:integer)
|
|
134
|
+
field :total, abstract: true do
|
|
135
|
+
field_value(:price) * field_value(:quantity)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
let(:data) { { "price" => "19.99", "quantity" => "3" } }
|
|
141
|
+
|
|
142
|
+
it "receives the converted value, not raw" do
|
|
143
|
+
expect(context.field_value(:total)).to eq BigDecimal("59.97")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
context "when a referenced field is missing" do
|
|
148
|
+
let(:definition) do
|
|
149
|
+
Class.new(super()) do
|
|
150
|
+
field :first_name
|
|
151
|
+
field :last_name
|
|
152
|
+
field :full_name, abstract: true do
|
|
153
|
+
"#{field_value(:first_name)} #{field_value(:last_name)}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
let(:data) { { "first_name" => "Arthur" } }
|
|
159
|
+
|
|
160
|
+
it "raises MissingField for the referenced field" do
|
|
161
|
+
expect { context.field_value(:full_name) }
|
|
162
|
+
.to raise_error(Importu::MissingField)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe "#raw_value" do
|
|
168
|
+
let(:definition) { Class.new(super()) { field :field1 } }
|
|
169
|
+
let(:data) { { "field1" => "zippy" } }
|
|
170
|
+
|
|
171
|
+
it "returns the data associated with the field" do
|
|
172
|
+
expect(context.raw_value(:field1)).to eq data["field1"]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
context "when a definition doesn't exist for the field" do
|
|
176
|
+
it "raises an InvalidDefinition error" do
|
|
177
|
+
expect { context.raw_value(:nonexistent_field) }
|
|
178
|
+
.to raise_error(Importu::InvalidDefinition)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
context "when data is nil for the field" do
|
|
183
|
+
let(:data) { { "field1" => nil } }
|
|
184
|
+
|
|
185
|
+
it "returns nil" do
|
|
186
|
+
expect(context.raw_value(:field1)).to be nil
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
context "when the field does not exist in data hash" do
|
|
191
|
+
let(:data) { {} }
|
|
192
|
+
|
|
193
|
+
it "returns nil" do
|
|
194
|
+
expect(context.raw_value(:field1)).to be nil
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
end
|