iostreams 1.3.1 → 1.5.1

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: 39e8db61b0e6f258822125da6f8dc00a8cbd3f4bda52ccd03d38ba1adc6e9176
4
- data.tar.gz: 962ff677f70e411e1fc0a59f8e080c5ca43e066d1b1d71edaf1663d933098e6c
3
+ metadata.gz: c665e5c262a98de9ceccf1cf93f5bc391370d0b674f966d9e266b731a31d3b7f
4
+ data.tar.gz: 1ab8c125e49abc178ce4c1e94f36dfb9219011ece6c8a9bd65b1c5a5d2f14604
5
5
  SHA512:
6
- metadata.gz: 9b6d30a60c024cfeadd06180e240a89a29ef2b7999ef45b5b50d19e467c98626196c105eac45cec2013a5807cfc5f996377dc41ef1439bf4c55e49619a37a9a6
7
- data.tar.gz: f9c1f0db6f77a848a6e2f6370c51a95e78083bb6f8abe30f4d1a6946c7a1d4d2d10deaffa7c6a61e3237f69d36fba1da4a65aa4db94f7e30f8b0ac11b740ad87
6
+ metadata.gz: 63bec4c3602cd4ab699bcf73abe3d97b7b808c6559c378e67be99c1af1ab7ada84dd0753c91dd2fe7be0924690a1deb30b44b87f56cd4638c4954a6bfcd38796
7
+ data.tar.gz: 2b1138c5389747892a33b5213a42b0fe4ececabc421c824db03d5218d436725a95d0306c7b0a1cc2a4b0a8eb4e1b4192152f6d60f11892fb2980c418c7be1f80
@@ -2,11 +2,11 @@ module IOStreams
2
2
  module Bzip2
3
3
  class Reader < IOStreams::Reader
4
4
  # Read from a Bzip2 stream, decompressing the contents as it is read
5
- def self.stream(input_stream, **_args)
6
- Utils.load_soft_dependency("rbzip2", "Bzip2") unless defined?(RBzip2)
5
+ def self.stream(input_stream, **args)
6
+ Utils.load_soft_dependency("bzip2-ffi", "Bzip2", "bzip2/ffi") unless defined?(::Bzip2::FFI)
7
7
 
8
8
  begin
9
- io = RBzip2.default_adapter::Decompressor.new(input_stream)
9
+ io = ::Bzip2::FFI::Reader.new(input_stream, args)
10
10
  yield io
11
11
  ensure
12
12
  io&.close
@@ -2,11 +2,11 @@ module IOStreams
2
2
  module Bzip2
3
3
  class Writer < IOStreams::Writer
4
4
  # Write to a stream, compressing with Bzip2
5
- def self.stream(input_stream, original_file_name: nil, **_args)
6
- Utils.load_soft_dependency("rbzip2", "Bzip2") unless defined?(RBzip2)
5
+ def self.stream(input_stream, original_file_name: nil, **args)
6
+ Utils.load_soft_dependency("bzip2-ffi", "Bzip2", "bzip2/ffi") unless defined?(::Bzip2::FFI)
7
7
 
8
8
  begin
9
- io = RBzip2.default_adapter::Compressor.new(input_stream)
9
+ io = ::Bzip2::FFI::Writer.new(input_stream, args)
10
10
  yield io
11
11
  ensure
12
12
  io&.close
@@ -78,8 +78,6 @@ module IOStreams
78
78
  block
79
79
  end
80
80
 
81
- private
82
-
83
81
  def self.extract_cleaner(cleaner)
84
82
  return if cleaner.nil?
85
83
 
@@ -298,8 +298,6 @@ module IOStreams
298
298
  @schemes[scheme_name.nil? ? nil : scheme_name.to_sym] || raise(ArgumentError, "Unknown Scheme type: #{scheme_name.inspect}")
299
299
  end
300
300
 
301
- private
302
-
303
301
  Extension = Struct.new(:reader_class, :writer_class)
304
302
 
305
303
  # Hold root paths
@@ -63,11 +63,11 @@ module IOStreams
63
63
  # Auto-detect windows/linux line endings if not supplied. \n or \r\n
64
64
  @delimiter ||= auto_detect_line_endings
65
65
 
66
- if @buffer
67
- # Change the delimiters encoding to match that of the input stream
68
- @delimiter = @delimiter.encode(@buffer.encoding)
69
- @delimiter_size = @delimiter.size
70
- end
66
+ return unless @buffer
67
+
68
+ # Change the delimiters encoding to match that of the input stream
69
+ @delimiter = @delimiter.encode(@buffer.encoding)
70
+ @delimiter_size = @delimiter.size
71
71
  end
72
72
 
73
73
  # Iterate over every line in the file/stream passing each line to supplied block in turn.
@@ -82,6 +82,7 @@ module IOStreams
82
82
  end
83
83
 
84
84
  # Cleanup an incomplete write to the target "file" if the copy fails.
85
+ # rubocop:disable Lint/SuppressedException
85
86
  def copy_from(source, **args)
86
87
  super(source, **args)
87
88
  rescue StandardError => e
@@ -91,6 +92,7 @@ module IOStreams
91
92
  end
92
93
  raise(e)
93
94
  end
95
+ # rubocop:enable Lint/SuppressedException
94
96
 
95
97
  # Moves the file by copying it to the new path and then deleting the current path.
96
98
  # Returns [IOStreams::Path] the target path.
@@ -3,7 +3,7 @@ require "uri"
3
3
  module IOStreams
4
4
  module Paths
5
5
  class S3 < IOStreams::Path
6
- attr_reader :bucket_name, :client
6
+ attr_reader :bucket_name, :client, :options
7
7
 
8
8
  # Arguments:
9
9
  #
@@ -179,17 +179,36 @@ module IOStreams
179
179
  #
180
180
  # Notes:
181
181
  # - Can copy across buckets.
182
+ # - No stream conversions are applied.
182
183
  def move_to(target_path)
184
+ target = copy_to(target_path, convert: false)
185
+ delete
186
+ target
187
+ end
188
+
189
+ # Make S3 perform direct copies within S3 itself.
190
+ def copy_to(target_path, convert: true)
191
+ return super(target_path) if convert
192
+
183
193
  target = IOStreams.new(target_path)
184
194
  return super(target) unless target.is_a?(self.class)
185
195
 
186
196
  source_name = ::File.join(bucket_name, path)
187
- # TODO: Does/should it also copy metadata?
188
- client.copy_object(bucket: target.bucket_name, key: target.path, copy_source: source_name)
189
- delete
197
+ client.copy_object(options.merge(bucket: target.bucket_name, key: target.path, copy_source: source_name))
190
198
  target
191
199
  end
192
200
 
201
+ # Make S3 perform direct copies within S3 itself.
202
+ def copy_from(source_path, convert: true)
203
+ return super(source_path) if convert
204
+
205
+ source = IOStreams.new(source_path)
206
+ return super(source) unless source.is_a?(self.class)
207
+
208
+ source_name = ::File.join(source.bucket_name, source.path)
209
+ client.copy_object(options.merge(bucket: bucket_name, key: path, copy_source: source_name))
210
+ end
211
+
193
212
  # S3 logically creates paths when a key is set.
194
213
  def mkpath
195
214
  self
@@ -220,7 +239,7 @@ module IOStreams
220
239
  # Shortcut method if caller has a filename already with no other streams applied:
221
240
  def read_file(file_name)
222
241
  ::File.open(file_name, "wb") do |file|
223
- client.get_object(@options.merge(response_target: file, bucket: bucket_name, key: path))
242
+ client.get_object(options.merge(response_target: file, bucket: bucket_name, key: path))
224
243
  end
225
244
  end
226
245
 
@@ -248,10 +267,10 @@ module IOStreams
248
267
  # Use multipart file upload
249
268
  s3 = Aws::S3::Resource.new(client: client)
250
269
  obj = s3.bucket(bucket_name).object(path)
251
- obj.upload_file(file_name)
270
+ obj.upload_file(file_name, options)
252
271
  else
253
272
  ::File.open(file_name, "rb") do |file|
254
- client.put_object(@options.merge(bucket: bucket_name, key: path, body: file))
273
+ client.put_object(options.merge(bucket: bucket_name, key: path, body: file))
255
274
  end
256
275
  end
257
276
  end
@@ -71,7 +71,9 @@ module IOStreams
71
71
  # end
72
72
  #
73
73
  # # When using the sftp executable use an identity file instead of a password to authenticate:
74
- # IOStreams.path("sftp://test.com/path/file_name.csv", username: "jack", ssh_options: {IdentityFile: "~/.ssh/private_key"}).reader do |io|
74
+ # IOStreams.path("sftp://test.com/path/file_name.csv",
75
+ # username: "jack",
76
+ # ssh_options: {IdentityFile: "~/.ssh/private_key"}).reader do |io|
75
77
  # puts io.read
76
78
  # end
77
79
  def initialize(url, username: nil, password: nil, ssh_options: {})
@@ -122,7 +124,8 @@ module IOStreams
122
124
  # end
123
125
  #
124
126
  # Example Output:
125
- # sftp://sftp.example.org/a/b/c/test.txt {:type=>1, :size=>37, :owner=>"test_owner", :group=>"test_group", :permissions=>420, :atime=>1572378136, :mtime=>1572378136, :link_count=>1, :extended=>{}}
127
+ # sftp://sftp.example.org/a/b/c/test.txt {:type=>1, :size=>37, :owner=>"test_owner", :group=>"test_group",
128
+ # :permissions=>420, :atime=>1572378136, :mtime=>1572378136, :link_count=>1, :extended=>{}}
126
129
  def each_child(pattern = "*", case_sensitive: true, directories: false, hidden: false)
127
130
  Utils.load_soft_dependency("net-sftp", "SFTP glob capability", "net/sftp") unless defined?(Net::SFTP)
128
131
 
@@ -74,9 +74,10 @@ module IOStreams
74
74
 
75
75
  raise(Pgp::Failure, "GPG Failed to generate key: #{err}#{out}") unless status.success?
76
76
 
77
- if (match = err.match(/gpg: key ([0-9A-F]+)\s+/))
78
- match[1]
79
- end
77
+ match = err.match(/gpg: key ([0-9A-F]+)\s+/)
78
+ return unless match
79
+
80
+ match[1]
80
81
  end
81
82
 
82
83
  # Delete all private and public keys for a particular email.
@@ -110,11 +111,6 @@ module IOStreams
110
111
  !list_keys(email: email, key_id: key_id, private: private).empty?
111
112
  end
112
113
 
113
- # Deprecated
114
- def self.has_key?(**args)
115
- key?(**args)
116
- end
117
-
118
114
  # Returns [Array<Hash>] the list of keys.
119
115
  # Each Hash consists of:
120
116
  # key_length: [Integer]
@@ -232,7 +228,7 @@ module IOStreams
232
228
  err.each_line do |line|
233
229
  if line =~ /secret key imported/
234
230
  secret = true
235
- elsif match = line.match(/key\s+(\w+):\s+(\w+).+\"(.*)<(.*)>\"/)
231
+ elsif (match = line.match(/key\s+(\w+):\s+(\w+).+\"(.*)<(.*)>\"/))
236
232
  results << {
237
233
  key_id: match[1].to_s.strip,
238
234
  private: secret,
@@ -347,8 +343,6 @@ module IOStreams
347
343
  end
348
344
  end
349
345
 
350
- private
351
-
352
346
  @logger = nil
353
347
 
354
348
  def self.logger
@@ -177,7 +177,7 @@ module IOStreams
177
177
  end
178
178
 
179
179
  def copy_to(target, convert: true)
180
- target = IOStreams.path(target) unless target.is_a?(Stream)
180
+ target = IOStreams.new(target)
181
181
  target.copy_from(self, convert: convert)
182
182
  end
183
183
 
@@ -184,8 +184,6 @@ module IOStreams
184
184
  @formats.keys
185
185
  end
186
186
 
187
- private
188
-
189
187
  # A registry to hold formats for processing files during upload or download
190
188
  @formats = {}
191
189
 
@@ -129,18 +129,17 @@ module IOStreams
129
129
  # Perform cleansing on returned Hash keys during the narrowing process.
130
130
  # For example, avoids issues with case etc.
131
131
  def cleanse_hash(hash)
132
- h = {}
133
- hash.each_pair do |key, value|
134
- cleansed_key =
135
- if columns.include?(key)
136
- key
137
- else
138
- key = cleanse_column(key)
139
- key if columns.include?(key)
140
- end
141
- h[cleansed_key] = value if cleansed_key
132
+ unmatched = columns - hash.keys
133
+ unless unmatched.empty?
134
+ hash = hash.dup
135
+ unmatched.each { |name| hash[cleanse_column(name)] = hash.delete(name) }
136
+ end
137
+ # Hash#slice as of Ruby 2.5
138
+ if hash.respond_to?(:slice)
139
+ hash.slice(*columns)
140
+ else
141
+ columns.each_with_object({}) { |column, new_hash| new_hash[column] = hash[column] }
142
142
  end
143
- h
144
143
  end
145
144
 
146
145
  def cleanse_column(name)
@@ -37,6 +37,22 @@ module IOStreams
37
37
  # The :size is the total size of this field including the `.` and the decimals.
38
38
  # Number of :decimals
39
39
  # Raises Errors::ValueTooLong when the supplied value cannot be rendered in `size` characters.
40
+ #
41
+ # In some circumstances the length of the last column is variable.
42
+ # layout: [Array<Hash>]
43
+ # [
44
+ # {size: 23, key: "name"},
45
+ # {size: :remainder, key: "rest"}
46
+ # ]
47
+ # By setting a size of `:remainder` it will take the rest of the line as the value for that column.
48
+ #
49
+ # A size of `:remainder` and no `:key` will discard the remainder of the line without validating the length.
50
+ # layout: [Array<Hash>]
51
+ # [
52
+ # {size: 23, key: "name"},
53
+ # {size: :remainder}
54
+ # ]
55
+ #
40
56
  def initialize(layout:, truncate: true)
41
57
  @layout = Layout.new(layout)
42
58
  @truncate = truncate
@@ -57,12 +73,7 @@ module IOStreams
57
73
 
58
74
  result = ""
59
75
  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)
76
+ result << column.render(hash[column.key], truncate)
66
77
  end
67
78
  result
68
79
  end
@@ -74,13 +85,18 @@ module IOStreams
74
85
  raise(Errors::TypeMismatch, "Line must be a String when format is :fixed. Actual: #{line.class.name}")
75
86
  end
76
87
 
77
- if line.length != layout.length
88
+ if layout.length.positive? && (line.length != layout.length)
78
89
  raise(Errors::InvalidLineLength, "Expected line length: #{layout.length}, actual line length: #{line.length}")
79
90
  end
80
91
 
81
92
  hash = {}
82
93
  index = 0
83
94
  layout.columns.each do |column|
95
+ if column.size == -1
96
+ hash[column.key] = column.parse(line[index..-1]) if column.key
97
+ break
98
+ end
99
+
84
100
  # Ignore "columns" that have no keys. E.g. Fillers
85
101
  hash[column.key] = column.parse(line[index, column.size]) if column.key
86
102
  index += column.size
@@ -93,8 +109,6 @@ module IOStreams
93
109
  false
94
110
  end
95
111
 
96
- private
97
-
98
112
  class Layout
99
113
  attr_reader :columns, :length
100
114
 
@@ -113,7 +127,15 @@ module IOStreams
113
127
  raise(Errors::InvalidLayout, "Missing required :size in: #{hash.inspect}") unless hash.key?(:size)
114
128
 
115
129
  column = Column.new(**hash)
116
- @length += column.size
130
+ if column.size == -1
131
+ if @length == -1
132
+ raise(Errors::InvalidLayout, "Only the last :size can be '-1' or :remainder in: #{hash.inspect}")
133
+ end
134
+
135
+ @length = -1
136
+ else
137
+ @length += column.size
138
+ end
117
139
  column
118
140
  end
119
141
  end
@@ -126,11 +148,13 @@ module IOStreams
126
148
 
127
149
  def initialize(key: nil, size:, type: :string, decimals: 2)
128
150
  @key = key
129
- @size = size.to_i
151
+ @size = size == :remainder ? -1 : size.to_i
130
152
  @type = type.to_sym
131
153
  @decimals = decimals
132
154
 
133
- raise(Errors::InvalidLayout, "Size #{size.inspect} must be positive") unless @size.positive?
155
+ unless @size.positive? || (@size == -1)
156
+ raise(Errors::InvalidLayout, "Size #{size.inspect} must be positive or :remainder")
157
+ end
134
158
  raise(Errors::InvalidLayout, "Unknown type: #{type.inspect}") unless TYPES.include?(type)
135
159
  end
136
160
 
@@ -151,27 +175,33 @@ module IOStreams
151
175
  end
152
176
  end
153
177
 
154
- def render(value)
155
- case type
156
- when :string
157
- format("%-#{size}.#{size}s", value.to_s)
158
- when :integer
159
- formatted = format("%0#{size}d", value.to_i)
160
- if formatted.length > size
161
- raise(Errors::ValueTooLong, "Value: #{value} is too large to fit into column:#{key} of size:#{size}")
178
+ def render(value, truncate)
179
+ formatted =
180
+ case type
181
+ when :string
182
+ value = value.to_s
183
+ return value if size == -1
184
+
185
+ format(truncate ? "%-#{size}.#{size}s" : "%-#{size}s", value)
186
+ when :integer
187
+ return value.to_i.to_s if size == -1
188
+
189
+ truncate = false
190
+ format("%0#{size}d", value.to_i)
191
+ when :float
192
+ return value.to_f.to_s if size == -1
193
+
194
+ truncate = false
195
+ format("%0#{size}.#{decimals}f", value.to_f)
196
+ else
197
+ raise(Errors::InvalidLayout, "Unsupported type: #{type.inspect}")
162
198
  end
163
199
 
164
- formatted
165
- when :float
166
- formatted = format("%0#{size}.#{decimals}f", value.to_f)
167
- if formatted.length > size
168
- raise(Errors::ValueTooLong, "Value: #{value} is too large to fit into column:#{key} of size:#{size}")
169
- end
170
-
171
- formatted
172
- else
173
- raise(Errors::InvalidLayout, "Unsupported type: #{type.inspect}")
200
+ if !truncate && formatted.length > size
201
+ raise(Errors::ValueTooLong, "Value: #{value} is too large to fit into column:#{key} of size:#{size}")
174
202
  end
203
+
204
+ formatted
175
205
  end
176
206
  end
177
207
  end
@@ -1,3 +1,3 @@
1
1
  module IOStreams
2
- VERSION = "1.3.1".freeze
2
+ VERSION = "1.5.1".freeze
3
3
  end
@@ -22,13 +22,14 @@ class Bzip2WriterTest < Minitest::Test
22
22
  it "file" do
23
23
  IOStreams::Bzip2::Writer.file(file_name) do |io|
24
24
  io.write(decompressed)
25
+ io.write(decompressed)
25
26
  end
26
27
 
27
28
  File.open(file_name, "rb") do |file|
28
- io = RBzip2.default_adapter::Decompressor.new(file)
29
+ io = ::Bzip2::FFI::Reader.new(file)
29
30
  result = io.read
30
31
  temp_file.delete
31
- assert_equal decompressed, result
32
+ assert_equal decompressed + decompressed, result
32
33
  end
33
34
  end
34
35
 
@@ -36,12 +37,13 @@ class Bzip2WriterTest < Minitest::Test
36
37
  io_string = StringIO.new("".b)
37
38
  IOStreams::Bzip2::Writer.stream(io_string) do |io|
38
39
  io.write(decompressed)
40
+ io.write(decompressed)
39
41
  end
40
42
 
41
43
  io = StringIO.new(io_string.string)
42
- rbzip2 = RBzip2.default_adapter::Decompressor.new(io)
44
+ rbzip2 = ::Bzip2::FFI::Reader.new(io)
43
45
  data = rbzip2.read
44
- assert_equal decompressed, data
46
+ assert_equal decompressed + decompressed, data
45
47
  end
46
48
  end
47
49
  end
@@ -138,7 +138,7 @@ module Paths
138
138
 
139
139
  it "returns all the children under a sub-dir" do
140
140
  write_raw_data
141
- expected = abd_file_names.collect { |file_name| each_root.join(file_name) }
141
+ expected = %w[abd/test1.txt abd/test5.file].collect { |file_name| each_root.join(file_name) }
142
142
  assert_equal expected.collect(&:to_s).sort, each_root.children("abd/*").collect(&:to_s).sort
143
143
  end
144
144
 
@@ -10,6 +10,36 @@ class TabularTest < Minitest::Test
10
10
  IOStreams::Tabular.new(columns: %w[first_field second third], format: format)
11
11
  end
12
12
 
13
+ let :fixed do
14
+ layout = [
15
+ {size: 23, key: :name},
16
+ {size: 40, key: :address},
17
+ {size: 2},
18
+ {size: 5, key: :zip, type: :integer},
19
+ {size: 8, key: :age, type: :integer},
20
+ {size: 10, key: :weight, type: :float, decimals: 2}
21
+ ]
22
+ IOStreams::Tabular.new(format: :fixed, format_options: {layout: layout})
23
+ end
24
+
25
+ let :fixed_with_remainder do
26
+ layout = [
27
+ {size: 23, key: :name},
28
+ {size: 40, key: :address},
29
+ {size: :remainder, key: :remainder}
30
+ ]
31
+ IOStreams::Tabular.new(format: :fixed, format_options: {layout: layout})
32
+ end
33
+
34
+ let :fixed_discard_remainder do
35
+ layout = [
36
+ {size: 23, key: :name},
37
+ {size: 40, key: :address},
38
+ {size: :remainder}
39
+ ]
40
+ IOStreams::Tabular.new(format: :fixed, format_options: {layout: layout})
41
+ end
42
+
13
43
  describe "#parse_header" do
14
44
  it "parses and sets the csv header" do
15
45
  tabular = IOStreams::Tabular.new(format: :csv)
@@ -136,57 +166,55 @@ class TabularTest < Minitest::Test
136
166
  end
137
167
 
138
168
  describe ":fixed format" do
139
- let :tabular do
140
- layout = [
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}
147
- ]
148
- IOStreams::Tabular.new(format: :fixed, format_options: {layout: layout})
149
- end
150
-
151
169
  it "parses to hash" do
152
- assert hash = tabular.record_parse("Jack over there XX34618012345670012345.01")
170
+ assert hash = fixed.record_parse("Jack over there XX34618012345670012345.01")
153
171
  assert_equal({name: "Jack", address: "over there", zip: 34_618, age: 1_234_567, weight: 12_345.01}, hash)
154
172
  end
155
173
 
156
174
  it "parses short string" do
157
175
  assert_raises IOStreams::Errors::InvalidLineLength do
158
- tabular.record_parse("Jack over th")
176
+ fixed.record_parse("Jack over th")
159
177
  end
160
178
  end
161
179
 
162
180
  it "parses longer string" do
163
181
  assert_raises IOStreams::Errors::InvalidLineLength do
164
- tabular.record_parse("Jack over there XX34618012345670012345.01............")
182
+ fixed.record_parse("Jack over there XX34618012345670012345.01............")
165
183
  end
166
184
  end
167
185
 
168
186
  it "parses zero values" do
169
- assert hash = tabular.record_parse(" 00000000000000000000000")
187
+ assert hash = fixed.record_parse(" 00000000000000000000000")
170
188
  assert_equal({name: "", address: "", zip: 0, age: 0, weight: 0.0}, hash)
171
189
  end
172
190
 
173
191
  it "parses empty values" do
174
- assert hash = tabular.record_parse(" XX ")
192
+ assert hash = fixed.record_parse(" XX ")
175
193
  assert_equal({name: "", address: "", zip: nil, age: nil, weight: nil}, hash)
176
194
  end
177
195
 
178
196
  it "parses blank strings" do
179
- skip "TODO: Part of tabular refactor to get this working"
180
- assert hash = tabular.record_parse(" ")
197
+ skip "TODO: Part of fixed refactor to get this working"
198
+ assert hash = fixed.record_parse(" ")
181
199
  assert_equal({name: "", address: "", zip: nil, age: nil, weight: nil}, hash)
182
200
  end
183
201
 
184
202
  it "parses nil data as nil" do
185
- refute tabular.record_parse(nil)
203
+ refute fixed.record_parse(nil)
186
204
  end
187
205
 
188
206
  it "parses empty string as nil" do
189
- refute tabular.record_parse("")
207
+ refute fixed.record_parse("")
208
+ end
209
+
210
+ it "parses remainder" do
211
+ hash = fixed_with_remainder.record_parse("Jack over there XX34618012345670012345.01............")
212
+ assert_equal({name: "Jack", address: "over there", remainder: "XX34618012345670012345.01............"}, hash)
213
+ end
214
+
215
+ it "discards remainder" do
216
+ hash = fixed_discard_remainder.record_parse("Jack over there XX34618012345670012345.01............")
217
+ assert_equal({name: "Jack", address: "over there"}, hash)
190
218
  end
191
219
  end
192
220
 
@@ -236,52 +264,55 @@ class TabularTest < Minitest::Test
236
264
  end
237
265
 
238
266
  describe ":fixed format" do
239
- let :tabular do
240
- layout = [
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}
247
- ]
248
- IOStreams::Tabular.new(format: :fixed, format_options: {layout: layout})
249
- end
250
-
251
267
  it "renders fixed data" do
252
- assert string = tabular.render(name: "Jack", address: "over there", zip: 34_618, weight: 123_456.789123, age: 21)
268
+ assert string = fixed.render(name: "Jack", address: "over there", zip: 34_618, weight: 123_456.789123, age: 21)
253
269
  assert_equal "Jack over there 34618000000210123456.79", string
254
270
  end
255
271
 
256
272
  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)
273
+ assert string = fixed.render(name: "Jack ran up the beanstalk and when jack reached the top it was truncated", address: "over there", zip: 34_618)
258
274
  assert_equal "Jack ran up the beanstaover there 34618000000000000000.00", string
259
275
  end
260
276
 
261
277
  it "when integer is too large" do
262
278
  assert_raises IOStreams::Errors::ValueTooLong do
263
- tabular.render(zip: 3_461_832_653_653_265)
279
+ fixed.render(zip: 3_461_832_653_653_265)
264
280
  end
265
281
  end
266
282
 
267
283
  it "when float is too large" do
268
284
  assert_raises IOStreams::Errors::ValueTooLong do
269
- tabular.render(weight: 3_461_832_653_653_265.234)
285
+ fixed.render(weight: 3_461_832_653_653_265.234)
270
286
  end
271
287
  end
272
288
 
273
289
  it "renders nil as empty string" do
274
- assert string = tabular.render(zip: 34_618)
290
+ assert string = fixed.render(zip: 34_618)
275
291
  assert_equal " 34618000000000000000.00", string
276
292
  end
277
293
 
278
294
  it "renders boolean" do
279
- assert string = tabular.render(name: true, address: false)
295
+ assert string = fixed.render(name: true, address: false)
280
296
  assert_equal "true false 00000000000000000000.00", string
281
297
  end
282
298
 
283
299
  it "renders no data as nil" do
284
- refute tabular.render({})
300
+ refute fixed.render({})
301
+ end
302
+
303
+ it "any size last string" do
304
+ assert string = fixed_with_remainder.render(name: "Jack", address: "over there", remainder: "XX34618012345670012345.01............")
305
+ assert_equal "Jack over there XX34618012345670012345.01............", string
306
+ end
307
+
308
+ it "nil last string" do
309
+ assert string = fixed_with_remainder.render(name: "Jack", address: "over there", remainder: nil)
310
+ assert_equal "Jack over there ", string
311
+ end
312
+
313
+ it "skips last filler" do
314
+ assert string = fixed_discard_remainder.render(name: "Jack", address: "over there")
315
+ assert_equal "Jack over there ", string
285
316
  end
286
317
  end
287
318
  end
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.3.1
4
+ version: 1.5.1
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-07-16 00:00:00.000000000 Z
11
+ date: 2020-09-29 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.8
133
+ rubygems_version: 3.1.2
134
134
  signing_key:
135
135
  specification_version: 4
136
136
  summary: Input and Output streaming for Ruby.