iostreams 0.20.3 → 1.0.0.beta

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/lib/io_streams/bzip2/reader.rb +9 -21
  3. data/lib/io_streams/bzip2/writer.rb +9 -21
  4. data/lib/io_streams/deprecated.rb +217 -0
  5. data/lib/io_streams/encode/reader.rb +12 -16
  6. data/lib/io_streams/encode/writer.rb +9 -13
  7. data/lib/io_streams/errors.rb +6 -6
  8. data/lib/io_streams/gzip/reader.rb +7 -14
  9. data/lib/io_streams/gzip/writer.rb +7 -15
  10. data/lib/io_streams/io_streams.rb +182 -524
  11. data/lib/io_streams/line/reader.rb +9 -9
  12. data/lib/io_streams/line/writer.rb +10 -11
  13. data/lib/io_streams/path.rb +190 -0
  14. data/lib/io_streams/paths/file.rb +176 -0
  15. data/lib/io_streams/paths/http.rb +92 -0
  16. data/lib/io_streams/paths/matcher.rb +61 -0
  17. data/lib/io_streams/paths/s3.rb +269 -0
  18. data/lib/io_streams/paths/sftp.rb +99 -0
  19. data/lib/io_streams/pgp.rb +47 -19
  20. data/lib/io_streams/pgp/reader.rb +20 -28
  21. data/lib/io_streams/pgp/writer.rb +24 -46
  22. data/lib/io_streams/reader.rb +28 -0
  23. data/lib/io_streams/record/reader.rb +20 -16
  24. data/lib/io_streams/record/writer.rb +28 -28
  25. data/lib/io_streams/row/reader.rb +22 -26
  26. data/lib/io_streams/row/writer.rb +29 -28
  27. data/lib/io_streams/stream.rb +400 -0
  28. data/lib/io_streams/streams.rb +125 -0
  29. data/lib/io_streams/symmetric_encryption/reader.rb +5 -13
  30. data/lib/io_streams/symmetric_encryption/writer.rb +16 -15
  31. data/lib/io_streams/tabular/header.rb +9 -3
  32. data/lib/io_streams/tabular/parser/array.rb +8 -3
  33. data/lib/io_streams/tabular/parser/csv.rb +6 -2
  34. data/lib/io_streams/tabular/parser/hash.rb +4 -1
  35. data/lib/io_streams/tabular/parser/json.rb +3 -1
  36. data/lib/io_streams/tabular/parser/psv.rb +3 -1
  37. data/lib/io_streams/tabular/utility/csv_row.rb +9 -8
  38. data/lib/io_streams/utils.rb +22 -0
  39. data/lib/io_streams/version.rb +1 -1
  40. data/lib/io_streams/writer.rb +28 -0
  41. data/lib/io_streams/xlsx/reader.rb +7 -19
  42. data/lib/io_streams/zip/reader.rb +7 -26
  43. data/lib/io_streams/zip/writer.rb +21 -38
  44. data/lib/iostreams.rb +15 -15
  45. data/test/bzip2_reader_test.rb +3 -3
  46. data/test/bzip2_writer_test.rb +3 -3
  47. data/test/deprecated_test.rb +123 -0
  48. data/test/encode_reader_test.rb +3 -3
  49. data/test/encode_writer_test.rb +6 -6
  50. data/test/gzip_reader_test.rb +2 -2
  51. data/test/gzip_writer_test.rb +3 -3
  52. data/test/io_streams_test.rb +43 -136
  53. data/test/line_reader_test.rb +20 -20
  54. data/test/line_writer_test.rb +3 -3
  55. data/test/path_test.rb +30 -28
  56. data/test/paths/file_test.rb +206 -0
  57. data/test/paths/http_test.rb +34 -0
  58. data/test/paths/matcher_test.rb +111 -0
  59. data/test/paths/s3_test.rb +207 -0
  60. data/test/pgp_reader_test.rb +8 -8
  61. data/test/pgp_writer_test.rb +13 -13
  62. data/test/record_reader_test.rb +5 -5
  63. data/test/record_writer_test.rb +4 -4
  64. data/test/row_reader_test.rb +5 -5
  65. data/test/row_writer_test.rb +6 -6
  66. data/test/stream_test.rb +116 -0
  67. data/test/streams_test.rb +255 -0
  68. data/test/utils_test.rb +20 -0
  69. data/test/xlsx_reader_test.rb +3 -3
  70. data/test/zip_reader_test.rb +12 -12
  71. data/test/zip_writer_test.rb +5 -5
  72. metadata +33 -45
  73. data/lib/io_streams/base_path.rb +0 -72
  74. data/lib/io_streams/file/path.rb +0 -58
  75. data/lib/io_streams/file/reader.rb +0 -12
  76. data/lib/io_streams/file/writer.rb +0 -22
  77. data/lib/io_streams/http/reader.rb +0 -71
  78. data/lib/io_streams/s3.rb +0 -26
  79. data/lib/io_streams/s3/path.rb +0 -40
  80. data/lib/io_streams/s3/reader.rb +0 -28
  81. data/lib/io_streams/s3/writer.rb +0 -85
  82. data/lib/io_streams/sftp/reader.rb +0 -67
  83. data/lib/io_streams/sftp/writer.rb +0 -68
  84. data/test/base_path_test.rb +0 -35
  85. data/test/file_path_test.rb +0 -97
  86. data/test/file_reader_test.rb +0 -33
  87. data/test/file_writer_test.rb +0 -50
  88. data/test/http_reader_test.rb +0 -38
  89. data/test/s3_reader_test.rb +0 -41
  90. data/test/s3_writer_test.rb +0 -41
@@ -2,34 +2,31 @@ require 'open3'
2
2
 
3
3
  module IOStreams
4
4
  module Pgp
5
- class Reader
5
+ class Reader < IOStreams::Reader
6
6
  # Passphrase to use to open the private key to decrypt the received file
7
- def self.default_passphrase=(default_passphrase)
8
- @default_passphrase = default_passphrase
7
+ class << self
8
+ attr_writer :default_passphrase
9
+
10
+ private
11
+
12
+ attr_reader :default_passphrase
13
+
14
+ @default_passphrase = nil
9
15
  end
10
16
 
11
- # Read from a PGP / GPG file or stream, decompressing the contents as it is read
12
- # file_name_or_io: [String|IO]
17
+ # Read from a PGP / GPG file , decompressing the contents as it is read.
18
+ #
19
+ # file_name: [String]
13
20
  # Name of file to read from
14
- # Or, the IO stream to receive the decrypted contents
21
+ #
15
22
  # passphrase: [String]
16
23
  # Pass phrase for private key to decrypt the file with
17
- def self.open(file_name_or_io, passphrase: self.default_passphrase, binary: true, &block)
24
+ def self.file(file_name, passphrase: self.default_passphrase)
18
25
  raise(ArgumentError, 'Missing both passphrase and IOStreams::Pgp::Reader.default_passphrase') unless passphrase
19
26
 
20
- return read_file(file_name_or_io, passphrase: passphrase, binary: binary, &block) if file_name_or_io.is_a?(String)
21
-
22
- # PGP can only work against a file, not a stream, so create temp file.
23
- IOStreams::File::Path.temp_file_name('iostreams_pgp') do |temp_file_name|
24
- IOStreams.copy(file_name_or_io, temp_file_name, target_options: {streams: []})
25
- read_file(temp_file_name, passphrase: passphrase, binary: binary, &block)
26
- end
27
- end
28
-
29
- def self.read_file(file_name, passphrase: self.default_passphrase, binary: true)
30
27
  loopback = IOStreams::Pgp.pgp_version.to_f >= 2.1 ? '--pinentry-mode loopback' : ''
31
28
  command = "#{IOStreams::Pgp.executable} #{loopback} --batch --no-tty --yes --decrypt --passphrase-fd 0 #{file_name}"
32
- IOStreams::Pgp.logger.debug { "IOStreams::Pgp::Reader.open: #{command}" } if IOStreams::Pgp.logger
29
+ IOStreams::Pgp.logger&.debug { "IOStreams::Pgp::Reader.open: #{command}" }
33
30
 
34
31
  # Read decrypted contents from stdout
35
32
  Open3.popen3(command) do |stdin, stdout, stderr, waith_thr|
@@ -37,24 +34,19 @@ module IOStreams
37
34
  stdin.close
38
35
  result =
39
36
  begin
40
- stdout.binmode if binary
37
+ stdout.binmode
41
38
  yield(stdout)
42
39
  rescue Errno::EPIPE
43
40
  # Ignore broken pipe because gpg terminates early due to an error
44
41
  raise(Pgp::Failure, "GPG Failed reading from encrypted file: #{file_name}: #{stderr.read.chomp}")
45
42
  end
46
- raise(Pgp::Failure, "GPG Failed to decrypt file: #{file_name}: #{stderr.read.chomp}") unless waith_thr.value.success?
43
+ unless waith_thr.value.success?
44
+ raise(Pgp::Failure, "GPG Failed to decrypt file: #{file_name}: #{stderr.read.chomp}")
45
+ end
46
+
47
47
  result
48
48
  end
49
49
  end
50
-
51
- private
52
-
53
- @default_passphrase = nil
54
-
55
- def self.default_passphrase
56
- @default_passphrase
57
- end
58
50
  end
59
51
  end
60
52
  end
@@ -2,24 +2,29 @@ require 'open3'
2
2
 
3
3
  module IOStreams
4
4
  module Pgp
5
- class Writer
6
- # Sign all encrypted files with this users key.
7
- # Default: Do not sign encyrpted files.
8
- def self.default_signer=(default_signer)
9
- @default_signer = default_signer
10
- end
5
+ class Writer < IOStreams::Writer
6
+ class << self
7
+ # Sign all encrypted files with this users key.
8
+ # Default: Do not sign encrypted files.
9
+ attr_writer :default_signer
10
+
11
+ # Passphrase to use to open the private key when signing the file.
12
+ # Default: None.
13
+ attr_writer :default_signer_passphrase
14
+
15
+ private
11
16
 
12
- # Passphrase to use to open the private key when signing the file.
13
- # Default: None.
14
- def self.default_signer_passphrase=(default_signer_passphrase)
15
- @default_signer_passphrase = default_signer_passphrase
17
+ attr_reader :default_signer_passphrase
18
+ attr_reader :default_signer
19
+
20
+ @default_signer_passphrase = nil
21
+ @default_signer = nil
16
22
  end
17
23
 
18
- # Write to a PGP / GPG file or stream, encrypting the contents as it is written
24
+ # Write to a PGP / GPG file, encrypting the contents as it is written.
19
25
  #
20
- # file_name_or_io: [String|IO]
26
+ # file_name: [String]
21
27
  # Name of file to write to.
22
- # Or, the IO stream to write the encrypted contents to.
23
28
  #
24
29
  # recipient: [String]
25
30
  # Email of user for which to encypt the file.
@@ -32,10 +37,6 @@ module IOStreams
32
37
  # Passphrase to use to open the private key when signing the file.
33
38
  # Default: default_signer_passphrase
34
39
  #
35
- # binary: [true|false]
36
- # Whether to write binary data.
37
- # Default: true
38
- #
39
40
  # compression: [:none|:zip|:zlib|:bzip2]
40
41
  # Note: Standard PGP only supports :zip.
41
42
  # :zlib is better than zip.
@@ -45,22 +46,12 @@ module IOStreams
45
46
  # compress_level: [Integer]
46
47
  # Compression level
47
48
  # Default: 6
48
- def self.open(file_name_or_io, recipient:, signer: default_signer, signer_passphrase: default_signer_passphrase, binary: true, compression: :zip, compress_level: 6, &block)
49
- compress_level = 0 if compression == :none
50
-
51
- if file_name_or_io.is_a?(String)
52
- IOStreams::File::Path.mkpath(file_name_or_io)
53
- return write_file(file_name_or_io, recipient: recipient, signer: signer, signer_passphrase: signer_passphrase, binary: binary, compression: compression, compress_level: compress_level, &block)
54
- end
49
+ def self.file(file_name, recipient: nil, import_and_trust_key: nil, signer: default_signer, signer_passphrase: default_signer_passphrase, compression: :zip, compress_level: 6, original_file_name: nil)
50
+ raise(ArgumentError, "Requires either :recipient or :import_and_trust_key") unless recipient || import_and_trust_key
55
51
 
56
- # PGP can only work against a file, not a stream, so create temp file.
57
- IOStreams::File::Path.temp_file_name('iostreams_pgp') do |temp_file_name|
58
- write_file(temp_file_name, recipient: recipient, signer: signer, signer_passphrase: signer_passphrase, binary: binary, compression: compression, compress_level: compress_level, &block)
59
- IOStreams.copy(temp_file_name, file_name_or_io, source_options: {streams: []})
60
- end
61
- end
52
+ recipient = IOStreams::Pgp.import_and_trust(key: import_and_trust_key) if import_and_trust_key
53
+ compress_level = 0 if compression == :none
62
54
 
63
- def self.write_file(file_name, recipient:, signer: default_signer, signer_passphrase: default_signer_passphrase, binary: true, compression: :zip, compress_level: 6)
64
55
  # Write to stdin, with encrypted contents being written to the file
65
56
  command = "#{IOStreams::Pgp.executable} --batch --no-tty --yes --encrypt"
66
57
  command << " --sign --local-user \"#{signer}\"" if signer
@@ -72,11 +63,11 @@ module IOStreams
72
63
  command << " --compress-algo #{compression}" unless compression == :none
73
64
  command << " --recipient \"#{recipient}\" -o \"#{file_name}\""
74
65
 
75
- IOStreams::Pgp.logger.debug { "IOStreams::Pgp::Writer.open: #{command}" } if IOStreams::Pgp.logger
66
+ IOStreams::Pgp.logger&.debug { "IOStreams::Pgp::Writer.open: #{command}" }
76
67
 
77
68
  Open3.popen2e(command) do |stdin, out, waith_thr|
78
69
  begin
79
- stdin.binmode if binary
70
+ stdin.binmode
80
71
  yield(stdin)
81
72
  stdin.close
82
73
  rescue Errno::EPIPE
@@ -91,19 +82,6 @@ module IOStreams
91
82
  end
92
83
  end
93
84
 
94
- private
95
-
96
- @default_signer_passphrase = nil
97
- @default_signer = nil
98
-
99
- def self.default_signer_passphrase
100
- @default_signer_passphrase
101
- end
102
-
103
- def self.default_signer
104
- @default_signer
105
- end
106
-
107
85
  end
108
86
  end
109
87
  end
@@ -0,0 +1,28 @@
1
+ module IOStreams
2
+ class Reader
3
+ # When a Reader does not support streams, we copy the stream to a local temp file
4
+ # and then pass that filename in for this reader.
5
+ def self.stream(input_stream, **args, &block)
6
+ Utils.temp_file_name("iostreams_reader") do |file_name|
7
+ ::File.open(file_name, 'wb') { |target| ::IO.copy_stream(input_stream, target) }
8
+ file(file_name, **args, &block)
9
+ end
10
+ end
11
+
12
+ # When a Writer supports streams, also allow it to simply support a file
13
+ def self.file(file_name, original_file_name: file_name, **args, &block)
14
+ ::File.open(file_name, 'rb') { |file| stream(file, original_file_name: original_file_name, **args, &block) }
15
+ end
16
+
17
+ # For processing by either a file name or an open IO stream.
18
+ def self.open(file_name_or_io, **args, &block)
19
+ file_name_or_io.is_a?(String) ? file(file_name_or_io, **args, &block) : stream(file_name_or_io, **args, &block)
20
+ end
21
+
22
+ attr_reader :input_stream
23
+
24
+ def initialize(input_stream)
25
+ @input_stream = input_stream
26
+ end
27
+ end
28
+ end
@@ -1,23 +1,23 @@
1
1
  module IOStreams
2
2
  module Record
3
3
  # Converts each line of an input stream into hash for every row
4
- class Reader
4
+ class Reader < IOStreams::Reader
5
5
  include Enumerable
6
6
 
7
- # Read a record as a Hash at a time from a file or stream.
8
- def self.open(file_name_or_io, delimiter: nil, buffer_size: 65536, encoding: nil, encode_cleaner: nil, encode_replace: nil, **args)
9
- if file_name_or_io.is_a?(String)
10
- IOStreams.line_reader(file_name_or_io,
11
- delimiter: delimiter,
12
- buffer_size: buffer_size,
13
- encoding: encoding,
14
- encode_cleaner: encode_cleaner,
15
- encode_replace: encode_replace
16
- ) do |io|
17
- yield new(io, file_name: file_name_or_io, **args)
18
- end
19
- else
20
- yield new(file_name_or_io, **args)
7
+ # Read a record at a time from a line stream
8
+ # Note:
9
+ # - The supplied stream _must_ already be a line stream, or a stream that responds to :each
10
+ def self.stream(line_reader, original_file_name: nil, **args, &block)
11
+ # Pass-through if already a record reader
12
+ return block.call(line_reader) if line_reader.is_a?(self.class)
13
+
14
+ yield new(line_reader, **args)
15
+ end
16
+
17
+ # When reading from a file also add the line reader stream
18
+ def self.file(file_name, original_file_name: file_name, delimiter: $/, **args)
19
+ IOStreams::Line::Reader.file(file_name, original_file_name: original_file_name, delimiter: delimiter) do |io|
20
+ yield new(io, **args)
21
21
  end
22
22
  end
23
23
 
@@ -33,8 +33,12 @@ module IOStreams
33
33
  #
34
34
  # For all other parameters, see Tabular::Header.new
35
35
  def initialize(line_reader, cleanse_header: true, **args)
36
+ unless line_reader.respond_to?(:each)
37
+ raise(ArgumentError, "Stream must be a IOStreams::Line::Reader or implement #each")
38
+ end
39
+
36
40
  @tabular = IOStreams::Tabular.new(**args)
37
- @line_reader = line_reader
41
+ @line_reader = line_reader
38
42
  @cleanse_header = cleanse_header
39
43
  end
40
44
 
@@ -1,28 +1,25 @@
1
1
  module IOStreams
2
2
  module Record
3
3
  # Example, implied header from first record:
4
- # IOStreams.record_writer do |stream|
4
+ # IOStreams.path('file.csv').record_writer do |stream|
5
5
  # stream << {name: 'Jack', address: 'Somewhere', zipcode: 12345}
6
6
  # stream << {name: 'Joe', address: 'Lost', zipcode: 32443, age: 23}
7
7
  # end
8
- #
9
- # Output:
10
- # name, add
11
- #
12
- class Writer
13
- # Write a record as a Hash at a time to a file or stream.
14
- def self.open(file_name_or_io, delimiter: $/, encoding: nil, encode_cleaner: nil, encode_replace: nil, **args)
15
- if file_name_or_io.is_a?(String)
16
- IOStreams.line_writer(file_name_or_io,
17
- delimiter: delimiter,
18
- encoding: encoding,
19
- encode_cleaner: encode_cleaner,
20
- encode_replace: encode_replace
21
- ) do |io|
22
- yield new(io, file_name: file_name_or_io, **args)
23
- end
24
- else
25
- yield new(file_name_or_io, **args)
8
+ class Writer < IOStreams::Writer
9
+ # Write a record as a Hash at a time to a stream.
10
+ # Note:
11
+ # - The supplied stream _must_ already be a line stream, or a stream that responds to :<<
12
+ def self.stream(line_writer, original_file_name: nil, **args, &block)
13
+ # Pass-through if already a row writer
14
+ return block.call(line_writer) if line_writer.is_a?(self.class)
15
+
16
+ yield new(line_writer, **args)
17
+ end
18
+
19
+ # When writing to a file also add the line writer stream
20
+ def self.file(file_name, original_file_name: file_name, delimiter: $/, **args, &block)
21
+ IOStreams::Line::Writer.file(file_name, original_file_name: original_file_name, delimiter: delimiter) do |io|
22
+ yield new(io, **args, &block)
26
23
  end
27
24
  end
28
25
 
@@ -37,24 +34,27 @@ module IOStreams
37
34
  # :csv, :hash, :array, :json, :psv, :fixed
38
35
  #
39
36
  # For all other parameters, see Tabular::Header.new
40
- #
41
- # columns: nil, allowed_columns: nil, required_columns: nil, skip_unknown: true)
42
- def initialize(delimited, columns: nil, **args)
43
- @tabular = IOStreams::Tabular.new(columns: columns, **args)
44
- @delimited = delimited
37
+ def initialize(line_writer, columns: nil, **args)
38
+ unless line_writer.respond_to?(:<<)
39
+ raise(ArgumentError, 'Stream must be a IOStreams::Line::Writer or implement #<<')
40
+ end
41
+
42
+ @tabular = IOStreams::Tabular.new(columns: columns, **args)
43
+ @line_writer = line_writer
45
44
 
46
45
  # Render header line when `columns` is supplied.
47
- @delimited << @tabular.render_header if columns && @tabular.requires_header?
46
+ @line_writer << @tabular.render_header if columns && @tabular.requires_header?
48
47
  end
49
48
 
50
49
  def <<(hash)
51
- raise(ArgumentError, 'Must supply a Hash') unless hash.is_a?(Hash)
50
+ raise(ArgumentError, '#<< only accepts a Hash argument') unless hash.is_a?(Hash)
51
+
52
52
  if @tabular.header?
53
53
  # Extract header from the keys from the first row when not supplied above.
54
54
  @tabular.header.columns = hash.keys
55
- @delimited << @tabular.render_header
55
+ @line_writer << @tabular.render_header
56
56
  end
57
- @delimited << @tabular.render(hash)
57
+ @line_writer << @tabular.render(hash)
58
58
  end
59
59
  end
60
60
  end
@@ -1,29 +1,21 @@
1
1
  module IOStreams
2
2
  module Row
3
3
  # Converts each line of an input stream into an array for every line
4
- class Reader
5
- # Read a line as an Array at a time from a file or stream.
6
- def self.open(file_name_or_io,
7
- delimiter: nil,
8
- buffer_size: 65_536,
9
- file_name: nil,
10
- encoding: nil,
11
- encode_cleaner: nil,
12
- encode_replace: nil,
13
- **args)
14
- if file_name_or_io.is_a?(String)
15
- IOStreams.line_reader(file_name_or_io,
16
- delimiter: delimiter,
17
- buffer_size: buffer_size,
18
- file_name: file_name,
19
- encoding: encoding,
20
- encode_cleaner: encode_cleaner,
21
- encode_replace: encode_replace
22
- ) do |io|
23
- yield new(io, file_name: file_name, **args)
24
- end
25
- else
26
- yield new(file_name_or_io, **args)
4
+ class Reader < IOStreams::Reader
5
+ # Read a line as an Array at a time from a stream.
6
+ # Note:
7
+ # - The supplied stream _must_ already be a line stream, or a stream that responds to :each
8
+ def self.stream(line_reader, original_file_name: nil, **args, &block)
9
+ # Pass-through if already a row reader
10
+ return block.call(line_reader) if line_reader.is_a?(self.class)
11
+
12
+ yield new(line_reader, **args)
13
+ end
14
+
15
+ # When reading from a file also add the line reader stream
16
+ def self.file(file_name, original_file_name: file_name, delimiter: $/, **args)
17
+ IOStreams::Line::Reader.file(file_name, original_file_name: original_file_name, delimiter: delimiter) do |io|
18
+ yield new(io, **args)
27
19
  end
28
20
  end
29
21
 
@@ -37,14 +29,18 @@ module IOStreams
37
29
  # :csv, :hash, :array, :json, :psv, :fixed
38
30
  #
39
31
  # For all other parameters, see Tabular::Header.new
40
- def initialize(delimited, cleanse_header: true, **args)
32
+ def initialize(line_reader, cleanse_header: true, **args)
33
+ unless line_reader.respond_to?(:each)
34
+ raise(ArgumentError, "Stream must be a IOStreams::Line::Reader or implement #each")
35
+ end
36
+
41
37
  @tabular = IOStreams::Tabular.new(**args)
42
- @delimited = delimited
38
+ @line_reader = line_reader
43
39
  @cleanse_header = cleanse_header
44
40
  end
45
41
 
46
42
  def each
47
- @delimited.each do |line|
43
+ @line_reader.each do |line|
48
44
  if @tabular.header?
49
45
  columns = @tabular.parse_header(line)
50
46
  @tabular.cleanse_header! if @cleanse_header
@@ -2,61 +2,62 @@ require 'csv'
2
2
  module IOStreams
3
3
  module Row
4
4
  # Example:
5
- # IOStreams.row_writer do |stream|
5
+ # IOStreams.path("file.csv").row_writer do |stream|
6
6
  # stream << ['name', 'address', 'zipcode']
7
7
  # stream << ['Jack', 'Somewhere', 12345]
8
8
  # stream << ['Joe', 'Lost', 32443]
9
9
  # end
10
- #
11
- # Output:
12
- # ...
13
- #
14
- class Writer
15
- # Write a record as a Hash at a time to a file or stream.
16
- def self.open(file_name_or_io, delimiter: $/, encoding: nil, encode_cleaner: nil, encode_replace: nil, **args)
17
- if file_name_or_io.is_a?(String)
18
- IOStreams.line_writer(file_name_or_io,
19
- delimiter: delimiter,
20
- encoding: encoding,
21
- encode_cleaner: encode_cleaner,
22
- encode_replace: encode_replace
23
- ) do |io|
24
- yield new(io, **args)
25
- end
26
- else
27
- yield new(file_name_or_io, **args)
10
+ class Writer < IOStreams::Writer
11
+ # Write a record from an Array at a time to a stream.
12
+ #
13
+ # Note:
14
+ # - The supplied stream _must_ already be a line stream, or a stream that responds to :<<
15
+ def self.stream(line_writer, original_file_name: nil, **args)
16
+ # Pass-through if already a row writer
17
+ return block.call(line_writer) if line_writer.is_a?(self.class)
18
+
19
+ yield new(line_writer, **args)
20
+ end
21
+
22
+ # When writing to a file also add the line writer stream
23
+ def self.file(file_name, original_file_name: file_name, delimiter: $/, **args, &block)
24
+ IOStreams::Line::Writer.file(file_name, original_file_name: original_file_name, delimiter: delimiter) do |io|
25
+ yield new(io, **args, &block)
28
26
  end
29
27
  end
30
28
 
31
29
  # Create a Tabular writer that takes individual rows as arrays.
32
30
  #
33
31
  # Parameters
34
- # delimited: [#<<]
32
+ # line_writer: [#<<]
35
33
  # Anything that accepts a line / record at a time when #<< is called on it.
36
34
  #
37
35
  # format: [Symbol]
38
36
  # :csv, :hash, :array, :json, :psv, :fixed
39
37
  #
40
38
  # For all other parameters, see Tabular::Header.new
41
- #
42
- # columns: nil, allowed_columns: nil, required_columns: nil, skip_unknown: true)
43
- def initialize(delimited, columns: nil, **args)
44
- @tabular = IOStreams::Tabular.new(columns: columns, **args)
45
- @delimited = delimited
39
+ def initialize(line_writer, columns: nil, **args)
40
+ unless line_writer.respond_to?(:<<)
41
+ raise(ArgumentError, 'Stream must be a IOStreams::Line::Writer or implement #<<')
42
+ end
43
+
44
+ @tabular = IOStreams::Tabular.new(columns: columns, **args)
45
+ @line_writer = line_writer
46
46
 
47
47
  # Render header line when `columns` is supplied.
48
- delimited << @tabular.render_header if columns && @tabular.requires_header?
48
+ line_writer << @tabular.render_header if columns && @tabular.requires_header?
49
49
  end
50
50
 
51
51
  # Supply a hash or an array to render
52
52
  def <<(array)
53
53
  raise(ArgumentError, 'Must supply an Array') unless array.is_a?(Array)
54
+
54
55
  if @tabular.header?
55
56
  # If header (columns) was not supplied as an argument, assume first line is the header.
56
57
  @tabular.header.columns = array
57
- @delimited << @tabular.render_header
58
+ @line_writer << @tabular.render_header
58
59
  else
59
- @delimited << @tabular.render(array)
60
+ @line_writer << @tabular.render(array)
60
61
  end
61
62
  end
62
63
  end