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.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +15 -0
  3. data/.github/workflows/ci.yml +48 -0
  4. data/.gitignore +4 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +311 -0
  7. data/.simplecov +14 -0
  8. data/.yardstick.yml +36 -0
  9. data/Appraisals +22 -0
  10. data/CHANGELOG.md +51 -0
  11. data/CONTRIBUTING.md +86 -0
  12. data/Gemfile +5 -1
  13. data/LICENSE +21 -0
  14. data/README.md +435 -52
  15. data/Rakefile +71 -0
  16. data/UPGRADING.md +188 -0
  17. data/gemfiles/rails_7_2.gemfile +11 -0
  18. data/gemfiles/rails_7_2.gemfile.lock +268 -0
  19. data/gemfiles/rails_8_0.gemfile +11 -0
  20. data/gemfiles/rails_8_0.gemfile.lock +271 -0
  21. data/gemfiles/rails_8_1.gemfile +11 -0
  22. data/gemfiles/rails_8_1.gemfile.lock +269 -0
  23. data/gemfiles/standalone.gemfile +8 -0
  24. data/gemfiles/standalone.gemfile.lock +197 -0
  25. data/importu.gemspec +41 -22
  26. data/lib/importu/backends/active_record.rb +171 -0
  27. data/lib/importu/backends/middleware/duplicate_manager_proxy.rb +41 -0
  28. data/lib/importu/backends/middleware/enforce_allowed_actions.rb +52 -0
  29. data/lib/importu/backends/middleware.rb +11 -0
  30. data/lib/importu/backends.rb +103 -0
  31. data/lib/importu/config_dsl.rb +381 -0
  32. data/lib/importu/converter_context.rb +94 -0
  33. data/lib/importu/converters.rb +119 -64
  34. data/lib/importu/definition.rb +23 -0
  35. data/lib/importu/duplicate_manager.rb +88 -0
  36. data/lib/importu/exceptions.rb +135 -4
  37. data/lib/importu/importer.rb +183 -96
  38. data/lib/importu/record.rb +138 -102
  39. data/lib/importu/sources/csv.rb +122 -0
  40. data/lib/importu/sources/json.rb +106 -0
  41. data/lib/importu/sources/ruby.rb +46 -0
  42. data/lib/importu/sources/xml.rb +133 -0
  43. data/lib/importu/sources.rb +13 -0
  44. data/lib/importu/summary.rb +277 -0
  45. data/lib/importu/version.rb +3 -1
  46. data/lib/importu.rb +45 -9
  47. data/spec/fixtures/books-duplicates/README.md +7 -0
  48. data/spec/fixtures/books-duplicates/infile.csv +7 -0
  49. data/spec/fixtures/books-duplicates/model.json +23 -0
  50. data/spec/fixtures/books-duplicates/summary.json +10 -0
  51. data/spec/fixtures/books-valid/README.md +13 -0
  52. data/spec/fixtures/books-valid/infile.csv +4 -0
  53. data/spec/fixtures/books-valid/infile.json +23 -0
  54. data/spec/fixtures/books-valid/infile.xml +21 -0
  55. data/spec/fixtures/books-valid/model.json +23 -0
  56. data/spec/fixtures/books-valid/record.json +26 -0
  57. data/spec/fixtures/books-valid/summary.json +8 -0
  58. data/spec/fixtures/source-empty-file/infile.csv +0 -0
  59. data/spec/fixtures/source-empty-file/infile.json +0 -0
  60. data/spec/fixtures/source-empty-file/infile.xml +0 -0
  61. data/spec/fixtures/source-empty-records/infile.csv +3 -0
  62. data/spec/fixtures/source-empty-records/infile.json +1 -0
  63. data/spec/fixtures/source-empty-records/infile.xml +6 -0
  64. data/spec/fixtures/source-malformed/infile.csv +1 -0
  65. data/spec/fixtures/source-malformed/infile.json +1 -0
  66. data/spec/fixtures/source-malformed/infile.xml +3 -0
  67. data/spec/fixtures/source-no-records/infile.csv +1 -0
  68. data/spec/fixtures/source-no-records/infile.json +1 -0
  69. data/spec/fixtures/source-no-records/infile.xml +3 -0
  70. data/spec/lib/importu/backends/active_record_spec.rb +150 -0
  71. data/spec/lib/importu/backends/middleware/duplicate_manager_proxy_spec.rb +70 -0
  72. data/spec/lib/importu/backends/middleware/enforce_allowed_actions_spec.rb +70 -0
  73. data/spec/lib/importu/backends_spec.rb +170 -0
  74. data/spec/lib/importu/converters_spec.rb +184 -141
  75. data/spec/lib/importu/definition_spec.rb +248 -0
  76. data/spec/lib/importu/duplicate_manager_spec.rb +92 -0
  77. data/spec/lib/importu/exceptions_spec.rb +69 -16
  78. data/spec/lib/importu/import_context_spec.rb +199 -0
  79. data/spec/lib/importu/importer_spec.rb +95 -0
  80. data/spec/lib/importu/integration_spec.rb +221 -0
  81. data/spec/lib/importu/record_spec.rb +130 -80
  82. data/spec/lib/importu/sources/csv_spec.rb +29 -0
  83. data/spec/lib/importu/sources/importer_source_examples.rb +175 -0
  84. data/spec/lib/importu/sources/json_spec.rb +29 -0
  85. data/spec/lib/importu/sources/ruby_spec.rb +102 -0
  86. data/spec/lib/importu/sources/xml_spec.rb +70 -0
  87. data/spec/lib/importu/summary_spec.rb +186 -0
  88. data/spec/spec_helper.rb +91 -7
  89. data/spec/support/active_record.rb +20 -0
  90. data/spec/support/book_importer.rb +31 -0
  91. data/spec/support/dummy_backend.rb +50 -0
  92. data/spec/support/fixtures_helper.rb +43 -0
  93. data/spec/support/matchers/delegate_matcher.rb +14 -8
  94. metadata +173 -100
  95. data/lib/importu/core_ext/array/deep_freeze.rb +0 -7
  96. data/lib/importu/core_ext/deep_freeze.rb +0 -3
  97. data/lib/importu/core_ext/hash/deep_freeze.rb +0 -7
  98. data/lib/importu/core_ext/object/deep_freeze.rb +0 -6
  99. data/lib/importu/core_ext.rb +0 -3
  100. data/lib/importu/dsl.rb +0 -127
  101. data/lib/importu/importer/csv.rb +0 -52
  102. data/lib/importu/importer/json.rb +0 -45
  103. data/lib/importu/importer/xml.rb +0 -55
  104. data/spec/factories/importer.rb +0 -12
  105. data/spec/factories/importer_record.rb +0 -13
  106. data/spec/factories/json_importer.rb +0 -14
  107. data/spec/factories/xml_importer.rb +0 -12
  108. data/spec/lib/importu/dsl_spec.rb +0 -26
  109. data/spec/lib/importu/importer/json_spec.rb +0 -37
  110. 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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
1
2
  module Importu
2
- VERSION = '0.1.0'
3
+ # Current version of the Importu gem.
4
+ VERSION = "0.2.0"
3
5
  end
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 'importu/core_ext'
42
+ require "importu/importer"
43
+
44
+ require "importu/sources/csv"
45
+ require "importu/sources/json"
46
+ require "importu/sources/xml"
4
47
 
5
- require 'importu/dsl'
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,10 @@
1
+ {
2
+ "created": 3,
3
+ "updated": 0,
4
+ "unchanged": 0,
5
+ "invalid": 3,
6
+ "total": 6,
7
+ "validation_errors": {
8
+ "DuplicateRecord: matches a previous record": 3
9
+ }
10
+ }
@@ -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
+ ]
@@ -0,0 +1,8 @@
1
+ {
2
+ "created": 3,
3
+ "updated": 0,
4
+ "unchanged": 0,
5
+ "invalid": 0,
6
+ "total": 3,
7
+ "validation_errors": {}
8
+ }
File without changes
File without changes
File without changes
@@ -0,0 +1,3 @@
1
+ "isbn10","title","author","release_date","pages"
2
+ ,,,,
3
+ ,,,,
@@ -0,0 +1 @@
1
+ [{},{}]
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+
3
+ <books>
4
+ <book />
5
+ <book></book>
6
+ </books>
@@ -0,0 +1 @@
1
+ "isbn10","title","author
@@ -0,0 +1 @@
1
+ [{},[],{
@@ -0,0 +1,3 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+
3
+ <boo
@@ -0,0 +1 @@
1
+ "isbn10","title","author","release_date","pages"
@@ -0,0 +1,3 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+
3
+ <books />