iostreams 2.1.0 → 2.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/docs/config.md +13 -9
  3. data/docs/copy_files.md +20 -2
  4. data/docs/extensions.md +18 -4
  5. data/docs/formats.md +644 -16
  6. data/docs/path.md +18 -2
  7. data/docs/streams.md +54 -9
  8. data/docs/tutorial.md +14 -2
  9. data/docs/upgrading.md +470 -23
  10. data/lib/io_streams/builder/deprecations.rb +250 -0
  11. data/lib/io_streams/builder.rb +256 -39
  12. data/lib/io_streams/bzip2/reader.rb +18 -2
  13. data/lib/io_streams/bzip2/writer.rb +11 -2
  14. data/lib/io_streams/deprecation/methods.rb +51 -0
  15. data/lib/io_streams/deprecation/once.rb +27 -0
  16. data/lib/io_streams/deprecation/option_names.rb +26 -0
  17. data/lib/io_streams/deprecation/words.rb +42 -0
  18. data/lib/io_streams/deprecation.rb +259 -0
  19. data/lib/io_streams/encode/reader.rb +54 -3
  20. data/lib/io_streams/encode/writer.rb +19 -2
  21. data/lib/io_streams/encode.rb +99 -0
  22. data/lib/io_streams/errors.rb +51 -0
  23. data/lib/io_streams/gzip/reader.rb +11 -0
  24. data/lib/io_streams/io_streams.rb +365 -35
  25. data/lib/io_streams/line/reader.rb +2 -1
  26. data/lib/io_streams/line/utf8_reader.rb +23 -0
  27. data/lib/io_streams/path.rb +125 -35
  28. data/lib/io_streams/paths/file.rb +72 -9
  29. data/lib/io_streams/paths/http.rb +48 -2
  30. data/lib/io_streams/paths/matcher.rb +6 -3
  31. data/lib/io_streams/paths/s3.rb +180 -9
  32. data/lib/io_streams/paths/sftp.rb +47 -3
  33. data/lib/io_streams/paths.rb +23 -0
  34. data/lib/io_streams/pgp/reader.rb +18 -0
  35. data/lib/io_streams/pgp/writer.rb +16 -0
  36. data/lib/io_streams/pgp.rb +16 -2
  37. data/lib/io_streams/reader.rb +44 -0
  38. data/lib/io_streams/record/reader.rb +59 -68
  39. data/lib/io_streams/record/writer.rb +17 -10
  40. data/lib/io_streams/row/header_row.rb +21 -0
  41. data/lib/io_streams/row/reader.rb +52 -15
  42. data/lib/io_streams/row/writer.rb +64 -13
  43. data/lib/io_streams/schema/columns.rb +456 -0
  44. data/lib/io_streams/schema/constraints.rb +99 -0
  45. data/lib/io_streams/schema/field.rb +967 -0
  46. data/lib/io_streams/schema/json_number.rb +56 -0
  47. data/lib/io_streams/schema/keys.rb +78 -0
  48. data/lib/io_streams/schema/pattern.rb +257 -0
  49. data/lib/io_streams/schema/source.rb +31 -0
  50. data/lib/io_streams/schema.rb +616 -0
  51. data/lib/io_streams/stream.rb +836 -67
  52. data/lib/io_streams/symmetric_encryption/reader.rb +8 -0
  53. data/lib/io_streams/tabular/header.rb +29 -0
  54. data/lib/io_streams/tabular/header_mapping.rb +332 -0
  55. data/lib/io_streams/tabular/mapping.rb +165 -0
  56. data/lib/io_streams/tabular/parser/array.rb +5 -0
  57. data/lib/io_streams/tabular/parser/base.rb +24 -0
  58. data/lib/io_streams/tabular/parser/fixed.rb +197 -0
  59. data/lib/io_streams/tabular/parser/fixed_fields.rb +160 -0
  60. data/lib/io_streams/tabular/parser/hash.rb +10 -0
  61. data/lib/io_streams/tabular/parser/json.rb +17 -0
  62. data/lib/io_streams/tabular/parser/json_fields.rb +132 -0
  63. data/lib/io_streams/tabular/schema_mapping.rb +361 -0
  64. data/lib/io_streams/tabular.rb +344 -30
  65. data/lib/io_streams/utils.rb +441 -6
  66. data/lib/io_streams/version.rb +1 -1
  67. data/lib/io_streams/writer.rb +27 -0
  68. data/lib/io_streams/xlsx/cells.rb +127 -0
  69. data/lib/io_streams/xlsx/reader.rb +56 -0
  70. data/lib/io_streams/xlsx/schema_reader.rb +90 -0
  71. data/lib/io_streams/xlsx/v3_writer.rb +16 -0
  72. data/lib/io_streams/zip/reader.rb +7 -0
  73. data/lib/io_streams/zip/writer.rb +24 -2
  74. data/lib/iostreams.rb +21 -17
  75. metadata +28 -2
data/docs/path.md CHANGED
@@ -309,7 +309,8 @@ end
309
309
 
310
310
  A username and password supplied in the url remain part of it, so `#to_s` returns them,
311
311
  as does any log or error message that includes the path. To keep them out of logs, supply them
312
- with the `username:` and `password:` arguments instead.
312
+ with the `username:` and `password:` arguments instead, and log `#display_name`, which never
313
+ includes them.
313
314
 
314
315
  #### Optional Arguments:
315
316
 
@@ -378,7 +379,8 @@ Notes:
378
379
 
379
380
  A username and password supplied in the url remain part of it, so `#to_s` returns them,
380
381
  as does any log or error message that includes the path. To keep them out of logs, supply them
381
- with the `username:` and `password:` arguments instead.
382
+ with the `username:` and `password:` arguments instead, and log `#display_name`, which never
383
+ includes them.
382
384
 
383
385
  #### Optional Arguments:
384
386
 
@@ -487,6 +489,20 @@ IOStreams.path("sample/data").mkpath
487
489
  Inspect the components of a path's file name:
488
490
 
489
491
  ~~~ruby
492
+ # The full name of the path, without any user name, password or query in an SFTP or HTTP url,
493
+ # for logging. Unlike #to_s it cannot be used to create the path again.
494
+ IOStreams.path("sftp://jack:secret@sftp.example.org/data/ruby.rb").display_name
495
+ # => "sftp://sftp.example.org/data/ruby.rb"
496
+
497
+ # The options for creating a path, such as a password, with each secret replaced, for an application
498
+ # that stores a url and its options, and displays them. The path class of the url decides which are
499
+ # secret. `redact_stream_options` does the same for the options of streams.
500
+ IOStreams.redact_path_options("sftp://sftp.example.org/data/ruby.rb", username: "jack", password: "secret")
501
+ # => {username: "jack", password: "[FILTERED]"}
502
+
503
+ IOStreams.redact_stream_options(pgp: {passphrase: "secret", recipient: "a@b.org"})
504
+ # => {pgp: {passphrase: "[FILTERED]", recipient: "a@b.org"}}
505
+
490
506
  # The last component of the path.
491
507
  IOStreams.path("/home/gumby/work/ruby.rb").basename
492
508
  # => "ruby.rb"
data/docs/streams.md CHANGED
@@ -21,6 +21,12 @@ IOStreams.path("example.csv").reader do |io|
21
21
  end
22
22
  ~~~
23
23
 
24
+ The stream that the block receives answers the methods of an IO that read, such as `#read` and `#eof?`. Rely on
25
+ those, rather than on the class of the stream, which depends on how the file is stored, such as a `Zlib::GzipReader`
26
+ for a `.gz` file. Where a library needs a `File`, copy the data into a local file first, such as with
27
+ `IOStreams.temp_file` and `#copy_to`, and read that file. To read the data from the start again, open the reader
28
+ again.
29
+
24
30
  Read one line at a time from the file:
25
31
  ~~~ruby
26
32
  IOStreams.path("example.csv").each do |line|
@@ -50,8 +56,13 @@ Notes:
50
56
  tabular format quotes its fields, such as CSV (detected from a `.csv` file name or set explicitly via
51
57
  `.format(:csv)`). `embedded_within` only needs to be supplied for quoted formats that are not
52
58
  auto-detected, or to override the quote character.
59
+ * When reading rows or records, with `each(:array)` or `each(:hash)`, the format of the file decides where
60
+ its lines are split, since its parser must agree with them, so set the format with `format` instead.
61
+ Supplying `embedded_within` there is deprecated, and reports a deprecation warning, see
62
+ [Seeing deprecation warnings](upgrading#seeing-deprecation-warnings).
53
63
  * A file that is named `.csv` but is actually pipe-delimited can avoid quote parsing by declaring its
54
- real format with `.format(:psv)`, or by passing `embedded_within: nil` to disable it explicitly:
64
+ real format with `.format(:psv)`, or when reading lines, by passing `embedded_within: nil` to disable it
65
+ explicitly:
55
66
  ~~~ruby
56
67
  IOStreams.path("pipe_delimited.csv").format(:psv).each(:line) do |line|
57
68
  puts line
@@ -72,6 +83,23 @@ IOStreams.path("example.csv").each(:hash) do |hash|
72
83
  end
73
84
  ~~~
74
85
 
86
+ The tabular format of the rows and records, such as CSV or JSON, is detected from the file name, or set with
87
+ `#format`, see [File Formats](formats). Set a schema on the path with `#schema` to read each value as the type of
88
+ its field, such as an `Integer` or a `Date`, with the names of the fields as the keys of each record, see
89
+ [Schema](formats#schema):
90
+ ~~~ruby
91
+ IOStreams.path("example.csv").schema(
92
+ fieldsMatch: "subset",
93
+ fields: [
94
+ {name: "name"},
95
+ {name: "zip_code", type: "integer"}
96
+ ]
97
+ ).each(:hash) do |hash|
98
+ p hash
99
+ end
100
+ # => {"name" => "Jack", "zip_code" => 1234}
101
+ ~~~
102
+
75
103
  Write data to the file.
76
104
  ~~~ruby
77
105
  IOStreams.path("abc.txt").writer do |io|
@@ -258,10 +286,12 @@ IOStreams.path("example.csv.pgp").
258
286
 
259
287
  An option that neither direction accepts, such as a misspelled one, raises an `ArgumentError`
260
288
  that lists the valid options. The exception is BZip2, which ignores options it does not accept and
261
- logs a warning instead. In v3.0 it will raise an `ArgumentError` too.
289
+ logs a warning instead. One that neither reading nor writing takes also reports a deprecation warning,
290
+ since in v3.0 it will raise an `ArgumentError` too, while v3.0 ignores an option of the other direction.
262
291
 
263
292
  Options for a stream that is not in the pipeline are still ignored, as described above,
264
- since they are not passed to any reader or writer.
293
+ since they are not passed to any reader or writer. One that neither direction accepts reports a
294
+ deprecation warning when it is set, for every stream, since v3.0 raises an `ArgumentError` for it then.
265
295
 
266
296
  #### Stream
267
297
 
@@ -305,7 +335,8 @@ In this example the file contains JSON data that was compressed with Zip, and si
305
335
  ~~~ruby
306
336
  IOStreams.path("tempfile2527").
307
337
  stream(:zip).
308
- each(:hash, format: :json) do |row|
338
+ format(:json).
339
+ each(:hash) do |row|
309
340
  p row
310
341
  end
311
342
  ~~~
@@ -329,9 +360,23 @@ path.setting(:pgp)
329
360
  # => {:passphrase=>"receiver_passphrase"}
330
361
  ~~~
331
362
 
332
- To ensure no streams are inferred or applied use stream `:none`
333
- ~~~ruby
334
- path = IOStreams.path("file.zip")
335
- path.stream(:none)
336
- path.read
363
+ #### Raw
364
+
365
+ To read or write the data as it is stored, applying no streams, neither those that the file name implies nor any
366
+ set with `option` or `stream`, and no encoding, use `raw`. For example, to checksum a compressed file as it is
367
+ stored, or to write data that is already compressed:
368
+
369
+ ~~~ruby
370
+ Digest::SHA256.hexdigest(IOStreams.path("data.csv.gz").raw.read)
371
+
372
+ IOStreams.path("data.csv.gz").raw.write(gzipped_data)
337
373
  ~~~
374
+
375
+ Setting a stream, option or encoding after `raw` raises `ArgumentError`, since the data is as stored.
376
+ `raw?` returns whether `raw` was called. `raw` replaces `stream(:none)`, which is deprecated: it still works, and
377
+ reports a deprecation warning, see [Upgrading](upgrading#deprecated-and-what-replaces-it).
378
+
379
+ To copy a file as it is stored, such as downloading a zip file without unzipping it, use
380
+ `copy_to(target, convert: false)` instead, since the target applies its own streams. To read a file that is not
381
+ named for its format, such as a CSV file called `data.txt`, set the file name that names its format, for example
382
+ `file_name("data.csv")`.
data/docs/tutorial.md CHANGED
@@ -429,8 +429,20 @@ Using `writer(:hash)` makes it easier to develop the application without regard
429
429
  - Specialized escaping of values to handle row or column delimiters
430
430
 
431
431
  Note: The first row written determines the column names as well as the order of the elements to be written.
432
- To supply the header columns up front, to set the order or to filter out which columns should be written to the
433
- target file, pass them to the writer: `path.writer(:hash, columns: ["name", "login"])`.
432
+ To set the columns up front, in your own order, describe them with a [schema](formats#schema) on the path: its
433
+ fields are the columns, in order, and `fieldsMatch: "subset"` skips the other keys of each record:
434
+ ~~~ruby
435
+ path = IOStreams.path("sample/logins.csv").schema(
436
+ fieldsMatch: "subset",
437
+ fields: [{name: "login"}, {name: "name"}]
438
+ )
439
+ path.writer(:hash) do |io|
440
+ io << {"name" => "Jack Jones", "login" => "jjones", "notes" => "Not written"}
441
+ io << {"login" => "jsmith", "name" => "Jill Smith"}
442
+ end
443
+ path.read
444
+ # => "login,name\njjones,Jack Jones\njsmith,Jill Smith\n"
445
+ ~~~
434
446
 
435
447
  Now lets write the same data into a JSON file, then read it to see what it looks like:
436
448
  ~~~ruby