iostreams 1.2.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -4
  3. data/lib/io_streams/builder.rb +27 -10
  4. data/lib/io_streams/bzip2/reader.rb +3 -3
  5. data/lib/io_streams/bzip2/writer.rb +3 -3
  6. data/lib/io_streams/deprecated.rb +1 -1
  7. data/lib/io_streams/encode/reader.rb +1 -3
  8. data/lib/io_streams/encode/writer.rb +1 -1
  9. data/lib/io_streams/errors.rb +22 -0
  10. data/lib/io_streams/io_streams.rb +1 -5
  11. data/lib/io_streams/line/reader.rb +28 -16
  12. data/lib/io_streams/path.rb +3 -1
  13. data/lib/io_streams/paths/file.rb +4 -4
  14. data/lib/io_streams/paths/http.rb +6 -3
  15. data/lib/io_streams/paths/s3.rb +30 -8
  16. data/lib/io_streams/paths/sftp.rb +34 -13
  17. data/lib/io_streams/pgp.rb +84 -71
  18. data/lib/io_streams/stream.rb +78 -12
  19. data/lib/io_streams/tabular.rb +28 -27
  20. data/lib/io_streams/tabular/header.rb +14 -12
  21. data/lib/io_streams/tabular/parser/csv.rb +4 -2
  22. data/lib/io_streams/tabular/parser/fixed.rb +166 -26
  23. data/lib/io_streams/tabular/utility/csv_row.rb +1 -4
  24. data/lib/io_streams/utils.rb +4 -4
  25. data/lib/io_streams/version.rb +1 -1
  26. data/lib/io_streams/zip/reader.rb +1 -1
  27. data/test/builder_test.rb +29 -0
  28. data/test/bzip2_writer_test.rb +6 -4
  29. data/test/deprecated_test.rb +2 -0
  30. data/test/files/test.psv +4 -0
  31. data/test/files/unclosed_quote_large_test.csv +1658 -0
  32. data/test/files/unclosed_quote_test2.csv +3 -0
  33. data/test/io_streams_test.rb +2 -2
  34. data/test/line_reader_test.rb +30 -4
  35. data/test/paths/file_test.rb +1 -1
  36. data/test/paths/s3_test.rb +3 -3
  37. data/test/paths/sftp_test.rb +4 -4
  38. data/test/pgp_test.rb +54 -4
  39. data/test/pgp_writer_test.rb +3 -3
  40. data/test/stream_test.rb +174 -8
  41. data/test/tabular_test.rb +100 -40
  42. data/test/test_helper.rb +1 -1
  43. metadata +47 -42
@@ -0,0 +1,3 @@
1
+ name, description, zip
2
+ Jack,Firstn"ame is Jack,234567
3
+ John,Firstname is John,234568
@@ -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
@@ -14,6 +14,14 @@ class LineReaderTest < Minitest::Test
14
14
  File.join(File.dirname(__FILE__), "files", "unclosed_quote_test.csv")
15
15
  end
16
16
 
17
+ let :unclosed_quote_file2 do
18
+ File.join(File.dirname(__FILE__), "files", "unclosed_quote_test2.csv")
19
+ end
20
+
21
+ let :unclosed_quote_large_file do
22
+ File.join(File.dirname(__FILE__), "files", "unclosed_quote_large_test.csv")
23
+ end
24
+
17
25
  let :data do
18
26
  data = []
19
27
  File.open(file_name, "rt") do |file|
@@ -51,13 +59,31 @@ class LineReaderTest < Minitest::Test
51
59
  assert_equal 4, lines.count
52
60
  end
53
61
 
54
- it "raises error for unclosed quote" do
55
- assert_raises(RuntimeError) do
62
+ it "raises error for unbalanced quotes" do
63
+ exc = assert_raises(IOStreams::Errors::MalformedDataError) do
56
64
  IOStreams::Line::Reader.file(unclosed_quote_file, embedded_within: '"') do |io|
57
- io.each do |line|
58
- end
65
+ io.each { |line| }
66
+ end
67
+ end
68
+ assert_includes exc.message, "Unbalanced delimited field, delimiter:"
69
+ end
70
+
71
+ it "raises error for unclosed quote" do
72
+ exc = assert_raises(IOStreams::Errors::MalformedDataError) do
73
+ IOStreams::Line::Reader.file(unclosed_quote_file2, embedded_within: '"') do |io|
74
+ io.each { |line| }
75
+ end
76
+ end
77
+ assert_includes exc.message, "Unbalanced delimited field, delimiter:"
78
+ end
79
+
80
+ it "raises error for unclosed quote before eof" do
81
+ exc = assert_raises(IOStreams::Errors::MalformedDataError) do
82
+ IOStreams::Line::Reader.file(unclosed_quote_large_file, embedded_within: '"', buffer_size: 20) do |io|
83
+ io.each { |line| }
59
84
  end
60
85
  end
86
+ assert_includes exc.message, "Unbalanced delimited field, delimiter:"
61
87
  end
62
88
  end
63
89
  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
@@ -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
 
@@ -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
data/test/pgp_test.rb CHANGED
@@ -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
data/test/stream_test.rb CHANGED
@@ -45,9 +45,9 @@ class StreamTest < Minitest::Test
45
45
  it "reads a zip file" do
46
46
  File.open(multiple_zip_file_name, "rb") do |io|
47
47
  result = IOStreams::Stream.new(io).
48
- file_name(multiple_zip_file_name).
49
- option(:zip, entry_file_name: "test.json").
50
- read
48
+ file_name(multiple_zip_file_name).
49
+ option(:zip, entry_file_name: "test.json").
50
+ read
51
51
  assert_equal contents_test_json, result
52
52
  end
53
53
  end
@@ -55,8 +55,8 @@ class StreamTest < Minitest::Test
55
55
  it "reads a zip file from within a gz file" do
56
56
  File.open(zip_gz_file_name, "rb") do |io|
57
57
  result = IOStreams::Stream.new(io).
58
- file_name(zip_gz_file_name).
59
- read
58
+ file_name(zip_gz_file_name).
59
+ read
60
60
  assert_equal contents_test_txt, result
61
61
  end
62
62
  end
@@ -71,7 +71,7 @@ class StreamTest < Minitest::Test
71
71
  describe ".record_reader" do
72
72
  end
73
73
 
74
- describe ".each_line" do
74
+ describe "#each(:line)" do
75
75
  it "returns a line at a time" do
76
76
  lines = []
77
77
  stream.stream(:none)
@@ -91,10 +91,114 @@ class StreamTest < Minitest::Test
91
91
  end
92
92
  end
93
93
 
94
- describe ".each row" do
94
+ describe "#each(:array)" do
95
+ describe "csv" do
96
+ let :source_file_name do
97
+ File.join(__dir__, "files", "test.csv")
98
+ end
99
+
100
+ let :expected_rows do
101
+ rows = []
102
+ CSV.open(source_file_name).each { |row| rows << row }
103
+ rows
104
+ end
105
+
106
+ it "detects format from file_name" do
107
+ output = []
108
+ stream.file_name = source_file_name
109
+ stream.each(:array) { |record| output << record }
110
+ assert_equal expected_rows, output
111
+ end
112
+
113
+ it "honors format" do
114
+ output = []
115
+ stream.file_name = "blah"
116
+ stream.format = :csv
117
+ stream.each(:array) { |record| output << record }
118
+ assert_equal expected_rows, output
119
+ end
120
+ end
121
+
122
+ describe "psv" do
123
+ let :source_file_name do
124
+ File.join(__dir__, "files", "test.psv")
125
+ end
126
+
127
+ let :expected_rows do
128
+ File.readlines(source_file_name).collect { |line| line.chomp.split("|") }
129
+ end
130
+
131
+ it "detects format from file_name" do
132
+ output = []
133
+ stream.file_name = source_file_name
134
+ stream.each(:array) { |record| output << record }
135
+ assert_equal expected_rows, output
136
+ end
137
+
138
+ it "honors format" do
139
+ output = []
140
+ stream.file_name = "blah"
141
+ stream.format = :psv
142
+ stream.each(:array) { |record| output << record }
143
+ assert_equal expected_rows, output
144
+ end
145
+ end
146
+
147
+ describe "json" do
148
+ let :source_file_name do
149
+ File.join(__dir__, "files", "test.json")
150
+ end
151
+
152
+ let :expected_rows do
153
+ hash_rows = File.readlines(source_file_name).collect { |line| JSON.load(line) }
154
+ rows = []
155
+ rows << hash_rows.first.keys
156
+ hash_rows.each { |hash| rows << hash.values }
157
+ rows
158
+ end
159
+
160
+ it "detects format from file_name" do
161
+ skip "TODO: Support reading json files as arrays"
162
+ output = []
163
+ stream.file_name = source_file_name
164
+ stream.each(:array) { |record| output << record }
165
+ assert_equal expected_rows, output
166
+ end
167
+
168
+ it "honors format" do
169
+ skip "TODO: Support reading json files as arrays"
170
+ output = []
171
+ stream.file_name = "blah"
172
+ stream.format = :json
173
+ stream.each(:array) { |record| output << record }
174
+ assert_equal expected_rows, output
175
+ end
176
+ end
95
177
  end
96
178
 
97
- describe ".each record" do
179
+ describe ".each hash" do
180
+ let :source_file_name do
181
+ File.join(__dir__, "files", "test.json")
182
+ end
183
+
184
+ let :expected_json do
185
+ File.readlines(source_file_name).collect { |line| JSON.load(line) }
186
+ end
187
+
188
+ it "detects format from file_name" do
189
+ output = []
190
+ stream.file_name = source_file_name
191
+ stream.each(:hash) { |record| output << record }
192
+ assert_equal expected_json, output
193
+ end
194
+
195
+ it "honors format" do
196
+ output = []
197
+ stream.file_name = "blah"
198
+ stream.format = :json
199
+ stream.each(:hash) { |record| output << record }
200
+ assert_equal expected_json, output
201
+ end
98
202
  end
99
203
 
100
204
  describe "#writer" do
@@ -359,6 +463,24 @@ class StreamTest < Minitest::Test
359
463
  end
360
464
  assert_equal "\nHe\n\nl\n\nlo \nWorld\n\n", io.string, io.string.inspect
361
465
  end
466
+
467
+ it "honors format" do
468
+ io = StringIO.new
469
+ IOStreams::Stream.new(io).format(:psv).writer(:array) do |stream|
470
+ stream << %w[first_name last_name]
471
+ stream << %w[Jack Johnson]
472
+ end
473
+ assert_equal "first_name|last_name\nJack|Johnson\n", io.string, io.string.inspect
474
+ end
475
+
476
+ it "auto detects format" do
477
+ io = StringIO.new
478
+ IOStreams::Stream.new(io).file_name("abc.psv").writer(:array) do |stream|
479
+ stream << %w[first_name last_name]
480
+ stream << %w[Jack Johnson]
481
+ end
482
+ assert_equal "first_name|last_name\nJack|Johnson\n", io.string, io.string.inspect
483
+ end
362
484
  end
363
485
  end
364
486
 
@@ -402,6 +524,50 @@ class StreamTest < Minitest::Test
402
524
  end
403
525
  assert_equal "first_name,last_name\nJack,Johnson\n\n{:first_name=>\"Able\", :last_name=>\"Smith\"}\n\n", io.string, io.string.inspect
404
526
  end
527
+
528
+ it "honors format" do
529
+ io = StringIO.new
530
+ IOStreams::Stream.new(io).format(:json).writer(:hash) do |stream|
531
+ stream << {first_name: "Jack", last_name: "Johnson"}
532
+ end
533
+ assert_equal "{\"first_name\":\"Jack\",\"last_name\":\"Johnson\"}\n", io.string, io.string.inspect
534
+ end
535
+
536
+ it "auto detects format" do
537
+ io = StringIO.new
538
+ IOStreams::Stream.new(io).file_name("abc.json").writer(:hash) do |stream|
539
+ stream << {first_name: "Jack", last_name: "Johnson"}
540
+ end
541
+ assert_equal "{\"first_name\":\"Jack\",\"last_name\":\"Johnson\"}\n", io.string, io.string.inspect
542
+ end
543
+ end
544
+ end
545
+
546
+ describe "#format" do
547
+ it "detects the format from the file name" do
548
+ stream.file_name = "abc.json"
549
+ assert_equal :json, stream.format
550
+ end
551
+
552
+ it "is nil if the file name has no meaningful format" do
553
+ assert_nil stream.format
554
+ end
555
+
556
+ it "returns set format with no file_name" do
557
+ stream.format = :csv
558
+ assert_equal :csv, stream.format
559
+ end
560
+
561
+ it "returns set format with file_name" do
562
+ stream.file_name = "abc.json"
563
+ stream.format = :csv
564
+ assert_equal :csv, stream.format
565
+ end
566
+
567
+ it "validates bad format" do
568
+ assert_raises ArgumentError do
569
+ stream.format = :blah
570
+ end
405
571
  end
406
572
  end
407
573
  end