iostreams 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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.