iostreams 1.2.1 → 1.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1dad581b0665992975c33f75b23f50964ae1311e025b7a1524fca4004f0ede2b
4
- data.tar.gz: 4db01e4d6c2d36ce522df3b323a6e0d9f42de0d1644a282a0cea06479e979289
3
+ metadata.gz: 2ff0af97e5f820b516ec7a7e695d1c4cb0f7ee7022d9463e6fe917e3c56554cd
4
+ data.tar.gz: 8f2831eb25377f9944280726e585efb524d0ab711da04aa6cb3a5872477e9635
5
5
  SHA512:
6
- metadata.gz: 4057a5c484129c60dbc9c84e462026da862900e17b0604b385164210f14814fbae6d065d015ee9171402eb9f793f33ac26c0ee7658f94b8cdeb0724c796cbe63
7
- data.tar.gz: 5a84fe37c1eebc775bd84b9903181ff035c325b1233ab64990e586f5b0bd3fd51c21d4f1429f9b0e8ab64733e9b63be5c5e05df7bf026e9d1d8c0cd8a7716417
6
+ metadata.gz: b5fe32aec626686316168b9bf627f01a86a7d0ab7aea6c56d4b522084a41ee7ac079ef9c6fb7fee20011db78ab9828adf39de75476dc33f5406b1d43b4066eb9
7
+ data.tar.gz: a59ded1990c6a7ffc13f42a65dc8f9dd89e1e42e19933c4b0aff026bdbf2712acc08c027293b44002ac9e937bd80916ffe4e4765b3903fcb85b286031de39acc
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
- # iostreams
1
+ # IOStreams
2
2
  [![Gem Version](https://img.shields.io/gem/v/iostreams.svg)](https://rubygems.org/gems/iostreams) [![Build Status](https://travis-ci.org/rocketjob/iostreams.svg?branch=master)](https://travis-ci.org/rocketjob/iostreams) [![Downloads](https://img.shields.io/gem/dt/iostreams.svg)](https://rubygems.org/gems/iostreams) [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](http://opensource.org/licenses/Apache-2.0) ![](https://img.shields.io/badge/status-Production%20Ready-blue.svg) [![Gitter chat](https://img.shields.io/badge/IRC%20(gitter)-Support-brightgreen.svg)](https://gitter.im/rocketjob/support)
3
3
 
4
- Input and Output streaming for Ruby.
4
+ IOStreams is an incredibly powerful streaming library that makes changes to file formats, compression, encryption,
5
+ or storage mechanism transparent to the application.
5
6
 
6
7
  ## Project Status
7
8
 
@@ -9,7 +10,9 @@ Production Ready, heavily used in production environments, many as part of Rocke
9
10
 
10
11
  ## Documentation
11
12
 
12
- [Semantic Logger Guide](http://rocketjob.github.io/iostreams)
13
+ Start with the [IOStreams tutorial](https://iostreams.rocketjob.io/tutorial) to get a great introduction to IOStreams.
14
+
15
+ Next, checkout the remaining [IOStreams documentation](https://iostreams.rocketjob.io/)
13
16
 
14
17
  ## Versioning
15
18
 
@@ -20,7 +20,7 @@ module IOStreams
20
20
  raise(ArgumentError, "Cannot call #option unless the `file_name` was already set}") unless file_name
21
21
 
22
22
  @options ||= {}
23
- if opts = @options[stream]
23
+ if (opts = @options[stream])
24
24
  opts.merge!(options)
25
25
  else
26
26
  @options[stream] = options.dup
@@ -40,7 +40,7 @@ module IOStreams
40
40
  raise(ArgumentError, "Invalid stream: #{stream.inspect}") unless IOStreams.extensions.include?(stream)
41
41
 
42
42
  @streams ||= {}
43
- if opts = @streams[stream]
43
+ if (opts = @streams[stream])
44
44
  opts.merge!(options)
45
45
  else
46
46
  @streams[stream] = options.dup
@@ -91,7 +91,8 @@ module IOStreams
91
91
  private
92
92
 
93
93
  def class_for_stream(type, stream)
94
- ext = IOStreams.extensions[stream.nil? ? nil : stream.to_sym] || raise(ArgumentError, "Unknown Stream type: #{stream.inspect}")
94
+ ext = IOStreams.extensions[stream.nil? ? nil : stream.to_sym] ||
95
+ raise(ArgumentError, "Unknown Stream type: #{stream.inspect}")
95
96
  ext.send("#{type}_class") || raise(ArgumentError, "No #{type} registered for Stream type: #{stream.inspect}")
96
97
  end
97
98
 
@@ -99,7 +100,7 @@ module IOStreams
99
100
  def parse_extensions
100
101
  parts = ::File.basename(file_name).split(".")
101
102
  extensions = []
102
- while extension = parts.pop
103
+ while (extension = parts.pop)
103
104
  sym = extension.downcase.to_sym
104
105
  break unless IOStreams.extensions[sym]
105
106
 
@@ -116,10 +117,12 @@ module IOStreams
116
117
  block.call(io_stream)
117
118
  elsif pipeline.size == 1
118
119
  stream, opts = pipeline.first
119
- class_for_stream(type, stream).open(io_stream, opts, &block)
120
+ class_for_stream(type, stream).open(io_stream, **opts, &block)
120
121
  else
121
122
  # Daisy chain multiple streams together
122
- last = pipeline.keys.inject(block) { |inner, stream_sym| ->(io) { class_for_stream(type, stream_sym).open(io, pipeline[stream_sym], &inner) } }
123
+ last = pipeline.keys.inject(block) do |inner, stream_sym|
124
+ ->(io) { class_for_stream(type, stream_sym).open(io, **pipeline[stream_sym], &inner) }
125
+ end
123
126
  last.call(io_stream)
124
127
  end
125
128
  end
@@ -206,7 +206,7 @@ module IOStreams
206
206
  elsif streams.is_a?(Array)
207
207
  streams.each { |stream| apply_old_style_streams(path, stream) }
208
208
  elsif streams.is_a?(Hash)
209
- streams.each_pair { |stream, options| path.stream(stream, options) }
209
+ streams.each_pair { |stream, options| path.stream(stream, **options) }
210
210
  else
211
211
  raise ArgumentError, "Invalid old style stream supplied: #{params.inspect}"
212
212
  end
@@ -73,7 +73,7 @@ module IOStreams
73
73
  # EOF reached?
74
74
  return unless block
75
75
 
76
- block = block.encode(@encoding, @encoding_options) unless block.encoding == @encoding
76
+ block = block.encode(@encoding, **@encoding_options) unless block.encoding == @encoding
77
77
  block = @cleaner.call(block, @replace) if @cleaner
78
78
  block
79
79
  end
@@ -66,7 +66,7 @@ module IOStreams
66
66
  return 0 if data.nil?
67
67
 
68
68
  data = data.to_s
69
- block = data.encoding == @encoding ? data : data.encode(@encoding, @encoding_options)
69
+ block = data.encoding == @encoding ? data : data.encode(@encoding, **@encoding_options)
70
70
  block = @cleaner.call(block, @replace) if @cleaner
71
71
  @output_stream.write(block)
72
72
  end
@@ -18,5 +18,15 @@ module IOStreams
18
18
  # When the specified delimiter is not found in the supplied stream / file
19
19
  class DelimiterNotFound < Error
20
20
  end
21
+
22
+ # Fixed length line has the wrong length
23
+ class InvalidLineLength < Error
24
+ end
25
+
26
+ class ValueTooLong < Error
27
+ end
28
+
29
+ class InvalidLayout < Error
30
+ end
21
31
  end
22
32
  end
@@ -58,7 +58,7 @@ module IOStreams
58
58
  end
59
59
 
60
60
  # For an existing IO Stream
61
- # IOStreams.stream(io).file_name('blah.zip').encoding('BINARY').reader(&:read)
61
+ # IOStreams.stream(io).file_name('blah.zip').encoding('BINARY').read
62
62
  # IOStreams.stream(io).file_name('blah.zip').encoding('BINARY').each(:line){ ... }
63
63
  # IOStreams.stream(io).file_name('blah.csv.zip').each(:line) { ... }
64
64
  # IOStreams.stream(io).stream(:zip).stream(:pgp, passphrase: 'receiver_passphrase').read
@@ -15,16 +15,16 @@ module IOStreams
15
15
  # Examples:
16
16
  #
17
17
  # # Case Insensitive file name lookup:
18
- # IOStreams::Paths::File.new("ruby").glob("r*.md") { |name| puts name }
18
+ # IOStreams.path("ruby").glob("r*.md") { |name| puts name }
19
19
  #
20
20
  # # Case Sensitive file name lookup:
21
- # IOStreams::Paths::File.new("ruby").each("R*.md", case_sensitive: true) { |name| puts name }
21
+ # IOStreams.path("ruby").each("R*.md", case_sensitive: true) { |name| puts name }
22
22
  #
23
23
  # # Also return the names of directories found during the search:
24
- # IOStreams::Paths::File.new("ruby").each("R*.md", directories: true) { |name| puts name }
24
+ # IOStreams.path("ruby").each("R*.md", directories: true) { |name| puts name }
25
25
  #
26
26
  # # Case Insensitive recursive file name lookup:
27
- # IOStreams::Paths::File.new("ruby").glob("**/*.md") { |name| puts name }
27
+ # IOStreams.path("ruby").glob("**/*.md") { |name| puts name }
28
28
  #
29
29
  # Parameters:
30
30
  # pattern [String]
@@ -26,16 +26,19 @@ module IOStreams
26
26
  #
27
27
  # http_redirect_count: [Integer]
28
28
  # Maximum number of http redirects to follow.
29
- def initialize(url, username: nil, password: nil, http_redirect_count: 10)
29
+ def initialize(url, username: nil, password: nil, http_redirect_count: 10, parameters: nil)
30
30
  uri = URI.parse(url)
31
31
  unless %w[http https].include?(uri.scheme)
32
- raise(ArgumentError, "Invalid URL. Required Format: 'http://<host_name>/<file_name>', or 'https://<host_name>/<file_name>'")
32
+ raise(
33
+ ArgumentError,
34
+ "Invalid URL. Required Format: 'http://<host_name>/<file_name>', or 'https://<host_name>/<file_name>'"
35
+ )
33
36
  end
34
37
 
35
38
  @username = username || uri.user
36
39
  @password = password || uri.password
37
40
  @http_redirect_count = http_redirect_count
38
- @url = url
41
+ @url = parameters ? "#{url}?#{URI.encode_www_form(parameters)}" : url
39
42
  super(uri.path)
40
43
  end
41
44
 
@@ -92,7 +92,7 @@ module IOStreams
92
92
  # encrypting data. This value is used to store the object and then it is
93
93
  # discarded; Amazon does not store the encryption key. The key must be
94
94
  # appropriate for use with the algorithm specified in the
95
- # x-amz-server-side​-encryption​-customer-algorithm header.
95
+ # x-amz-server-side-encryption-customer-algorithm header.
96
96
  #
97
97
  # @option params [String] :sse_customer_key_md5
98
98
  # Specifies the 128-bit MD5 digest of the encryption key according to
@@ -173,7 +173,10 @@ module IOStreams
173
173
  writer.close
174
174
  out = reader.read.chomp
175
175
  unless waith_thr.value.success?
176
- raise(Errors::CommunicationsFailure, "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}")
176
+ raise(
177
+ Errors::CommunicationsFailure,
178
+ "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
179
+ )
177
180
  end
178
181
 
179
182
  out
@@ -183,7 +186,10 @@ module IOStreams
183
186
  rescue StandardError
184
187
  nil
185
188
  end
186
- raise(Errors::CommunicationsFailure, "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}")
189
+ raise(
190
+ Errors::CommunicationsFailure,
191
+ "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
192
+ )
187
193
  end
188
194
  end
189
195
  end
@@ -201,7 +207,10 @@ module IOStreams
201
207
  writer.close
202
208
  out = reader.read.chomp
203
209
  unless waith_thr.value.success?
204
- raise(Errors::CommunicationsFailure, "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}")
210
+ raise(
211
+ Errors::CommunicationsFailure,
212
+ "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
213
+ )
205
214
  end
206
215
 
207
216
  out
@@ -211,7 +220,10 @@ module IOStreams
211
220
  rescue StandardError
212
221
  nil
213
222
  end
214
- raise(Errors::CommunicationsFailure, "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}")
223
+ raise(
224
+ Errors::CommunicationsFailure,
225
+ "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
226
+ )
215
227
  end
216
228
  end
217
229
  end
@@ -46,7 +46,15 @@ module IOStreams
46
46
  # `SecureRandom.urlsafe_base64(128)`
47
47
  #
48
48
  # See `man gpg` for the remaining options
49
- def self.generate_key(name:, email:, comment: nil, passphrase:, key_type: "RSA", key_length: 4096, subkey_type: "RSA", subkey_length: key_length, expire_date: nil)
49
+ def self.generate_key(name:,
50
+ email:,
51
+ comment: nil,
52
+ passphrase:,
53
+ key_type: "RSA",
54
+ key_length: 4096,
55
+ subkey_type: "RSA",
56
+ subkey_length: key_length,
57
+ expire_date: nil)
50
58
  version_check
51
59
  params = ""
52
60
  params << "Key-Type: #{key_type}\n" if key_type
@@ -63,12 +71,11 @@ module IOStreams
63
71
 
64
72
  out, err, status = Open3.capture3(command, binmode: true, stdin_data: params)
65
73
  logger&.debug { "IOStreams::Pgp.generate_key: #{command}\n#{params}\n#{err}#{out}" }
66
- if status.success?
67
- if match = err.match(/gpg: key ([0-9A-F]+)\s+/)
68
- match[1]
69
- end
70
- else
71
- raise(Pgp::Failure, "GPG Failed to generate key: #{err}#{out}")
74
+
75
+ raise(Pgp::Failure, "GPG Failed to generate key: #{err}#{out}") unless status.success?
76
+
77
+ if (match = err.match(/gpg: key ([0-9A-F]+)\s+/))
78
+ match[1]
72
79
  end
73
80
  end
74
81
 
@@ -77,7 +84,8 @@ module IOStreams
77
84
  # Returns false if no key was found.
78
85
  # Raises an exception if it fails to delete the key.
79
86
  #
80
- # email: [String] Email address for the key.
87
+ # email: [String] Optional email address for the key.
88
+ # key_id: [String] Optional id for the key.
81
89
  #
82
90
  # public: [true|false]
83
91
  # Whether to delete the public key
@@ -86,12 +94,12 @@ module IOStreams
86
94
  # private: [true|false]
87
95
  # Whether to delete the private key
88
96
  # Default: false
89
- def self.delete_keys(email:, public: true, private: false)
97
+ def self.delete_keys(email: nil, key_id: nil, public: true, private: false)
90
98
  version_check
91
99
  method_name = pgp_version.to_f >= 2.2 ? :delete_public_or_private_keys : :delete_public_or_private_keys_v1
92
100
  status = false
93
- status = send(method_name, email: email, private: true) if private
94
- status = send(method_name, email: email, private: false) if public
101
+ status = send(method_name, email: email, key_id: key_id, private: true) if private
102
+ status = send(method_name, email: email, key_id: key_id, private: false) if public
95
103
  status
96
104
  end
97
105
 
@@ -150,16 +158,15 @@ module IOStreams
150
158
 
151
159
  out, err, status = Open3.capture3(command, binmode: true, stdin_data: key)
152
160
  logger&.debug { "IOStreams::Pgp.key_info: #{command}\n#{err}#{out}" }
153
- if status.success? && out.length.positive?
154
- # Sample Output:
155
- #
156
- # pub 4096R/3A5456F5 2017-06-07
157
- # uid Joe Bloggs <j@bloggs.net>
158
- # sub 4096R/2C9B240B 2017-06-07
159
- parse_list_output(out)
160
- else
161
- raise(Pgp::Failure, "GPG Failed extracting key details: #{err} #{out}")
162
- end
161
+
162
+ raise(Pgp::Failure, "GPG Failed extracting key details: #{err} #{out}") unless status.success? && out.length.positive?
163
+
164
+ # Sample Output:
165
+ #
166
+ # pub 4096R/3A5456F5 2017-06-07
167
+ # uid Joe Bloggs <j@bloggs.net>
168
+ # sub 4096R/2C9B240B 2017-06-07
169
+ parse_list_output(out)
163
170
  end
164
171
 
165
172
  # Returns [String] containing all the public keys for the supplied email address.
@@ -181,11 +188,10 @@ module IOStreams
181
188
 
182
189
  out, err, status = Open3.capture3(command, binmode: true)
183
190
  logger&.debug { "IOStreams::Pgp.export: #{command}\n#{err}" }
184
- if status.success? && out.length.positive?
185
- out
186
- else
187
- raise(Pgp::Failure, "GPG Failed reading key: #{email}: #{err}")
188
- end
191
+
192
+ raise(Pgp::Failure, "GPG Failed reading key: #{email}: #{err}") unless status.success? && out.length.positive?
193
+
194
+ out
189
195
  end
190
196
 
191
197
  # Imports the supplied public/private key
@@ -251,11 +257,14 @@ module IOStreams
251
257
  def self.import_and_trust(key:)
252
258
  raise(ArgumentError, "Key cannot be empty") if key.nil? || (key == "")
253
259
 
254
- email = key_info(key: key).last.fetch(:email)
255
- raise(ArgumentError, "Recipient email cannot be extracted from supplied key") unless email
260
+ key_info = key_info(key: key).last
261
+
262
+ email = key_info.fetch(:email, nil)
263
+ key_id = key_info.fetch(:key_id, nil)
264
+ raise(ArgumentError, "Recipient email or key id cannot be extracted from supplied key") unless email || key_id
256
265
 
257
266
  import(key: key)
258
- set_trust(email: email)
267
+ set_trust(email: email, key_id: key_id)
259
268
  email
260
269
  end
261
270
 
@@ -266,20 +275,19 @@ module IOStreams
266
275
  #
267
276
  # After importing keys, they are not trusted and the relevant trust level must be set.
268
277
  # Default: 5 : Ultimate
269
- def self.set_trust(email:, level: 5)
278
+ def self.set_trust(email: nil, key_id: nil, level: 5)
270
279
  version_check
271
- fingerprint = fingerprint(email: email)
280
+ fingerprint = key_id || fingerprint(email: email)
272
281
  return unless fingerprint
273
282
 
274
283
  command = "#{executable} --import-ownertrust"
275
284
  trust = "#{fingerprint}:#{level + 1}:\n"
276
285
  out, err, status = Open3.capture3(command, stdin_data: trust)
277
286
  logger&.debug { "IOStreams::Pgp.set_trust: #{command}\n#{err}#{out}" }
278
- if status.success?
279
- err
280
- else
281
- raise(Pgp::Failure, "GPG Failed trusting key: #{err} #{out}")
282
- end
287
+
288
+ raise(Pgp::Failure, "GPG Failed trusting key: #{err} #{out}") unless status.success?
289
+
290
+ err
283
291
  end
284
292
 
285
293
  # DEPRECATED - Use key_ids instead of fingerprints
@@ -287,18 +295,18 @@ module IOStreams
287
295
  version_check
288
296
  Open3.popen2e("#{executable} --list-keys --fingerprint --with-colons #{email}") do |_stdin, out, waith_thr|
289
297
  output = out.read.chomp
290
- if waith_thr.value.success?
291
- output.each_line do |line|
292
- if match = line.match(/\Afpr.*::([^\:]*):\Z/)
293
- return match[1]
294
- end
298
+ unless waith_thr.value.success?
299
+ unless output =~ /(public key not found|No public key)/i
300
+ raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email}: #{output}")
295
301
  end
296
- nil
297
- else
298
- return if output =~ /(public key not found|No public key)/i
302
+ end
299
303
 
300
- raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email}: #{output}")
304
+ output.each_line do |line|
305
+ if (match = line.match(/\Afpr.*::([^\:]*):\Z/))
306
+ return match[1]
307
+ end
301
308
  end
309
+ nil
302
310
  end
303
311
  end
304
312
 
@@ -328,7 +336,7 @@ module IOStreams
328
336
  # CAMELLIA128, CAMELLIA192, CAMELLIA256
329
337
  # Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
330
338
  # Compression: Uncompressed, ZIP, ZLIB, BZIP2
331
- if match = out.lines.first.match(/(\d+\.\d+.\d+)/)
339
+ if (match = out.lines.first.match(/(\d+\.\d+.\d+)/))
332
340
  match[1]
333
341
  end
334
342
  else
@@ -348,9 +356,12 @@ module IOStreams
348
356
  end
349
357
 
350
358
  def self.version_check
351
- if pgp_version.to_f >= 2.3
352
- raise(Pgp::UnsupportedVersion, "Version #{pgp_version} of #{executable} is not yet supported. You are welcome to submit a Pull Request.")
353
- end
359
+ return unless pgp_version.to_f >= 2.3
360
+
361
+ raise(
362
+ Pgp::UnsupportedVersion,
363
+ "Version #{pgp_version} of #{executable} is not yet supported. Please submit a Pull Request to support it."
364
+ )
354
365
  end
355
366
 
356
367
  # v2.2.1 output:
@@ -370,7 +381,7 @@ module IOStreams
370
381
  results = []
371
382
  hash = {}
372
383
  out.each_line do |line|
373
- if match = line.match(/(pub|sec)\s+(\D+)(\d+)\s+(\d+-\d+-\d+)\s+(.*)/)
384
+ if (match = line.match(/(pub|sec)\s+(\D+)(\d+)\s+(\d+-\d+-\d+)\s+(.*)/))
374
385
  # v2.2: pub rsa1024 2017-10-24 [SCEA]
375
386
  hash = {
376
387
  private: match[1] == "sec",
@@ -382,7 +393,7 @@ module IOStreams
382
393
  match[4]
383
394
  end)
384
395
  }
385
- elsif match = line.match(%r{(pub|sec)\s+(\d+)(.*)/(\w+)\s+(\d+-\d+-\d+)(\s+(.+)<(.+)>)?})
396
+ elsif (match = line.match(%r{(pub|sec)\s+(\d+)(.*)/(\w+)\s+(\d+-\d+-\d+)(\s+(.+)<(.+)>)?}))
386
397
  # Matches: pub 2048R/C7F9D9CB 2016-10-26
387
398
  # Or: pub 2048R/C7F9D9CB 2016-10-26 Receiver <receiver@example.org>
388
399
  hash = {
@@ -403,7 +414,7 @@ module IOStreams
403
414
  results << hash
404
415
  hash = {}
405
416
  end
406
- elsif match = line.match(/uid\s+(\[(.+)\]\s+)?(.+)<(.+)>/)
417
+ elsif (match = line.match(/uid\s+(\[(.+)\]\s+)?(.+)<(.+)>/))
407
418
  # Matches: uid [ unknown] Joe Bloggs <j@bloggs.net>
408
419
  # Or: uid Joe Bloggs <j@bloggs.net>
409
420
  # v2.2: uid [ultimate] Joe Bloggs <pgp_test@iostreams.net>
@@ -412,7 +423,15 @@ module IOStreams
412
423
  hash[:trust] = match[2].to_s.strip if match[1]
413
424
  results << hash
414
425
  hash = {}
415
- elsif match = line.match(/([A-Z0-9]+)/)
426
+ elsif (match = line.match(/uid\s+(\[(.+)\]\s+)?(.+)/))
427
+ # Matches: uid [ unknown] Joe Bloggs
428
+ # Or: uid Joe Bloggs
429
+ # v2.2: uid [ultimate] Joe Bloggs
430
+ hash[:name] = match[3].to_s.strip
431
+ hash[:trust] = match[2].to_s.strip if match[1]
432
+ results << hash
433
+ hash = {}
434
+ elsif (match = line.match(/([A-Z0-9]+)/))
416
435
  # v2.2 18A0FC1C09C0D8AE34CE659257DC4AE323C7368C
417
436
  hash[:key_id] ||= match[1]
418
437
  end
@@ -420,10 +439,10 @@ module IOStreams
420
439
  results
421
440
  end
422
441
 
423
- def self.delete_public_or_private_keys(email:, private: false)
442
+ def self.delete_public_or_private_keys(email: nil, key_id: nil, private: false)
424
443
  keys = private ? "secret-keys" : "keys"
425
444
 
426
- list = list_keys(email: email, private: private)
445
+ list = email ? list_keys(email: email, private: private) : list_keys(key_id: key_id)
427
446
  return false if list.empty?
428
447
 
429
448
  list.each do |key_info|
@@ -435,17 +454,17 @@ module IOStreams
435
454
  logger&.debug { "IOStreams::Pgp.delete_keys: #{command}\n#{err}#{out}" }
436
455
 
437
456
  unless status.success?
438
- raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email}: #{err}: #{out}")
457
+ raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email || key_id}: #{err}: #{out}")
439
458
  end
440
- raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email} #{err.strip}:#{out}") if out.include?("error")
459
+ raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email || key_id} #{err.strip}:#{out}") if out.include?("error")
441
460
  end
442
461
  true
443
462
  end
444
463
 
445
- def self.delete_public_or_private_keys_v1(email:, private: false)
464
+ def self.delete_public_or_private_keys_v1(email: nil, key_id: nil, private: false)
446
465
  keys = private ? "secret-keys" : "keys"
447
466
 
448
- command = "for i in `#{executable} --list-#{keys} --with-colons --fingerprint #{email} | grep \"^fpr\" | cut -d: -f10`; do\n"
467
+ command = "for i in `#{executable} --list-#{keys} --with-colons --fingerprint #{email || key_id} | grep \"^fpr\" | cut -d: -f10`; do\n"
449
468
  command << "#{executable} --batch --no-tty --yes --delete-#{keys} \"$i\" ;\n"
450
469
  command << "done"
451
470
 
@@ -454,9 +473,9 @@ module IOStreams
454
473
 
455
474
  return false if err =~ /(not found|no public key)/i
456
475
  unless status.success?
457
- raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email}: #{err}: #{out}")
476
+ raise(Pgp::Failure, "GPG Failed calling #{executable} to delete #{keys} for #{email || key_id}: #{err}: #{out}")
458
477
  end
459
- raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email} #{err.strip}: #{out}") if out.include?("error")
478
+ raise(Pgp::Failure, "GPG Failed to delete #{keys} for #{email || key_id} #{err.strip}: #{out}") if out.include?("error")
460
479
 
461
480
  true
462
481
  end
@@ -17,7 +17,7 @@ module IOStreams
17
17
  #
18
18
  # Example:
19
19
  #
20
- # IOStreams.path('tempfile2527').stream(:zip).stream(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
20
+ # IOStreams.path("tempfile2527").stream(:zip).stream(:pgp, passphrase: "receiver_passphrase").read
21
21
  def stream(stream, **options)
22
22
  builder.stream(stream, **options)
23
23
  self
@@ -27,12 +27,12 @@ module IOStreams
27
27
  # If the relevant stream is not found for this file it is ignored.
28
28
  # For example, if the file does not have a pgp extension then the pgp option is not relevant.
29
29
  #
30
- # IOStreams.path('keep_safe.pgp').option(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
30
+ # IOStreams.path("keep_safe.pgp").option(:pgp, passphrase: "receiver_passphrase").read
31
31
  #
32
32
  # # In this case the file is not pgp so the `passphrase` option is ignored.
33
- # IOStreams.path('keep_safe.enc').option(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
33
+ # IOStreams.path("keep_safe.enc").option(:pgp, passphrase: "receiver_passphrase").read
34
34
  #
35
- # IOStreams.path(output_file_name).option(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
35
+ # IOStreams.path(output_file_name).option(:pgp, passphrase: "receiver_passphrase").read
36
36
  def option(stream, **options)
37
37
  builder.option(stream, **options)
38
38
  self
@@ -282,7 +282,12 @@ module IOStreams
282
282
  def line_reader(embedded_within: nil, **args)
283
283
  embedded_within = '"' if embedded_within.nil? && builder.file_name&.include?(".csv")
284
284
 
285
- stream_reader { |io| yield IOStreams::Line::Reader.new(io, original_file_name: builder.file_name, embedded_within: embedded_within, **args) }
285
+ stream_reader do |io|
286
+ yield IOStreams::Line::Reader.new(io,
287
+ original_file_name: builder.file_name,
288
+ embedded_within: embedded_within,
289
+ **args)
290
+ end
286
291
  end
287
292
 
288
293
  # Iterate over a file / stream returning each line as an array, one at a time.
@@ -306,19 +311,25 @@ module IOStreams
306
311
  def line_writer(**args, &block)
307
312
  return block.call(io_stream) if io_stream&.is_a?(IOStreams::Line::Writer)
308
313
 
309
- writer { |io| IOStreams::Line::Writer.stream(io, original_file_name: builder.file_name, **args, &block) }
314
+ writer do |io|
315
+ IOStreams::Line::Writer.stream(io, original_file_name: builder.file_name, **args, &block)
316
+ end
310
317
  end
311
318
 
312
319
  def row_writer(delimiter: $/, **args, &block)
313
320
  return block.call(io_stream) if io_stream&.is_a?(IOStreams::Row::Writer)
314
321
 
315
- line_writer(delimiter: delimiter) { |io| IOStreams::Row::Writer.stream(io, original_file_name: builder.file_name, **args, &block) }
322
+ line_writer(delimiter: delimiter) do |io|
323
+ IOStreams::Row::Writer.stream(io, original_file_name: builder.file_name, **args, &block)
324
+ end
316
325
  end
317
326
 
318
327
  def record_writer(delimiter: $/, **args, &block)
319
328
  return block.call(io_stream) if io_stream&.is_a?(IOStreams::Record::Writer)
320
329
 
321
- line_writer(delimiter: delimiter) { |io| IOStreams::Record::Writer.stream(io, original_file_name: builder.file_name, **args, &block) }
330
+ line_writer(delimiter: delimiter) do |io|
331
+ IOStreams::Record::Writer.stream(io, original_file_name: builder.file_name, **args, &block)
332
+ end
322
333
  end
323
334
  end
324
335
  end
@@ -89,7 +89,7 @@ module IOStreams
89
89
  else
90
90
  self.class.parser_class(format)
91
91
  end
92
- @parser = format_options ? klass.new(format_options) : klass.new
92
+ @parser = format_options ? klass.new(**format_options) : klass.new
93
93
  end
94
94
 
95
95
  # Returns [true|false] whether a header is still required in order to parse or render the current format.
@@ -142,7 +142,10 @@ module IOStreams
142
142
  return unless requires_header?
143
143
 
144
144
  if IOStreams::Utils.blank?(header.columns)
145
- raise(Errors::MissingHeader, "Header columns must be set before attempting to render a header for format: #{format.inspect}")
145
+ raise(
146
+ Errors::MissingHeader,
147
+ "Header columns must be set before attempting to render a header for format: #{format.inspect}"
148
+ )
146
149
  end
147
150
 
148
151
  parser.render(header.columns, header)
@@ -109,7 +109,10 @@ module IOStreams
109
109
  end
110
110
 
111
111
  unless row.is_a?(Array)
112
- raise(IOStreams::Errors::TypeMismatch, "Don't know how to convert #{row.class.name} to an Array without the header columns being set.")
112
+ raise(
113
+ IOStreams::Errors::TypeMismatch,
114
+ "Don't know how to convert #{row.class.name} to an Array without the header columns being set."
115
+ )
113
116
  end
114
117
 
115
118
  row
@@ -3,31 +3,66 @@ module IOStreams
3
3
  module Parser
4
4
  # Parsing and rendering fixed length data
5
5
  class Fixed < Base
6
- attr_reader :fixed_layout
6
+ attr_reader :layout, :truncate
7
7
 
8
8
  # Returns [IOStreams::Tabular::Parser]
9
9
  #
10
10
  # Parameters:
11
11
  # layout: [Array<Hash>]
12
12
  # [
13
- # {key: 'name', size: 23 },
14
- # {key: 'address', size: 40 },
15
- # {key: 'zip', size: 5 }
13
+ # {size: 23, key: "name"},
14
+ # {size: 40, key: "address"},
15
+ # {size: 2},
16
+ # {size: 5, key: "zip"},
17
+ # {size: 8, key: "age", type: :integer},
18
+ # {size: 10, key: "weight", type: :float, decimals: 2}
16
19
  # ]
17
- def initialize(layout:)
18
- @fixed_layout = parse_layout(layout)
20
+ #
21
+ # Notes:
22
+ # * Leave out the name of the key to ignore that column during parsing,
23
+ # and to space fill when rendering. For example as a filler.
24
+ #
25
+ # Types:
26
+ # :string
27
+ # This is the default type.
28
+ # Applies space padding and the value is left justified.
29
+ # Returns value as a String
30
+ # :integer
31
+ # Applies zero padding to the left.
32
+ # Returns value as an Integer
33
+ # Raises Errors::ValueTooLong when the supplied value cannot be rendered in `size` characters.
34
+ # :float
35
+ # Applies zero padding to the left.
36
+ # Returns value as a float.
37
+ # The :size is the total size of this field including the `.` and the decimals.
38
+ # Number of :decimals
39
+ # Raises Errors::ValueTooLong when the supplied value cannot be rendered in `size` characters.
40
+ def initialize(layout:, truncate: true)
41
+ @layout = Layout.new(layout)
42
+ @truncate = truncate
43
+ end
44
+
45
+ # The required line length for every fixed length line
46
+ def line_length
47
+ layout.length
19
48
  end
20
49
 
21
50
  # Returns [String] fixed layout values extracted from the supplied hash.
22
- # String will be encoded to `encoding`
51
+ #
52
+ # Notes:
53
+ # * A nil value is considered an empty string
54
+ # * When a supplied value exceeds the column size it is truncated.
23
55
  def render(row, header)
24
56
  hash = header.to_hash(row)
25
57
 
26
58
  result = ""
27
- fixed_layout.each do |map|
28
- # A nil value is considered an empty string
29
- value = hash[map.key].to_s
30
- result << format("%-#{map.size}.#{map.size}s", value)
59
+ layout.columns.each do |column|
60
+ value = hash[column.key].to_s
61
+ if !truncate && (value.length > column.size)
62
+ raise(Errors::ValueTooLong, "Value: #{value.inspect} is too long to fit into column #{column.key} of size #{column.size}")
63
+ end
64
+
65
+ result << column.render(value)
31
66
  end
32
67
  result
33
68
  end
@@ -36,32 +71,102 @@ module IOStreams
36
71
  # String will be encoded to `encoding`
37
72
  def parse(line)
38
73
  unless line.is_a?(String)
39
- raise(IOStreams::Errors::TypeMismatch, "Format is :fixed. Invalid parse input: #{line.class.name}")
74
+ raise(Errors::TypeMismatch, "Line must be a String when format is :fixed. Actual: #{line.class.name}")
75
+ end
76
+
77
+ if line.length != layout.length
78
+ raise(Errors::InvalidLineLength, "Expected line length: #{layout.length}, actual line length: #{line.length}")
40
79
  end
41
80
 
42
81
  hash = {}
43
82
  index = 0
44
- fixed_layout.each do |map|
45
- value = line[index..(index + map.size - 1)]
46
- index += map.size
47
- hash[map.key] = value.to_s.strip
83
+ layout.columns.each do |column|
84
+ # Ignore "columns" that have no keys. E.g. Fillers
85
+ hash[column.key] = column.parse(line[index, column.size]) if column.key
86
+ index += column.size
48
87
  end
49
88
  hash
50
89
  end
51
90
 
52
91
  private
53
92
 
54
- FixedLayout = Struct.new(:key, :size)
93
+ class Layout
94
+ attr_reader :columns, :length
95
+
96
+ # Returns [Array<FixedLayout>] the layout for this fixed width file.
97
+ # Also validates values
98
+ def initialize(layout)
99
+ @length = 0
100
+ @columns = parse_layout(layout)
101
+ end
102
+
103
+ private
104
+
105
+ def parse_layout(layout)
106
+ @length = 0
107
+ layout.collect do |hash|
108
+ raise(Errors::InvalidLayout, "Missing required :size in: #{hash.inspect}") unless hash.key?(:size)
109
+
110
+ column = Column.new(**hash)
111
+ @length += column.size
112
+ column
113
+ end
114
+ end
115
+ end
116
+
117
+ class Column
118
+ TYPES = %i[string integer float].freeze
119
+
120
+ attr_reader :key, :size, :type, :decimals
121
+
122
+ def initialize(key: nil, size:, type: :string, decimals: 2)
123
+ @key = key
124
+ @size = size.to_i
125
+ @type = type.to_sym
126
+ @decimals = decimals
127
+
128
+ raise(Errors::InvalidLayout, "Size #{size.inspect} must be positive") unless @size.positive?
129
+ raise(Errors::InvalidLayout, "Unknown type: #{type.inspect}") unless TYPES.include?(type)
130
+ end
131
+
132
+ def parse(value)
133
+ return if value.nil?
134
+
135
+ stripped_value = value.to_s.strip
136
+
137
+ case type
138
+ when :string
139
+ stripped_value
140
+ when :integer
141
+ stripped_value.length.zero? ? nil : value.to_i
142
+ when :float
143
+ stripped_value.length.zero? ? nil : value.to_f
144
+ else
145
+ raise(Errors::InvalidLayout, "Unsupported type: #{type.inspect}")
146
+ end
147
+ end
148
+
149
+ def render(value)
150
+ case type
151
+ when :string
152
+ format("%-#{size}.#{size}s", value.to_s)
153
+ when :integer
154
+ formatted = format("%0#{size}d", value.to_i)
155
+ if formatted.length > size
156
+ raise(Errors::ValueTooLong, "Value: #{value} is too large to fit into column:#{key} of size:#{size}")
157
+ end
55
158
 
56
- # Returns [Array<FixedLayout>] the layout for this fixed width file.
57
- # Also validates values
58
- def parse_layout(layout)
59
- layout.collect do |map|
60
- size = map[:size]
61
- key = map[:key]
62
- raise(ArgumentError, "Missing required :key and :size in: #{map.inspect}") unless size && key
159
+ formatted
160
+ when :float
161
+ formatted = format("%0#{size}.#{decimals}f", value.to_f)
162
+ if formatted.length > size
163
+ raise(Errors::ValueTooLong, "Value: #{value} is too large to fit into column:#{key} of size:#{size}")
164
+ end
63
165
 
64
- FixedLayout.new(key, size)
166
+ formatted
167
+ else
168
+ raise(Errors::InvalidLayout, "Unsupported type: #{type.inspect}")
169
+ end
65
170
  end
66
171
  end
67
172
  end
@@ -49,10 +49,10 @@ module IOStreams
49
49
  @user = uri.user
50
50
  @password = uri.password
51
51
  @port = uri.port
52
- if uri.query
53
- @query = {}
54
- ::URI.decode_www_form(uri.query).each { |key, value| @query[key] = value }
55
- end
52
+ return unless uri.query
53
+
54
+ @query = {}
55
+ ::URI.decode_www_form(uri.query).each { |key, value| @query[key] = value }
56
56
  end
57
57
  end
58
58
  end
@@ -1,3 +1,3 @@
1
1
  module IOStreams
2
- VERSION = "1.2.1".freeze
2
+ VERSION = "1.3.0".freeze
3
3
  end
@@ -38,7 +38,7 @@ module IOStreams
38
38
  return true
39
39
  end
40
40
 
41
- while entry = zin.get_next_entry
41
+ while (entry = zin.get_next_entry)
42
42
  return true if entry.name == entry_file_name
43
43
  end
44
44
  false
@@ -16,7 +16,7 @@ module IOStreams
16
16
  end
17
17
 
18
18
  let :json_file_name do
19
- "/tmp/io_streams/abc.json"
19
+ "/tmp/iostreams_abc.json"
20
20
  end
21
21
 
22
22
  describe ".root" do
@@ -90,7 +90,7 @@ module IOStreams
90
90
  it "hash reader detects json format from file name" do
91
91
  ::File.open(json_file_name, "wb") { |file| file.write(expected_json) }
92
92
  rows = []
93
- path = IOStreams.path("/tmp/io_streams/abc.json")
93
+ path = IOStreams.path(json_file_name)
94
94
  path.each(:hash) do |row|
95
95
  rows << row
96
96
  end
@@ -171,7 +171,7 @@ module Paths
171
171
 
172
172
  describe "reader" do
173
173
  it "reads file" do
174
- assert_equal data, file_path.reader(&:read)
174
+ assert_equal data, file_path.read
175
175
  end
176
176
  end
177
177
 
@@ -73,7 +73,7 @@ module Paths
73
73
 
74
74
  describe "#reader" do
75
75
  it "reads" do
76
- assert_equal raw, existing_path.reader(&:read)
76
+ assert_equal raw, existing_path.read
77
77
  end
78
78
  end
79
79
 
@@ -89,7 +89,7 @@ module Paths
89
89
 
90
90
  describe "#writer" do
91
91
  it "writes" do
92
- assert_equal raw.size, write_path.writer { |io| io.write(raw) }
92
+ assert_equal(raw.size, write_path.writer { |io| io.write(raw) })
93
93
  assert write_path.exist?
94
94
  assert_equal raw, write_path.read
95
95
  end
@@ -42,7 +42,7 @@ module Paths
42
42
 
43
43
  describe "#reader" do
44
44
  it "reads" do
45
- assert_equal raw, existing_path.reader(&:read)
45
+ assert_equal raw, existing_path.read
46
46
  end
47
47
 
48
48
  it "fails when the file does not exist" do
@@ -60,7 +60,7 @@ module Paths
60
60
 
61
61
  describe "#writer" do
62
62
  it "writes" do
63
- assert_equal raw.size, write_path.writer { |io| io.write(raw) }
63
+ assert_equal(raw.size, write_path.writer { |io| io.write(raw) })
64
64
  assert_equal raw, write_path.read
65
65
  end
66
66
 
@@ -77,7 +77,7 @@ module Paths
77
77
 
78
78
  it "writes" do
79
79
  skip "No identity file env var set: SFTP_IDENTITY_FILE" unless ENV["SFTP_IDENTITY_FILE"]
80
- assert_equal raw.size, write_path.writer { |io| io.write(raw) }
80
+ assert_equal(raw.size, write_path.writer { |io| io.write(raw) })
81
81
  assert_equal raw, write_path.read
82
82
  end
83
83
  end
@@ -90,7 +90,7 @@ module Paths
90
90
 
91
91
  it "writes" do
92
92
  skip "No identity file env var set: SFTP_IDENTITY_FILE" unless ENV["SFTP_IDENTITY_FILE"]
93
- assert_equal raw.size, write_path.writer { |io| io.write(raw) }
93
+ assert_equal(raw.size, write_path.writer { |io| io.write(raw) })
94
94
  assert_equal raw, write_path.read
95
95
  end
96
96
  end
@@ -70,7 +70,7 @@ class PgpTest < Minitest::Test
70
70
  refute IOStreams::Pgp.delete_keys(email: "random@iostreams.net", public: true, private: true)
71
71
  end
72
72
 
73
- it "deletes existing keys" do
73
+ it "deletes existing keys with specified email" do
74
74
  generated_key_id
75
75
  # There is a timing issue with creating and then deleting keys.
76
76
  # Call list_keys again to give GnuPGP time.
@@ -78,7 +78,16 @@ class PgpTest < Minitest::Test
78
78
  assert IOStreams::Pgp.delete_keys(email: email, public: true, private: true)
79
79
  end
80
80
 
81
- it "deletes just the private key" do
81
+ it "deletes existing keys with specified key_id" do
82
+ generated_key_id
83
+
84
+ # There is a timing issue with creating and then deleting keys.
85
+ # Call list_keys again to give GnuPGP time.
86
+ IOStreams::Pgp.list_keys(key_id: generated_key_id, private: true)
87
+ assert IOStreams::Pgp.delete_keys(key_id: generated_key_id, public: true, private: true)
88
+ end
89
+
90
+ it "deletes just the private key with specified email" do
82
91
  generated_key_id
83
92
  # There is a timing issue with creating and then deleting keys.
84
93
  # Call list_keys again to give GnuPGP time.
@@ -87,6 +96,16 @@ class PgpTest < Minitest::Test
87
96
  refute IOStreams::Pgp.key?(key_id: generated_key_id, private: true)
88
97
  assert IOStreams::Pgp.key?(key_id: generated_key_id, private: false)
89
98
  end
99
+
100
+ it "deletes just the private key with specified key_id" do
101
+ generated_key_id
102
+ # There is a timing issue with creating and then deleting keys.
103
+ # Call list_keys again to give GnuPGP time.
104
+ IOStreams::Pgp.list_keys(key_id: generated_key_id, private: true)
105
+ assert IOStreams::Pgp.delete_keys(key_id: generated_key_id, public: false, private: true)
106
+ refute IOStreams::Pgp.key?(key_id: generated_key_id, private: true)
107
+ assert IOStreams::Pgp.key?(key_id: generated_key_id, private: false)
108
+ end
90
109
  end
91
110
 
92
111
  describe ".export" do
@@ -113,7 +132,7 @@ class PgpTest < Minitest::Test
113
132
  IOStreams::Pgp.list_keys(email: email)
114
133
  end
115
134
 
116
- it "lists public keys" do
135
+ it "lists public keys for email" do
117
136
  assert keys = IOStreams::Pgp.list_keys(email: email)
118
137
  assert_equal 1, keys.size
119
138
  assert key = keys.first
@@ -130,7 +149,24 @@ class PgpTest < Minitest::Test
130
149
  assert_equal "ultimate", key[:trust] if (ver.to_f >= 2) && (maint >= 30)
131
150
  end
132
151
 
133
- it "lists private keys" do
152
+ it "lists public keys for key_id" do
153
+ assert keys = IOStreams::Pgp.list_keys(key_id: generated_key_id)
154
+ assert_equal 1, keys.size
155
+ assert key = keys.first
156
+
157
+ assert_equal Date.today, key[:date]
158
+ assert_equal email, key[:email]
159
+ assert_includes key[:key_id], generated_key_id
160
+ assert_equal 1024, key[:key_length]
161
+ assert_includes %w[R rsa], key[:key_type]
162
+ assert_equal user_name, key[:name]
163
+ refute key[:private], key
164
+ ver = IOStreams::Pgp.pgp_version
165
+ maint = ver.split(".").last.to_i
166
+ assert_equal "ultimate", key[:trust] if (ver.to_f >= 2) && (maint >= 30)
167
+ end
168
+
169
+ it "lists private keys for email" do
134
170
  assert keys = IOStreams::Pgp.list_keys(email: email, private: true)
135
171
  assert_equal 1, keys.size
136
172
  assert key = keys.first
@@ -143,6 +179,20 @@ class PgpTest < Minitest::Test
143
179
  assert_equal user_name, key[:name]
144
180
  assert key[:private], key
145
181
  end
182
+
183
+ it "lists private keys for key_id" do
184
+ assert keys = IOStreams::Pgp.list_keys(key_id: generated_key_id, private: true)
185
+ assert_equal 1, keys.size
186
+ assert key = keys.first
187
+
188
+ assert_equal Date.today, key[:date]
189
+ assert_equal email, key[:email]
190
+ assert_includes key[:key_id], generated_key_id
191
+ assert_equal 1024, key[:key_length]
192
+ assert_includes %w[R rsa], key[:key_type]
193
+ assert_equal user_name, key[:name]
194
+ assert key[:private], key
195
+ end
146
196
  end
147
197
 
148
198
  describe ".key_info" do
@@ -53,9 +53,9 @@ class PgpWriterTest < Minitest::Test
53
53
  end
54
54
 
55
55
  it "supports multiple recipients" do
56
- IOStreams::Pgp::Writer.file(file_name, recipient: %w[receiver@example.org receiver2@example.org], signer: "sender@example.org", signer_passphrase: "sender_passphrase") do |io|
57
- io.write(decrypted)
58
- end
56
+ IOStreams::Pgp::Writer.file(file_name, recipient: %w[receiver@example.org receiver2@example.org], signer: "sender@example.org", signer_passphrase: "sender_passphrase") do |io|
57
+ io.write(decrypted)
58
+ end
59
59
 
60
60
  result = IOStreams::Pgp::Reader.file(file_name, passphrase: "receiver_passphrase", &:read)
61
61
  assert_equal decrypted, result
@@ -138,33 +138,47 @@ class TabularTest < Minitest::Test
138
138
  describe ":fixed format" do
139
139
  let :tabular do
140
140
  layout = [
141
- {key: "name", size: 23},
142
- {key: "address", size: 40},
143
- {key: "zip", size: 5}
141
+ {size: 23, key: :name},
142
+ {size: 40, key: :address},
143
+ {size: 2},
144
+ {size: 5, key: :zip, type: :integer},
145
+ {size: 8, key: :age, type: :integer},
146
+ {size: 10, key: :weight, type: :float, decimals: 2}
144
147
  ]
145
148
  IOStreams::Tabular.new(format: :fixed, format_options: {layout: layout})
146
149
  end
147
150
 
148
151
  it "parses to hash" do
149
- assert hash = tabular.record_parse("Jack over there 34618")
150
- assert_equal({"name" => "Jack", "address" => "over there", "zip" => "34618"}, hash)
152
+ assert hash = tabular.record_parse("Jack over there XX34618012345670012345.01")
153
+ assert_equal({name: "Jack", address: "over there", zip: 34_618, age: 1_234_567, weight: 12_345.01}, hash)
151
154
  end
152
155
 
153
156
  it "parses short string" do
154
- # TODO: Raise exception on lines that are too short?
155
- assert hash = tabular.record_parse("Jack over th")
156
- assert_equal({"name" => "Jack", "address" => "over th", "zip" => ""}, hash)
157
+ assert_raises IOStreams::Errors::InvalidLineLength do
158
+ tabular.record_parse("Jack over th")
159
+ end
157
160
  end
158
161
 
159
162
  it "parses longer string" do
160
- # TODO: Raise exception on lines that are too long?
161
- assert hash = tabular.record_parse("Jack over there 34618........................................")
162
- assert_equal({"name" => "Jack", "address" => "over there", "zip" => "34618"}, hash)
163
+ assert_raises IOStreams::Errors::InvalidLineLength do
164
+ tabular.record_parse("Jack over there XX34618012345670012345.01............")
165
+ end
166
+ end
167
+
168
+ it "parses zero values" do
169
+ assert hash = tabular.record_parse(" 00000000000000000000000")
170
+ assert_equal({name: "", address: "", zip: 0, age: 0, weight: 0.0}, hash)
171
+ end
172
+
173
+ it "parses empty values" do
174
+ assert hash = tabular.record_parse(" XX ")
175
+ assert_equal({name: "", address: "", zip: nil, age: nil, weight: nil}, hash)
163
176
  end
164
177
 
165
- it "parses empty strings" do
166
- assert hash = tabular.record_parse(" 34618")
167
- assert_equal({"name" => "", "address" => "", "zip" => "34618"}, hash)
178
+ it "parses blank strings" do
179
+ skip "TODO: Part of tabular refactor to get this working"
180
+ assert hash = tabular.record_parse(" ")
181
+ assert_equal({name: "", address: "", zip: nil, age: nil, weight: nil}, hash)
168
182
  end
169
183
 
170
184
  it "parses nil data as nil" do
@@ -224,31 +238,46 @@ class TabularTest < Minitest::Test
224
238
  describe ":fixed format" do
225
239
  let :tabular do
226
240
  layout = [
227
- {key: "name", size: 23},
228
- {key: "address", size: 40},
229
- {key: "zip", size: 5}
241
+ {size: 23, key: :name},
242
+ {size: 40, key: :address},
243
+ {size: 2},
244
+ {size: 5, key: :zip, type: :integer},
245
+ {size: 8, key: :age, type: :integer},
246
+ {size: 10, key: :weight, type: :float, decimals: 2}
230
247
  ]
231
248
  IOStreams::Tabular.new(format: :fixed, format_options: {layout: layout})
232
249
  end
233
250
 
234
251
  it "renders fixed data" do
235
- assert string = tabular.render({"name" => "Jack", "address" => "over there", "zip" => 34_618, "phone" => "5551231234"})
236
- assert_equal "Jack over there 34618", string
252
+ assert string = tabular.render(name: "Jack", address: "over there", zip: 34_618, weight: 123_456.789123, age: 21)
253
+ assert_equal "Jack over there 34618000000210123456.79", string
237
254
  end
238
255
 
239
- it "truncates long data" do
240
- assert string = tabular.render({"name" => "Jack", "address" => "over there", "zip" => 3_461_832_653_653_265, "phone" => "5551231234"})
241
- assert_equal "Jack over there 34618", string
256
+ it "truncates long strings" do
257
+ assert string = tabular.render(name: "Jack ran up the beanstalk and when jack reached the top it was truncated", address: "over there", zip: 34_618)
258
+ assert_equal "Jack ran up the beanstaover there 34618000000000000000.00", string
259
+ end
260
+
261
+ it "when integer is too large" do
262
+ assert_raises IOStreams::Errors::ValueTooLong do
263
+ tabular.render(zip: 3_461_832_653_653_265)
264
+ end
265
+ end
266
+
267
+ it "when float is too large" do
268
+ assert_raises IOStreams::Errors::ValueTooLong do
269
+ tabular.render(weight: 3_461_832_653_653_265.234)
270
+ end
242
271
  end
243
272
 
244
273
  it "renders nil as empty string" do
245
- assert string = tabular.render("zip" => 3_461_832_653_653_265)
246
- assert_equal " 34618", string
274
+ assert string = tabular.render(zip: 34_618)
275
+ assert_equal " 34618000000000000000.00", string
247
276
  end
248
277
 
249
278
  it "renders boolean" do
250
- assert string = tabular.render({"name" => true, "address" => false, "zip" => nil, "phone" => "5551231234"})
251
- assert_equal "true false ", string
279
+ assert string = tabular.render(name: true, address: false)
280
+ assert_equal "true false 00000000000000000000.00", string
252
281
  end
253
282
 
254
283
  it "renders no data as nil" do
@@ -4,7 +4,7 @@ require "yaml"
4
4
  require "minitest/autorun"
5
5
  require "minitest/reporters"
6
6
  require "iostreams"
7
- require "awesome_print"
7
+ require "amazing_print"
8
8
  require "symmetric-encryption"
9
9
 
10
10
  # Since PGP libraries use UTC for Dates
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iostreams
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-19 00:00:00.000000000 Z
11
+ date: 2020-07-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -130,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
130
  - !ruby/object:Gem::Version
131
131
  version: '0'
132
132
  requirements: []
133
- rubygems_version: 3.0.6
133
+ rubygems_version: 3.0.8
134
134
  signing_key:
135
135
  specification_version: 4
136
136
  summary: Input and Output streaming for Ruby.