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
@@ -8,9 +8,25 @@ module IOStreams
8
8
  nil
9
9
  end
10
10
 
11
+ # Returns [Array<Symbol>] the options that this class takes, which v3.0 declares as its `option_names`, see
12
+ # `IOStreams::Deprecation::OptionNames#known_option_names`, so that an option that neither it nor the writer takes
13
+ # is reported as deprecated when it is set, while one of the writer is not, since v3.0 accepts it, and ignores it,
14
+ # and so that a path opens it with these alone, and logs the others, see `IOStreams::Builder#taken_options`.
15
+ def self.known_option_names
16
+ OPTION_NAMES
17
+ end
18
+
19
+ # Returns [Array<Symbol>] the methods that the stream that v3.0 supplies to a reader block, in place of the
20
+ # `Bzip2::FFI::Reader` that this reader yields, answers: those of an IO that read, see
21
+ # `IOStreams::Reader.v3_stream_methods`.
22
+ def self.v3_stream_methods(_options)
23
+ V3_READ_METHODS
24
+ end
25
+
11
26
  # Read from a Bzip2 stream, decompressing the contents as it is read
12
27
  #
13
- # Any other option is ignored and logs a warning. It will raise `ArgumentError` in v3.0.
28
+ # Any other option is ignored, and logs a warning. One that the writer does not take either also reports a
29
+ # deprecation warning when it is set, see .known_option_names, since it will raise `ArgumentError` in v3.0.
14
30
  #
15
31
  # Parameters are passed through to `Bzip2::FFI::Reader`:
16
32
  # autoclose: [true|false]
@@ -25,7 +41,7 @@ module IOStreams
25
41
  # Use an alternative decompression algorithm that uses less memory but is slower.
26
42
  # Default: false
27
43
  def self.stream(input_stream, autoclose: false, first_only: false, small: false, **unknown)
28
- Utils.warn_unknown_options(unknown, :bz2, "reading", OPTION_NAMES)
44
+ Utils.warn_unknown_options(unknown, :bz2, :reader, OPTION_NAMES)
29
45
  Utils.load_soft_dependency("bzip2-ffi", "Bzip2", "bzip2/ffi") unless defined?(::Bzip2::FFI)
30
46
 
31
47
  begin
@@ -8,9 +8,18 @@ module IOStreams
8
8
  nil
9
9
  end
10
10
 
11
+ # Returns [Array<Symbol>] the options that this class takes, which v3.0 declares as its `option_names`, see
12
+ # `IOStreams::Deprecation::OptionNames#known_option_names`, so that an option that neither it nor the reader takes
13
+ # is reported as deprecated when it is set, while one of the reader is not, since v3.0 accepts it, and ignores it,
14
+ # and so that a path opens it with these alone, and logs the others, see `IOStreams::Builder#taken_options`.
15
+ def self.known_option_names
16
+ OPTION_NAMES
17
+ end
18
+
11
19
  # Write to a stream, compressing with Bzip2
12
20
  #
13
- # Any other option is ignored and logs a warning. It will raise `ArgumentError` in v3.0.
21
+ # Any other option is ignored, and logs a warning. One that the reader does not take either also reports a
22
+ # deprecation warning when it is set, see .known_option_names, since it will raise `ArgumentError` in v3.0.
14
23
  #
15
24
  # Parameters are passed through to `Bzip2::FFI::Writer`:
16
25
  # autoclose: [true|false]
@@ -26,7 +35,7 @@ module IOStreams
26
35
  # to a slower algorithm, from 0 to 250. 0 uses the libbz2 default.
27
36
  # Default: 0
28
37
  def self.stream(input_stream, autoclose: false, block_size: nil, work_factor: nil, **unknown)
29
- Utils.warn_unknown_options(unknown, :bz2, "writing", OPTION_NAMES)
38
+ Utils.warn_unknown_options(unknown, :bz2, :writer, OPTION_NAMES)
30
39
  Utils.load_soft_dependency("bzip2-ffi", "Bzip2", "bzip2/ffi") unless defined?(::Bzip2::FFI)
31
40
 
32
41
  begin
@@ -0,0 +1,51 @@
1
+ module IOStreams
2
+ module Deprecation
3
+ # A module whose methods report a deprecation warning when the application calls them, and then call the method of
4
+ # the object that it extends, see .deprecate: for the methods of an object that IOStreams hands the application, such
5
+ # as the `Zlib::GzipReader` that a reader block receives for a `.gz` file, that the object that v3.0 hands it in its
6
+ # place does not answer, such as `#rewind`.
7
+ class Methods < ::Module
8
+ @modules = {}
9
+ @mutex = Mutex.new
10
+
11
+ # Extends the supplied object, which IOStreams hands to the application, with a module that reports a deprecation
12
+ # warning for each of its public methods that the object in its place in v3.0 does not answer: those other than
13
+ # the supplied methods, which that object answers, and the methods of every object. Each warning is the name of the
14
+ # method, such as `#rewind`, followed by the supplied message, such as "is deprecated on ...", or by the message of
15
+ # its name when the message is a Hash, such as one that names what replaces it, otherwise that of nil.
16
+ #
17
+ # The module is prepended to the singleton class of the object, rather than extending it, so that its methods
18
+ # come before those that the object defines on itself, such as the `#path` of a `Zlib::GzipReader`. The object
19
+ # keeps its class, and answers the same methods, which each still call its own method. Only the calls that the
20
+ # application makes itself report a warning, see `IOStreams::Deprecation.warn_call`, and none while deprecation
21
+ # warnings are off. The module is built once for each class of object, and kept, so that each object costs one
22
+ # `prepend`, and nothing else until a deprecated method is called.
23
+ #
24
+ # Returns the object.
25
+ def self.deprecate(object, kept_methods, message)
26
+ return object if object.frozen?
27
+
28
+ key = [object.class, object.singleton_methods.sort, kept_methods, message]
29
+ deprecated = @mutex.synchronize do
30
+ @modules[key] ||= new(object.public_methods - ::Object.public_instance_methods - kept_methods, message)
31
+ end
32
+ object.singleton_class.prepend(deprecated)
33
+ object
34
+ end
35
+
36
+ # Defines a method for each of the supplied names that reports the deprecation warning of its name, followed by the
37
+ # supplied message, or its own message when it is a Hash, see .deprecate, when the application calls it, and then
38
+ # calls the method of the object.
39
+ def initialize(names, message)
40
+ super()
41
+ names.each do |name|
42
+ warning = "##{name} #{message.is_a?(Hash) ? message.fetch(name) { message.fetch(nil) } : message}".freeze
43
+ define_method(name) do |*args, **kwargs, &block|
44
+ ::IOStreams::Deprecation.warn_call(warning) if ::Warning[:deprecated]
45
+ super(*args, **kwargs, &block)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOStreams
4
+ module Deprecation
5
+ # Reports a deprecation warning the first time that it applies, for an object that would otherwise report one for each
6
+ # value that it converts, such as a field of a schema, which converts a value of each row of a file. The object holds
7
+ # one for each of its deprecations, which is not frozen with it, so that an object that is frozen, as a field is,
8
+ # still reports each one once.
9
+ class Once
10
+ # Reports the deprecation warning that the block returns, see `IOStreams::Deprecation.warn`, unless this has
11
+ # reported it already, or deprecation warnings are off, see `IOStreams::Deprecation.on?`, when the block is not
12
+ # called, so that a caller builds its message only when it is reported. The block returns nil when the
13
+ # deprecation does not apply to the value at hand, which leaves it to be reported for a later one.
14
+ #
15
+ # Returns nil.
16
+ def warn
17
+ return if @reported || !Deprecation.on?
18
+
19
+ message = yield
20
+ return if message.nil?
21
+
22
+ @reported = true
23
+ Deprecation.warn(message)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOStreams
4
+ module Deprecation
5
+ # The names of the options that a reader or a writer takes in IOStreams 3.0, which `IOStreams::Reader` and
6
+ # `IOStreams::Writer` extend, so that each of their subclasses answers them. `#option`, `#stream` and
7
+ # `#option_or_stream` report a deprecation warning for an option that IOStreams 3.0 raises ArgumentError for when it
8
+ # is set: one that the reader or the writer of the stream replaces, see #replaced_option_names, and one that neither
9
+ # of them takes, see #known_option_names, such as a misspelled one, see `IOStreams::Builder#option`.
10
+ module OptionNames
11
+ # Returns [Array<Symbol>] the names of the options that this class takes in IOStreams 3.0 too, or [nil] when they
12
+ # are not known, since it does not declare its options: by default those that it declares, see `.option_names`,
13
+ # other than those that it replaces, see #replaced_option_names.
14
+ def known_option_names
15
+ names = option_names
16
+ names && (names - replaced_option_names.keys)
17
+ end
18
+
19
+ # Returns [Hash<Symbol, Symbol>] the deprecated options that this class still takes, which IOStreams 3.0 does not,
20
+ # with the option that replaces each, which the deprecation warning names. None by default.
21
+ def replaced_option_names
22
+ {}
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOStreams
4
+ module Deprecation
5
+ # The words of the message of a deprecation warning that depend on how many things it is about, such as the options
6
+ # that are deprecated, so that each message says "is" and "it" of one, and "are" and "them" of more, see .warn.
7
+ class Words
8
+ # Parameters:
9
+ # items: [Array]
10
+ # The things that the message is about, such as the names of options.
11
+ def initialize(items)
12
+ @items = items
13
+ end
14
+
15
+ # Returns [String] the items as the keywords that a caller supplies them as, such as `compress:, level:`, and an
16
+ # item that is not a Symbol, which is not a keyword, as it is written, such as `"acl"`.
17
+ def keywords
18
+ @items.map { |item| item.is_a?(Symbol) ? "#{item}:" : item.inspect }.join(", ")
19
+ end
20
+
21
+ # Returns [true|false] whether the message is about one item.
22
+ def one?
23
+ @items.size == 1
24
+ end
25
+
26
+ # Returns [String] "are", or "is" of one item.
27
+ def are
28
+ one? ? "is" : "are"
29
+ end
30
+
31
+ # Returns [String] "them", or "it" of one item.
32
+ def them
33
+ one? ? "it" : "them"
34
+ end
35
+
36
+ # Returns [String] "they are", or "it is" of one item.
37
+ def they_are
38
+ one? ? "it is" : "they are"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module IOStreams
6
+ # Reports the use of a deprecated method or option as a Ruby deprecation warning, see `Kernel#warn`, so that Ruby
7
+ # decides whether and where it is reported, as it does for its own deprecations, rather than a setting of IOStreams:
8
+ # - Only while deprecation warnings are on, see `Warning[:deprecated]`. They are off by default, so production stays
9
+ # quiet, and on in tests run by Minitest or test-unit, or with `ruby -W:deprecated` or `RUBYOPT=-W:deprecated`.
10
+ # - Written to `$stderr`, unless the application routes them elsewhere, such as to its log, or raises them in its
11
+ # tests, by overriding `Warning.warn`.
12
+ #
13
+ # Each warning names the line of the application that used what is deprecated, rather than a line of IOStreams, of
14
+ # Ruby itself, or of a gem that IOStreams called with a block, see .application_location.
15
+ module Deprecation
16
+ autoload :Methods, "io_streams/deprecation/methods"
17
+ autoload :Once, "io_streams/deprecation/once"
18
+ autoload :OptionNames, "io_streams/deprecation/option_names"
19
+ autoload :Words, "io_streams/deprecation/words"
20
+
21
+ # The directory of the files of this library, whose lines are skipped when finding the line that used it.
22
+ LIB_DIR = "#{__dir__}/".freeze
23
+ private_constant :LIB_DIR
24
+
25
+ # The file that loads this library, whose lines are skipped too. Only the files of this library are skipped, rather
26
+ # than every file of the directory that holds them, which can hold other libraries, such as `site_ruby`, or the
27
+ # `lib` directory of an application that it is copied into.
28
+ LIB_FILE = File.expand_path("../iostreams.rb", __dir__).freeze
29
+ private_constant :LIB_FILE
30
+
31
+ # The text that each warning starts with, after the location of the line that used what is deprecated.
32
+ PREFIX = "IOStreams: "
33
+ private_constant :PREFIX
34
+
35
+ # A warning reported by .warn, as `Kernel#warn` passes it to `Warning.warn`, see .warning?.
36
+ WARNING = /\A(?:.+:\d+: warning: )?#{Regexp.escape(PREFIX)}/
37
+ private_constant :WARNING
38
+
39
+ # The start of the paths of the core methods that Ruby writes in Ruby, such as `Kernel#tap`, see .core?.
40
+ CORE_PATH = "<internal:"
41
+ private_constant :CORE_PATH
42
+
43
+ # The start of the paths of the standard library of JRuby, when it is loaded from its jar.
44
+ JAR_PATH = "uri:classloader:"
45
+ private_constant :JAR_PATH
46
+
47
+ # The files of this module, whose methods find the line that called a deprecated method, see .calling_location.
48
+ OWN_FILES = ["#{__dir__}/deprecation.rb", "#{__dir__}/deprecation/methods.rb"].freeze
49
+ private_constant :OWN_FILES
50
+
51
+ # The end of the names of the files of the standard library of Ruby that define its delegators: `delegate.rb`, which
52
+ # defines `SimpleDelegator` and `DelegateClass`, and `forwardable.rb`, which defines `Forwardable`, see .delegator?.
53
+ DELEGATOR_FILES = %w[/delegate.rb /forwardable.rb].freeze
54
+ private_constant :DELEGATOR_FILES
55
+
56
+ # The directory of the standard library of Ruby.
57
+ RUBY_LIB_DIRS = [RbConfig::CONFIG["rubylibdir"]].compact.map { |dir| "#{dir}/" }.freeze
58
+ private_constant :RUBY_LIB_DIRS
59
+
60
+ # Reports that the application used something deprecated, unless deprecation warnings are off, with the message,
61
+ # which says what is deprecated and what to use instead, after "IOStreams: ", at the line of the application that
62
+ # used it, see .application_location, in the same form as `Kernel#warn` names the line of its caller, such as
63
+ # `app/jobs/export_job.rb:12: warning: IOStreams: stream(:none) is deprecated, ...`, or without a line when the
64
+ # call stack has none outside this library.
65
+ #
66
+ # Returns nil.
67
+ def self.warn(message)
68
+ return unless on?
69
+
70
+ report(message, application_location)
71
+ end
72
+
73
+ # Reports, as .warn does, that the application uses something deprecated while this library is loaded, such as a
74
+ # version of Ruby that IOStreams 3.0 does not support, at the line of the application that loaded it, see
75
+ # .loading_location, rather than at a line of a gem that requires it for the application, such as Bundler.
76
+ #
77
+ # Returns nil.
78
+ def self.warn_on_load(message)
79
+ return unless on?
80
+
81
+ report(message, loading_location)
82
+ end
83
+
84
+ # Reports, as .warn does, that the application called a deprecated method of an object that IOStreams handed it,
85
+ # such as the stream that a reader block receives, which calls this from that method, see
86
+ # `IOStreams::Deprecation::Methods`, at the line that called the method, see .calling_location, when it is the
87
+ # application's own: not a line of IOStreams, which calls the methods of the object for itself, of a gem, such as the
88
+ # gem whose object it is, which calls its own methods, or of a library that the application handed the object to,
89
+ # including the standard library of Ruby, see .standard_library?. So a method that the application hands IOStreams
90
+ # as the block itself, such as `reader(&:rewind)`, is not reported, since IOStreams calls it, while one that it hands
91
+ # a core method of Ruby, such as `io.then(&:rewind)`, or calls through a delegator of Ruby that wraps the object,
92
+ # such as `SimpleDelegator.new(io).rewind`, is, since they call it for the application.
93
+ #
94
+ # Returns nil.
95
+ def self.warn_call(message)
96
+ return unless Warning[:deprecated]
97
+
98
+ location = calling_location
99
+ return if location.nil? || library?(location) || gem?(location, installed_gem_dirs) || standard_library?(location)
100
+
101
+ report(message, location)
102
+ end
103
+
104
+ # Returns [true|false] whether the message, as `Kernel#warn` passes it to `Warning.warn`, is a warning reported by
105
+ # .warn, so that an application, or its tests, can tell the deprecation warnings of IOStreams from the others.
106
+ def self.warning?(message)
107
+ WARNING.match?(message)
108
+ end
109
+
110
+ # Returns [true|false] whether deprecation warnings are on, see `Warning[:deprecated]`, so that .warn reports them,
111
+ # for a caller to find out whether something is deprecated only while they are, where finding it out costs more
112
+ # than the call to .warn.
113
+ def self.on?
114
+ Warning[:deprecated]
115
+ end
116
+
117
+ # Returns [Thread::Backtrace::Location|nil] the line of the application that used this library, or nil when the
118
+ # call stack has none outside this library. It is the first line of the call stack outside this library, other
119
+ # than:
120
+ # - A line of Ruby itself, see .ruby?, which runs on behalf of its caller, such as `Kernel#tap`, or `Open3.popen2e`
121
+ # that this library calls with a block, as Ruby names the caller of its own core methods.
122
+ # - A line of a gem that this library called with a block, such as `Net::SFTP.start`, which has a line of this
123
+ # library further out in the call stack. A line of a gem that called this library, such as one of Rocket Job, is
124
+ # the one returned, since it is the gem that used it.
125
+ #
126
+ # The call stack is walked outward from the caller, until the line of the application is found, rather than
127
+ # creating all of it.
128
+ def self.application_location
129
+ gem_dirs = installed_gem_dirs
130
+ candidate = nil
131
+ Thread.each_caller_location do |location|
132
+ if library?(location)
133
+ candidate = nil
134
+ elsif gem?(location, gem_dirs)
135
+ candidate ||= location
136
+ elsif !ruby?(location)
137
+ return candidate || location
138
+ end
139
+ end
140
+ candidate
141
+ end
142
+ private_class_method :application_location
143
+
144
+ # Returns [Thread::Backtrace::Location|nil] the line of the application that loaded this library, such as its
145
+ # `require "iostreams"`, or its `Bundler.require`: the first line of the call stack outside this library, Ruby
146
+ # itself, see .ruby?, and the installed gems, since a gem that loads this library, such as Bundler, Bootsnap, or a gem
147
+ # that requires it, loads it for the application. Otherwise the first line of an installed gem outside this library,
148
+ # for an application that is itself an installed gem, or nil when the call stack has neither.
149
+ def self.loading_location
150
+ gem_dirs = installed_gem_dirs
151
+ candidate = nil
152
+ Thread.each_caller_location do |location|
153
+ next if library?(location) || ruby?(location)
154
+ return location unless gem?(location, gem_dirs)
155
+
156
+ candidate ||= location
157
+ end
158
+ candidate
159
+ end
160
+ private_class_method :loading_location
161
+
162
+ # Returns [Thread::Backtrace::Location|nil] the line that called the deprecated method that called .warn_call: the
163
+ # first line of the call stack outside the files of this module, other than a line of a core method, see .core?, or
164
+ # of a delegator of Ruby, see .delegator?, which calls the method for its caller, such as `Kernel#then` in
165
+ # `io.then(&:rewind)`, or the `SimpleDelegator` in `SimpleDelegator.new(io).rewind`. Ruby names the line that
166
+ # called a core method written in C in its place, and CRuby 4.0 does the same for one written in Ruby, so skipping
167
+ # the lines that earlier versions name gives the same line on each version of Ruby.
168
+ def self.calling_location
169
+ Thread.each_caller_location do |location|
170
+ next if OWN_FILES.include?(location.absolute_path || location.path) || core?(location) || delegator?(location)
171
+
172
+ return location
173
+ end
174
+ nil
175
+ end
176
+ private_class_method :calling_location
177
+
178
+ # Writes the message as a deprecation warning, see .warn, at the supplied line, or without a line when it is nil.
179
+ def self.report(message, location)
180
+ text = "#{PREFIX}#{message}"
181
+ text = "#{location.path}:#{location.lineno}: warning: #{text}" if location
182
+ Kernel.warn(text, category: :deprecated)
183
+ nil
184
+ end
185
+ private_class_method :report
186
+
187
+ # Returns [true|false] whether the line of the call stack is in a file of this library. Code run with `eval` has
188
+ # only the file name that it was given.
189
+ def self.library?(location)
190
+ file = location.absolute_path || location.path
191
+ file.start_with?(LIB_DIR) || file == LIB_FILE
192
+ end
193
+ private_class_method :library?
194
+
195
+ # Returns [true|false] whether the line of the call stack is in the code of Ruby itself: a core method, see .core?,
196
+ # or a file of its standard library, see .standard_library?.
197
+ def self.ruby?(location)
198
+ core?(location) || standard_library?(location)
199
+ end
200
+ private_class_method :ruby?
201
+
202
+ # Returns [true|false] whether the line of the call stack is in a core method of Ruby that is not written in C: one
203
+ # written in Ruby, such as `Kernel#tap`, in a file whose name starts with `<internal:`, which CRuby before 4.0 and
204
+ # JRuby name in the call stack, or one of JRuby written in Java. Ruby names the line that called a core method
205
+ # written in C in its place, as CRuby 4.0 does for one written in Ruby.
206
+ def self.core?(location)
207
+ path = location.path
208
+ path.start_with?(CORE_PATH) || path.end_with?(".java")
209
+ end
210
+ private_class_method :core?
211
+
212
+ # Returns [true|false] whether the line of the call stack is in a file of the standard library of Ruby, see
213
+ # RUBY_LIB_DIRS, or of that of JRuby, when it is loaded from its jar.
214
+ def self.standard_library?(location)
215
+ location.path.start_with?(JAR_PATH) || in_dirs?(location, RUBY_LIB_DIRS)
216
+ end
217
+ private_class_method :standard_library?
218
+
219
+ # Returns [true|false] whether the line of the call stack is in a delegator of the standard library of Ruby, see
220
+ # DELEGATOR_FILES, which calls the method of the object that it wraps for its caller, as a core method does: the
221
+ # `#method_missing` of a `SimpleDelegator`, or a method that `DelegateClass` or `Forwardable` defines. The other
222
+ # files of the standard library call the methods of an object for themselves, such as a library that the
223
+ # application hands the object to, see .warn_call.
224
+ def self.delegator?(location)
225
+ location.path.end_with?(*DELEGATOR_FILES) && standard_library?(location)
226
+ end
227
+ private_class_method :delegator?
228
+
229
+ # Returns [true|false] whether the line of the call stack is in a file of a gem, in one of the directories of the
230
+ # installed gems.
231
+ def self.gem?(location, gem_dirs)
232
+ in_dirs?(location, gem_dirs)
233
+ end
234
+ private_class_method :gem?
235
+
236
+ # Returns [true|false] whether the file of the line of the call stack is in one of the directories, by the path
237
+ # that it was loaded from, or its absolute path, which differs when the path holds a link.
238
+ def self.in_dirs?(location, dirs)
239
+ location.path.start_with?(*dirs) || location.absolute_path&.start_with?(*dirs) || false
240
+ end
241
+ private_class_method :in_dirs?
242
+
243
+ # Returns [Array<String>] the directories of the installed gems, see `Gem.path`, each ending with "/". They are kept
244
+ # for as long as `Gem.path` returns the same Array, since Bundler replaces it when it changes them, so that a
245
+ # deprecated method that a gem calls for each line, see .warn_call, builds them once, rather than on each call.
246
+ def self.installed_gem_dirs
247
+ return [] unless defined?(::Gem)
248
+
249
+ path = ::Gem.path
250
+ cached = @installed_gem_dirs
251
+ return cached.last if cached&.first.equal?(path)
252
+
253
+ dirs = path.map { |dir| dir.end_with?("/") ? dir : "#{dir}/" }.freeze
254
+ @installed_gem_dirs = [path, dirs].freeze
255
+ dirs
256
+ end
257
+ private_class_method :installed_gem_dirs
258
+ end
259
+ end
@@ -5,6 +5,23 @@ module IOStreams
5
5
  %i[encoding cleaner replace]
6
6
  end
7
7
 
8
+ # The methods that the stream that v3.0 supplies to a reader block for the encoding answers: those of an IO that
9
+ # read, and #input_stream, see .v3_stream_methods.
10
+ V3_METHODS = [*V3_READ_METHODS, :input_stream].freeze
11
+ private_constant :V3_METHODS
12
+
13
+ # Returns [Array<Symbol>] the methods that the stream that v3.0 supplies to a reader block for the encoding answers,
14
+ # in place of this reader, see `IOStreams::Reader.v3_stream_methods`. It has no #encoding or #cleaner, which the
15
+ # settings of the path replace, see .v3_stream_advice.
16
+ def self.v3_stream_methods(_options)
17
+ V3_METHODS
18
+ end
19
+
20
+ # Returns [Hash<Symbol:String>] what replaces #encoding and #cleaner, see `IOStreams::Reader.v3_stream_advice`.
21
+ def self.v3_stream_advice
22
+ V3_STREAM_ADVICE
23
+ end
24
+
8
25
  attr_reader :encoding, :cleaner
9
26
 
10
27
  NOT_PRINTABLE = /[^[:print:]|\r\n]/
@@ -60,7 +77,11 @@ module IOStreams
60
77
 
61
78
  # Returns [String] data returned from the input stream.
62
79
  # Returns [nil] if end of file and no further data was read.
63
- def read(size = nil)
80
+ #
81
+ # As IO#read does, the data is read into the supplied buffer, which is returned, and which is empty once the end
82
+ # of the file was reached, so that `IO.copy_stream`, which supplies one, can read it, such as for a copy without a
83
+ # mode from a stream with an encoding.
84
+ def read(size = nil, outbuf = nil)
64
85
  block =
65
86
  if @read_cache_buffer
66
87
  begin
@@ -75,11 +96,18 @@ module IOStreams
75
96
  end
76
97
 
77
98
  # EOF reached?
78
- return unless block
99
+ unless block
100
+ outbuf&.clear
101
+ return
102
+ end
79
103
 
104
+ block = text(block, size) if text_bytes?(block)
80
105
  block = block.encode(@encoding, **@encoding_options) unless block.encoding == @encoding
81
106
  block = @cleaner.call(block, @replace) if @cleaner
82
- block
107
+ return outbuf.replace(block) if outbuf
108
+
109
+ # A copy of the buffer that it reads into, which the next read writes over, so that the caller keeps the data.
110
+ block.equal?(@read_cache_buffer) ? block.dup : block
83
111
  end
84
112
 
85
113
  def self.extract_cleaner(cleaner)
@@ -95,6 +123,29 @@ module IOStreams
95
123
  cleaner
96
124
  end
97
125
  end
126
+
127
+ private
128
+
129
+ # Returns [true|false] whether the binary data that a read returned can hold the bytes of text in the encoding,
130
+ # rather than text to convert into it, see #text. Without `replace:` it reads into a buffer of the encoding, whose
131
+ # bytes are then the text, as `IO#read` keeps the encoding of the buffer that it is supplied for a read of a length.
132
+ # A stream that returns the bytes as binary instead, since its `#read` takes no buffer, as `Zlib::GzipReader` on
133
+ # JRuby, or makes the buffer binary, as `Bzip2::FFI::Reader` does, holds the same text, as does a read of the whole
134
+ # of the data, which `IO#read` returns as binary, and converting it from binary would reject each character that is
135
+ # not ASCII. With `replace:` it is converted from binary, replacing each character that is not ASCII, as before.
136
+ def text_bytes?(block)
137
+ @replace.nil? && block.encoding == Encoding::BINARY
138
+ end
139
+
140
+ # Returns [String] the bytes of the binary data as text in the encoding, in a String of its own, rather than the
141
+ # String that the input stream returned, which it can still hold, such as a buffer that it reuses. Those of a read
142
+ # of a length are taken as they are, as `IO#read` reads them into a buffer of the encoding, since a character can
143
+ # be split between two reads. The whole of the data is taken only when it is valid in the encoding; otherwise the
144
+ # data itself is returned, which is converted from binary, raising for each character that is not ASCII, as before.
145
+ def text(block, size)
146
+ text = ::String.new(block, encoding: @encoding)
147
+ size.nil? && !text.valid_encoding? ? block : text
148
+ end
98
149
  end
99
150
  end
100
151
  end
@@ -5,6 +5,23 @@ module IOStreams
5
5
  %i[encoding cleaner replace]
6
6
  end
7
7
 
8
+ # The methods that the stream that v3.0 supplies to a writer block for the encoding answers: those of an IO that
9
+ # write, #finish and #output_stream, see .v3_stream_methods.
10
+ V3_METHODS = [*V3_WRITE_METHODS, :finish, :output_stream].freeze
11
+ private_constant :V3_METHODS
12
+
13
+ # Returns [Array<Symbol>] the methods that the stream that v3.0 supplies to a writer block for the encoding answers,
14
+ # in place of this writer, see `IOStreams::Writer.v3_stream_methods`. It has no #encoding or #cleaner, which the
15
+ # settings of the path replace, see .v3_stream_advice.
16
+ def self.v3_stream_methods(_options)
17
+ V3_METHODS
18
+ end
19
+
20
+ # Returns [Hash<Symbol:String>] what replaces #encoding and #cleaner, see `IOStreams::Reader.v3_stream_advice`.
21
+ def self.v3_stream_advice
22
+ V3_STREAM_ADVICE
23
+ end
24
+
8
25
  attr_reader :encoding, :cleaner
9
26
 
10
27
  # Write a line at a time to a file or stream
@@ -50,7 +67,7 @@ module IOStreams
50
67
  # Write a line to the output stream
51
68
  #
52
69
  # Example:
53
- # IOStreams.path('a.txt').option(:encode, encoding: 'UTF-8').writer do |stream|
70
+ # IOStreams.path('a.txt').encoding('UTF-8').writer do |stream|
54
71
  # stream << 'first line' << 'second line'
55
72
  # end
56
73
  def <<(record)
@@ -62,7 +79,7 @@ module IOStreams
62
79
  # Returns [Integer] the number of bytes written.
63
80
  #
64
81
  # Example:
65
- # IOStreams.path('a.txt').option(:encode, encoding: 'UTF-8').writer do |stream|
82
+ # IOStreams.path('a.txt').encoding('UTF-8').writer do |stream|
66
83
  # count = stream.write('first line')
67
84
  # puts "Wrote #{count} bytes to the output file"
68
85
  # end