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,277 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Records and aggregates results from an import.
|
|
4
|
+
#
|
|
5
|
+
# The Summary tracks counts for each result type (created, updated, unchanged,
|
|
6
|
+
# invalid) and collects validation errors for reporting.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# summary = importer.import!
|
|
10
|
+
#
|
|
11
|
+
# puts "Processed #{summary.total} records"
|
|
12
|
+
# puts "Created: #{summary.created}, Updated: #{summary.updated}"
|
|
13
|
+
# puts "Failed: #{summary.invalid}"
|
|
14
|
+
#
|
|
15
|
+
# @example Checking for errors
|
|
16
|
+
# if summary.invalid > 0
|
|
17
|
+
# puts "Errors encountered:"
|
|
18
|
+
# summary.validation_errors.each do |message, count|
|
|
19
|
+
# puts " #{message}: #{count} occurrences"
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example Getting detailed errors by record
|
|
24
|
+
# summary.itemized_errors.each do |index, errors|
|
|
25
|
+
# puts "Record #{index} failed: #{errors.map(&:to_s).join(', ')}"
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example Human-readable output
|
|
29
|
+
# puts summary.result_msg
|
|
30
|
+
#
|
|
31
|
+
# @example Machine-readable output (for JSON APIs, etc.)
|
|
32
|
+
# summary.to_hash # => { created: 5, updated: 2, ... }
|
|
33
|
+
#
|
|
34
|
+
# @see Importu::Importer#import!
|
|
35
|
+
# @api public
|
|
36
|
+
class Importu::Summary
|
|
37
|
+
|
|
38
|
+
# The number of times a :created result was recorded
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# summary.created # => 2
|
|
42
|
+
#
|
|
43
|
+
# @return [Integer]
|
|
44
|
+
#
|
|
45
|
+
# @api public
|
|
46
|
+
attr_reader :created
|
|
47
|
+
|
|
48
|
+
# The number of times an :invalid result was recorded
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# summary.invalid # => 3
|
|
52
|
+
#
|
|
53
|
+
# @return [Integer]
|
|
54
|
+
#
|
|
55
|
+
# @api public
|
|
56
|
+
attr_reader :invalid
|
|
57
|
+
|
|
58
|
+
# A hash of record indexes and the errors recorded for that record
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# summary.itemized_errors
|
|
62
|
+
#
|
|
63
|
+
# {
|
|
64
|
+
# 0 => [
|
|
65
|
+
# #<Importu::InvalidRecord: ...>,
|
|
66
|
+
# #<Importu::InvalidRecord: ...>,
|
|
67
|
+
# ],
|
|
68
|
+
# 3 => [
|
|
69
|
+
# #<Importu::InvalidRecord: ...>
|
|
70
|
+
# ]
|
|
71
|
+
# }
|
|
72
|
+
#
|
|
73
|
+
# @return [Hash<Integer=>Array>] a hash of record indexes and the errors
|
|
74
|
+
# recorded for that record. Each key represents the position of the
|
|
75
|
+
# record in the source data and the value is a list of errors.
|
|
76
|
+
# Errors will all be Importu::InvalidRecord exceptions or subclasses
|
|
77
|
+
# that can be converted to a string using #to_s.
|
|
78
|
+
#
|
|
79
|
+
# @api public
|
|
80
|
+
attr_reader :itemized_errors
|
|
81
|
+
|
|
82
|
+
# The total number of times any result was recorded
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# summary.total # => 36
|
|
86
|
+
#
|
|
87
|
+
# @return [Integer]
|
|
88
|
+
#
|
|
89
|
+
# @api public
|
|
90
|
+
attr_reader :total
|
|
91
|
+
|
|
92
|
+
# The number of times an :unchanged result was recorded
|
|
93
|
+
#
|
|
94
|
+
# @example
|
|
95
|
+
# summary.unchanged # => 30
|
|
96
|
+
#
|
|
97
|
+
# @return [Integer]
|
|
98
|
+
#
|
|
99
|
+
# @api public
|
|
100
|
+
attr_reader :unchanged
|
|
101
|
+
|
|
102
|
+
# The number of times an :updated result was recorded
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# summary.updated # => 1
|
|
106
|
+
#
|
|
107
|
+
# @return [Integer]
|
|
108
|
+
#
|
|
109
|
+
# @api public
|
|
110
|
+
attr_reader :updated
|
|
111
|
+
|
|
112
|
+
# A hash of error messages and the number of occurrences of each
|
|
113
|
+
#
|
|
114
|
+
# @example
|
|
115
|
+
# summary.validation_errors
|
|
116
|
+
#
|
|
117
|
+
# {
|
|
118
|
+
# "description is required" => 3,
|
|
119
|
+
# "title is too long" => 2
|
|
120
|
+
# }
|
|
121
|
+
#
|
|
122
|
+
# @return [Hash<String=>Integer>] a hash of error messages, with each key
|
|
123
|
+
# being an error message and the value representing the number of times
|
|
124
|
+
# the error was recorded.
|
|
125
|
+
#
|
|
126
|
+
# @api public
|
|
127
|
+
attr_reader :validation_errors
|
|
128
|
+
|
|
129
|
+
# Creates a new instance of a summary. Generally, a new summary object
|
|
130
|
+
# would get created on each attempt to import data.
|
|
131
|
+
#
|
|
132
|
+
# @example
|
|
133
|
+
# Summary.new # => Importu::Summary.new
|
|
134
|
+
#
|
|
135
|
+
# @api semipublic
|
|
136
|
+
def initialize
|
|
137
|
+
@total = @invalid = @created = @updated = @unchanged = 0
|
|
138
|
+
@validation_errors = Hash.new(0) # counter for each validation error
|
|
139
|
+
|
|
140
|
+
# Sparse array of error messages grouped by the index of the record.
|
|
141
|
+
# Should stay ordered by index because rows are processed sequentially
|
|
142
|
+
# and hashes preserve insertion order. Recorded errors without an index
|
|
143
|
+
# will be ignored. Index is 0-based from first record.
|
|
144
|
+
@itemized_errors = Hash.new {|h, idx| h[idx] = [] }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Record the result of an import. The result may be used for aggregated
|
|
148
|
+
# statistics or, in the case of errors, a way to retrieve error messages
|
|
149
|
+
# associated with a record after the import has completed.
|
|
150
|
+
#
|
|
151
|
+
# @example
|
|
152
|
+
# summary.record(:created, index: 4)
|
|
153
|
+
# summary.record(:unchanged, index: 9)
|
|
154
|
+
# summary.record(:invalid, index: 17, errors: [
|
|
155
|
+
# Importu::InvalidRecord.new("contains non utf8 characters")
|
|
156
|
+
# ])
|
|
157
|
+
#
|
|
158
|
+
# @param result [:created, :invalid, :unchanged, :updated] the result of
|
|
159
|
+
# trying to import the record.
|
|
160
|
+
# @param index [Integer] A zero-indexed position of the record relative
|
|
161
|
+
# to where it was read from the source data.
|
|
162
|
+
# @param errors [Array<Importu::InvalidRecord>] A list of errors
|
|
163
|
+
# encountered while converting or importing the record.
|
|
164
|
+
# @return [void]
|
|
165
|
+
#
|
|
166
|
+
# @api semipublic
|
|
167
|
+
def record(result, index: nil, errors: [])
|
|
168
|
+
@total += 1
|
|
169
|
+
|
|
170
|
+
case result
|
|
171
|
+
when :created then @created += 1
|
|
172
|
+
when :updated then @updated += 1
|
|
173
|
+
when :unchanged then @unchanged += 1
|
|
174
|
+
when :invalid
|
|
175
|
+
@invalid += 1
|
|
176
|
+
record_errors(errors, index: index)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# An aggregated summary of results meant for human consumption, such
|
|
181
|
+
# as displaying in a terminal window. If any errors were encountered
|
|
182
|
+
# during the import, an aggregated list of error messages and the
|
|
183
|
+
# number of times each error was encountered will also be included.
|
|
184
|
+
#
|
|
185
|
+
# @example
|
|
186
|
+
# puts summary.result_msg
|
|
187
|
+
#
|
|
188
|
+
# Total: 36
|
|
189
|
+
# Created: 2
|
|
190
|
+
# Updated: 1
|
|
191
|
+
# Invalid: 3
|
|
192
|
+
# Unchanged: 30
|
|
193
|
+
#
|
|
194
|
+
# Validation Errors:
|
|
195
|
+
# - description is required: 3
|
|
196
|
+
# - title is too long: 2
|
|
197
|
+
#
|
|
198
|
+
# @return [String] a human-readable aggregate summary of results suitable
|
|
199
|
+
# for displaying in a terminal window.
|
|
200
|
+
#
|
|
201
|
+
# @api public
|
|
202
|
+
def result_msg
|
|
203
|
+
msg = <<-END.gsub(/^\s*/, "")
|
|
204
|
+
Total: #{total}
|
|
205
|
+
Created: #{created}
|
|
206
|
+
Updated: #{updated}
|
|
207
|
+
Invalid: #{invalid}
|
|
208
|
+
Unchanged: #{unchanged}
|
|
209
|
+
END
|
|
210
|
+
|
|
211
|
+
if validation_errors.any?
|
|
212
|
+
msg << "\nValidation Errors:\n"
|
|
213
|
+
msg << validation_errors.map {|e, c| " - #{e}: #{c}" }.join("\n")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
msg
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# An aggregated summary of results that can be used by a custom formatter,
|
|
220
|
+
# or for any purpose by software interacting with this gem.
|
|
221
|
+
#
|
|
222
|
+
# @example
|
|
223
|
+
# summary.to_hash
|
|
224
|
+
#
|
|
225
|
+
# {
|
|
226
|
+
# :created => 2,
|
|
227
|
+
# :invalid => 3,
|
|
228
|
+
# :total => 36,
|
|
229
|
+
# :unchanged => 30,
|
|
230
|
+
# :updated => 1,
|
|
231
|
+
# :validation_errors => {
|
|
232
|
+
# "description is required" => 3,
|
|
233
|
+
# "title is too long" => 2
|
|
234
|
+
# }
|
|
235
|
+
# }
|
|
236
|
+
#
|
|
237
|
+
# @return [Hash<Symbol=>Integer,Hash>] a hash of
|
|
238
|
+
# aggregated results. Top-level keys are always symbols with all values
|
|
239
|
+
# being integers except for :validation_errors; :validation_errors
|
|
240
|
+
# will always contain a nested hash of error messages and their counts,
|
|
241
|
+
# with each error message keys being represented as a string.
|
|
242
|
+
#
|
|
243
|
+
# @api public
|
|
244
|
+
def to_hash
|
|
245
|
+
{
|
|
246
|
+
created: created,
|
|
247
|
+
invalid: invalid,
|
|
248
|
+
total: total,
|
|
249
|
+
unchanged: unchanged,
|
|
250
|
+
updated: updated,
|
|
251
|
+
validation_errors: validation_errors,
|
|
252
|
+
}
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
alias to_s result_msg
|
|
256
|
+
|
|
257
|
+
# Updates error attributes to include the newly encountered errors.
|
|
258
|
+
#
|
|
259
|
+
# @return [void]
|
|
260
|
+
#
|
|
261
|
+
# @api private
|
|
262
|
+
private def record_errors(errors, index: nil)
|
|
263
|
+
errors.each do |error|
|
|
264
|
+
# Use normalized_message if available (set by backends like ActiveRecord),
|
|
265
|
+
# otherwise fall back to stripping trailing parenthesized data for aggregation.
|
|
266
|
+
has_normalized = error.respond_to?(:normalized_message) && error.normalized_message
|
|
267
|
+
normalized_error = has_normalized \
|
|
268
|
+
? error.normalized_message
|
|
269
|
+
: error.to_s.gsub(/ *\([^)]+\) *$/, "")
|
|
270
|
+
|
|
271
|
+
@validation_errors[normalized_error] += 1
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
@itemized_errors[index] += errors if index
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
end
|
data/lib/importu/version.rb
CHANGED
data/lib/importu.rb
CHANGED
|
@@ -1,12 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Importu is a Ruby gem for declarative data import.
|
|
4
|
+
#
|
|
5
|
+
# Define importers as readable contracts with fields, converters, and rules.
|
|
6
|
+
# The gem handles parsing, converting, finding/creating records, and reporting.
|
|
7
|
+
#
|
|
8
|
+
# ## Quick Start
|
|
9
|
+
#
|
|
10
|
+
# require "importu"
|
|
11
|
+
#
|
|
12
|
+
# class BookImporter < Importu::Importer
|
|
13
|
+
# fields :title, :author, :isbn
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# source = Importu::Sources::CSV.new("books.csv")
|
|
17
|
+
# importer = BookImporter.new(source)
|
|
18
|
+
# importer.records.each { |record| puts record[:title] }
|
|
19
|
+
#
|
|
20
|
+
# ## Key Classes
|
|
21
|
+
#
|
|
22
|
+
# - {Importu::Importer} - Base class for defining importers
|
|
23
|
+
# - {Importu::Sources::CSV} - Parse CSV files
|
|
24
|
+
# - {Importu::Sources::JSON} - Parse JSON files
|
|
25
|
+
# - {Importu::Sources::XML} - Parse XML files
|
|
26
|
+
# - {Importu::Summary} - Import results and error reporting
|
|
27
|
+
# - {Importu::Record} - Individual record from source data
|
|
28
|
+
#
|
|
29
|
+
# ## DSL Reference
|
|
30
|
+
#
|
|
31
|
+
# See {Importu::ConfigDSL} for all importer configuration options:
|
|
32
|
+
# fields, converters, model, find_by, allow_actions, and more.
|
|
33
|
+
#
|
|
34
|
+
# ## Built-in Converters
|
|
35
|
+
#
|
|
36
|
+
# See {Importu::Converters} for type conversion: boolean, date, datetime,
|
|
37
|
+
# decimal, float, integer, string, trimmed.
|
|
38
|
+
#
|
|
39
|
+
# @api public
|
|
1
40
|
module Importu; end
|
|
2
41
|
|
|
3
|
-
require
|
|
42
|
+
require "importu/importer"
|
|
43
|
+
|
|
44
|
+
require "importu/sources/csv"
|
|
45
|
+
require "importu/sources/json"
|
|
46
|
+
require "importu/sources/xml"
|
|
4
47
|
|
|
5
|
-
require
|
|
6
|
-
require 'importu/exceptions'
|
|
7
|
-
require 'importu/converters'
|
|
8
|
-
require 'importu/record'
|
|
9
|
-
require 'importu/importer'
|
|
10
|
-
require 'importu/importer/csv'
|
|
11
|
-
require 'importu/importer/json'
|
|
12
|
-
require 'importu/importer/xml'
|
|
48
|
+
require "importu/backends/active_record" if defined?(::ActiveRecord)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Contains the original three records from the "books-valid" fixture, along with
|
|
2
|
+
three additional records that share the same isbn10 value as valid records. The
|
|
3
|
+
additional records are valid, but they appear in the same source file as other
|
|
4
|
+
records sharing the same `find_by` key. Because they share the same values used
|
|
5
|
+
to find the object, they will find an object with the same :id used earlier in
|
|
6
|
+
the import. If an object's id matches an id used earlier in the import, the
|
|
7
|
+
record is considered to be a duplicate.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"isbn10","title","author","release_date","pages"
|
|
2
|
+
"0596516177","The Ruby Programming Language","David Flanagan and Yukihiro Matsumoto","Feb 1, 2008","0448"
|
|
3
|
+
"1449355978","Computer Science Programming Basics in Ruby","Ophir Frieder, Gideon Frieder and David Grossman","1 May, 2013","188"
|
|
4
|
+
" 0596523696 "," Ruby Cookbook "," Lucas Carlson and Leonard Richardson "," 2006-7-26 "," 910 "
|
|
5
|
+
"0596516177","Duplicate/conflicting record 1","An author","2017-01-01","1"
|
|
6
|
+
"0596516177","Duplicate/conflicting record 2","An author","2017-01-01","1"
|
|
7
|
+
"1449355978","Duplicate/conflicting record 3","An author","2017-01-01","1"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"isbn10": "0596516177",
|
|
4
|
+
"title": "The Ruby Programming Language",
|
|
5
|
+
"authors": ["David Flanagan", "Yukihiro Matsumoto"],
|
|
6
|
+
"release_date": "2008-02-01",
|
|
7
|
+
"pages": 448
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"isbn10": "1449355978",
|
|
11
|
+
"title": "Computer Science Programming Basics in Ruby",
|
|
12
|
+
"authors": ["Ophir Frieder", "Gideon Frieder", "David Grossman"],
|
|
13
|
+
"release_date": "2013-05-01",
|
|
14
|
+
"pages": 188
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"isbn10": "0596523696",
|
|
18
|
+
"title": "Ruby Cookbook",
|
|
19
|
+
"authors": ["Lucas Carlson", "Leonard Richardson"],
|
|
20
|
+
"release_date": "2006-07-26",
|
|
21
|
+
"pages": 910
|
|
22
|
+
}
|
|
23
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Contains three records that are compatible with the `BookImporter`. They
|
|
2
|
+
should all be created successfully on import.
|
|
3
|
+
|
|
4
|
+
The following anomalies exist in the data:
|
|
5
|
+
|
|
6
|
+
| Record | Field | Formats | Value | Description |
|
|
7
|
+
| _all_ | _all_ | xml | n/a | mixed xml attribute-element styles, should treat similarly |
|
|
8
|
+
| 1 | release\_date | _all_ | "Feb 1, 2008" | should detect date format |
|
|
9
|
+
| 1 | pages | _all_ | "0448" | leading 0 should parse as decimal not octal |
|
|
10
|
+
| 2 | release\_date | _all_ | "1 May, 2013" | should detect date format |
|
|
11
|
+
| 2 | pages | json | 188 | should handle value already being an integer |
|
|
12
|
+
| 3 | _all_ | _all_ | " value " | should trim surrounding whitespace |
|
|
13
|
+
| 3 | release\_date | _all_ | " 2006-7-26 " | should handle missing leading '0' in date |
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"isbn10","title","author","release_date","pages"
|
|
2
|
+
"0596516177","The Ruby Programming Language","David Flanagan and Yukihiro Matsumoto","Feb 1, 2008","0448"
|
|
3
|
+
"1449355978","Computer Science Programming Basics in Ruby","Ophir Frieder, Gideon Frieder and David Grossman","1 May, 2013","188"
|
|
4
|
+
" 0596523696 "," Ruby Cookbook "," Lucas Carlson and Leonard Richardson "," 2006-7-26 "," 910 "
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"isbn10": "0596516177",
|
|
4
|
+
"title": "The Ruby Programming Language",
|
|
5
|
+
"author": "David Flanagan and Yukihiro Matsumoto",
|
|
6
|
+
"release_date": "Feb 1, 2008",
|
|
7
|
+
"pages": "0448"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"isbn10": "1449355978",
|
|
11
|
+
"title": "Computer Science Programming Basics in Ruby",
|
|
12
|
+
"author": "Ophir Frieder, Gideon Frieder and David Grossman",
|
|
13
|
+
"release_date": "1 May, 2013",
|
|
14
|
+
"pages": 188
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"isbn10": " 0596523696 ",
|
|
18
|
+
"title": " Ruby Cookbook ",
|
|
19
|
+
"author": " Lucas Carlson and Leonard Richardson ",
|
|
20
|
+
"release_date": " 2006-7-26 ",
|
|
21
|
+
"pages": " 910 "
|
|
22
|
+
}
|
|
23
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
|
|
3
|
+
<books>
|
|
4
|
+
<!-- attribute-style record -->
|
|
5
|
+
<book isbn10="0596516177" title="The Ruby Programming Language" author="David Flanagan and Yukihiro Matsumoto" release_date="Feb 1, 2008" pages="0448" />
|
|
6
|
+
|
|
7
|
+
<!-- element style record -->
|
|
8
|
+
<book>
|
|
9
|
+
<isbn10>1449355978</isbn10>
|
|
10
|
+
<title>Computer Science Programming Basics in Ruby</title>
|
|
11
|
+
<author>Ophir Frieder, Gideon Frieder and David Grossman</author>
|
|
12
|
+
<release_date>1 May, 2013</release_date>
|
|
13
|
+
<pages>188</pages>
|
|
14
|
+
</book>
|
|
15
|
+
|
|
16
|
+
<!-- mixed style record -->
|
|
17
|
+
<book isbn10=" 0596523696 " title=" Ruby Cookbook " author=" Lucas Carlson and Leonard Richardson ">
|
|
18
|
+
<release_date> 2006-7-26 </release_date>
|
|
19
|
+
<pages> 910 </pages>
|
|
20
|
+
</book>
|
|
21
|
+
</books>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"isbn10": "0596516177",
|
|
4
|
+
"title": "The Ruby Programming Language",
|
|
5
|
+
"authors": ["David Flanagan", "Yukihiro Matsumoto"],
|
|
6
|
+
"release_date": "2008-02-01",
|
|
7
|
+
"pages": 448
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"isbn10": "1449355978",
|
|
11
|
+
"title": "Computer Science Programming Basics in Ruby",
|
|
12
|
+
"authors": ["Ophir Frieder", "Gideon Frieder", "David Grossman"],
|
|
13
|
+
"release_date": "2013-05-01",
|
|
14
|
+
"pages": 188
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"isbn10": "0596523696",
|
|
18
|
+
"title": "Ruby Cookbook",
|
|
19
|
+
"authors": ["Lucas Carlson", "Leonard Richardson"],
|
|
20
|
+
"release_date": "2006-07-26",
|
|
21
|
+
"pages": 910
|
|
22
|
+
}
|
|
23
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"isbn10": "0596516177",
|
|
4
|
+
"title": "The Ruby Programming Language",
|
|
5
|
+
"authors": ["David Flanagan", "Yukihiro Matsumoto"],
|
|
6
|
+
"release_date": "2008-02-01",
|
|
7
|
+
"pages": 448,
|
|
8
|
+
"by_matz": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"isbn10": "1449355978",
|
|
12
|
+
"title": "Computer Science Programming Basics in Ruby",
|
|
13
|
+
"authors": ["Ophir Frieder", "Gideon Frieder", "David Grossman"],
|
|
14
|
+
"release_date": "2013-05-01",
|
|
15
|
+
"pages": 188,
|
|
16
|
+
"by_matz": false
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"isbn10": "0596523696",
|
|
20
|
+
"title": "Ruby Cookbook",
|
|
21
|
+
"authors": ["Lucas Carlson", "Leonard Richardson"],
|
|
22
|
+
"release_date": "2006-07-26",
|
|
23
|
+
"pages": 910,
|
|
24
|
+
"by_matz": false
|
|
25
|
+
}
|
|
26
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{},{}]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"isbn10","title","author
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{},[],{
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"isbn10","title","author","release_date","pages"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|