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
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOStreams
4
+ # Converts the text that the application reads or writes to an encoding, and optionally cleanses it, see
5
+ # `IOStreams::Encode::Reader` and `IOStreams::Encode::Writer`.
6
+ module Encode
7
+ autoload :Reader, "io_streams/encode/reader"
8
+ autoload :Writer, "io_streams/encode/writer"
9
+
10
+ # What the application uses in place of the #encoding and #cleaner of the reader and writer of the encode stream,
11
+ # which v3.0 removes: the settings of the path, which return what was set in both versions, see
12
+ # `IOStreams::Reader.v3_stream_advice`.
13
+ V3_STREAM_ADVICE = {
14
+ encoding: "use the #encoding of the path instead, which returns the encoding that was set, such as \"UTF-8\", or " \
15
+ "Encoding.find(path.encoding) for its Encoding",
16
+ cleaner: "use path.setting(:encode)[:cleaner] instead, which returns the cleaner that was set, such as :printable"
17
+ }.freeze
18
+ private_constant :V3_STREAM_ADVICE
19
+
20
+ # Returns [Array<Symbol>] the names of the options of the encode stream, which its reader and writer both take.
21
+ def self.option_names
22
+ Reader.option_names | Writer.option_names
23
+ end
24
+
25
+ # Raises ArgumentError for the options of the encode stream that its reader or writer would raise for once the text
26
+ # is read or written: an option that it does not take, an encoding that Ruby does not know, or that is neither a
27
+ # String nor an Encoding, one that Ruby cannot convert text to, see .check_convertible, a `replace` that is not a
28
+ # String, or a cleaner that is not the name of a built-in rule. Also for a cleaner that is neither a Symbol nor a
29
+ # Proc, which the reader and writer ignore, so that the text would not be cleansed.
30
+ #
31
+ # So that `IOStreams::Builder#encoding` raises when the options are set, rather than once a writer has replaced the
32
+ # file that it writes.
33
+ def self.check(**options)
34
+ unknown = options.keys - option_names
35
+ if unknown.any?
36
+ raise(ArgumentError, "Unknown encoding #{unknown.size == 1 ? 'option' : 'options'} #{list(unknown)}. " \
37
+ "Valid options: #{list(option_names)}.")
38
+ end
39
+
40
+ check_encoding(options[:encoding]) if options.key?(:encoding)
41
+ check_replace(options[:replace])
42
+ check_cleaner(options[:cleaner])
43
+ nil
44
+ end
45
+
46
+ def self.check_encoding(encoding)
47
+ unless encoding.is_a?(String) || encoding.is_a?(Encoding)
48
+ raise(ArgumentError, "Invalid encoding #{encoding.inspect}: supply the name of an encoding, such as " \
49
+ "\"UTF-8\" or \"Windows-1252\", or an Encoding")
50
+ end
51
+
52
+ check_convertible(encoding, encoding.is_a?(String) ? Encoding.find(encoding) : encoding)
53
+ end
54
+ private_class_method :check_encoding
55
+
56
+ # Raises ArgumentError for an encoding that Ruby has no converter for, such as "UTF-7", so that it raises when it is
57
+ # set, rather than Encoding::ConverterNotFoundError once the text is read or written. The text that is read and
58
+ # written is converted to the encoding, such as from the UTF-8 that the application writes. An ASCII compatible
59
+ # encoding needs no converter for ASCII text, so the text of one that Ruby cannot convert, such as "Windows-1258",
60
+ # is still read and written as it is.
61
+ def self.check_convertible(option, encoding)
62
+ return if encoding.ascii_compatible? || converter?(Encoding::UTF_8, encoding)
63
+
64
+ raise(ArgumentError, "Invalid encoding #{option.inspect}: Ruby cannot convert text to or from #{encoding}")
65
+ end
66
+ private_class_method :check_convertible
67
+
68
+ # Returns [true|false] whether Ruby has a converter of text from the source encoding to the destination encoding.
69
+ def self.converter?(source, destination)
70
+ Encoding::Converter.new(source, destination)
71
+ true
72
+ rescue Encoding::ConverterNotFoundError
73
+ false
74
+ end
75
+ private_class_method :converter?
76
+
77
+ def self.check_replace(replace)
78
+ return if replace.nil? || replace.is_a?(String)
79
+
80
+ raise(ArgumentError, "Invalid replace #{replace.inspect}: supply the String that replaces each invalid character")
81
+ end
82
+ private_class_method :check_replace
83
+
84
+ def self.check_cleaner(cleaner)
85
+ unless cleaner.nil? || cleaner.is_a?(Symbol) || cleaner.is_a?(Proc)
86
+ raise(ArgumentError,
87
+ "Invalid cleaner #{cleaner.inspect}: supply the name of a built-in rule, such as :printable, or a Proc")
88
+ end
89
+
90
+ Reader.extract_cleaner(cleaner)
91
+ end
92
+ private_class_method :check_cleaner
93
+
94
+ def self.list(names)
95
+ names.map(&:inspect).join(", ")
96
+ end
97
+ private_class_method :list
98
+ end
99
+ end
@@ -33,6 +33,57 @@ module IOStreams
33
33
  class ValueTooLong < Error
34
34
  end
35
35
 
36
+ # A value that is not one of the type of its field in the schema, such as `"abc"` for an integer,
37
+ # see `IOStreams::Stream#schema`.
38
+ class InvalidValue < Error
39
+ end
40
+
41
+ # Text that is not valid in the encoding that it is read or written in, such as bytes that are not valid UTF-8 in
42
+ # a file read with a schema, see `IOStreams::Stream#schema`.
43
+ #
44
+ # It is an `Encoding::UndefinedConversionError`, as the encode stream raises, so that it can be rescued as one.
45
+ #
46
+ # Example:
47
+ # IOStreams.path("data.csv").schema("people.json").each(:hash) { |hash| p hash }
48
+ # # => IOStreams::Errors::InvalidEncoding: "\xFF" is not valid UTF-8
49
+ class InvalidEncoding < ::Encoding::UndefinedConversionError
50
+ # Returns [Integer] the offset of the first invalid byte within the data, which IOStreams 3.0 gives, or [nil] in
51
+ # IOStreams 2.2, which does not count the bytes of the data read.
52
+ attr_reader :byte_offset
53
+
54
+ # Returns [Integer] the number of the line that the invalid data is on, counting from 1,
55
+ # or [nil] when the data is not read a line at a time.
56
+ attr_reader :line_number
57
+
58
+ # Returns [String] the invalid bytes, as binary, like `Encoding::UndefinedConversionError#error_char`.
59
+ attr_reader :error_char
60
+
61
+ # Returns [Encoding] the encoding that the invalid bytes were read or written as, binary for data read from a file,
62
+ # and the encoding that they are not valid in, like `Encoding::UndefinedConversionError#source_encoding` and
63
+ # `#destination_encoding`.
64
+ attr_reader :source_encoding, :destination_encoding
65
+
66
+ def initialize(message, byte_offset: nil, line_number: nil, error_char: nil, source_encoding: nil,
67
+ destination_encoding: nil)
68
+ @byte_offset = byte_offset
69
+ @line_number = line_number
70
+ @error_char = error_char
71
+ @source_encoding = source_encoding
72
+ @destination_encoding = destination_encoding
73
+ super(message)
74
+ end
75
+
76
+ # Returns [String] the name of the #source_encoding, such as "ASCII-8BIT", or nil.
77
+ def source_encoding_name
78
+ source_encoding&.name
79
+ end
80
+
81
+ # Returns [String] the name of the #destination_encoding, such as "UTF-8", or nil.
82
+ def destination_encoding_name
83
+ destination_encoding&.name
84
+ end
85
+ end
86
+
36
87
  class MalformedDataError < RuntimeError
37
88
  attr_reader :line_number
38
89
 
@@ -5,6 +5,17 @@ module IOStreams
5
5
  []
6
6
  end
7
7
 
8
+ # The methods that the stream that v3.0 supplies to a reader block for gzip answers: those of an IO that read, and
9
+ # #close, see .v3_stream_methods.
10
+ V3_METHODS = [*V3_READ_METHODS, :close].freeze
11
+ private_constant :V3_METHODS
12
+
13
+ # Returns [Array<Symbol>] the methods that the stream that v3.0 supplies to a reader block, in place of the
14
+ # `Zlib::GzipReader` that this reader yields, answers, see `IOStreams::Reader.v3_stream_methods`.
15
+ def self.v3_stream_methods(_options)
16
+ V3_METHODS
17
+ end
18
+
8
19
  # Read from a gzip stream, decompressing the contents as it is read
9
20
  def self.stream(input_stream)
10
21
  io = ::Zlib::GzipReader.new(input_stream)