csvops 0.1.0.alpha → 0.2.0.alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +49 -10
- data/docs/release-v0.2.0-alpha.md +80 -0
- data/lib/csvtool/application/use_cases/run_extraction.rb +17 -17
- data/lib/csvtool/application/use_cases/run_row_extraction.rb +111 -0
- data/lib/csvtool/cli.rb +6 -2
- data/lib/csvtool/domain/{extraction_session → column_session}/column_selection.rb +1 -1
- data/lib/csvtool/domain/{extraction_session/extraction_session.rb → column_session/column_session.rb} +2 -2
- data/lib/csvtool/domain/{extraction_session → column_session}/csv_source.rb +1 -1
- data/lib/csvtool/domain/{extraction_session → column_session}/extraction_options.rb +1 -1
- data/lib/csvtool/domain/{extraction_session → column_session}/extraction_value.rb +1 -1
- data/lib/csvtool/domain/{extraction_session → column_session}/output_destination.rb +1 -1
- data/lib/csvtool/domain/{extraction_session → column_session}/preview.rb +1 -1
- data/lib/csvtool/domain/{extraction_session → column_session}/separator.rb +1 -1
- data/lib/csvtool/domain/row_session/row_output_destination.rb +31 -0
- data/lib/csvtool/domain/row_session/row_range.rb +39 -0
- data/lib/csvtool/domain/row_session/row_session.rb +25 -0
- data/lib/csvtool/domain/row_session/row_source.rb +16 -0
- data/lib/csvtool/infrastructure/csv/row_streamer.rb +27 -0
- data/lib/csvtool/infrastructure/output/csv_row_console_writer.rb +34 -0
- data/lib/csvtool/infrastructure/output/csv_row_file_writer.rb +45 -0
- data/lib/csvtool/interface/cli/errors/presenter.rb +16 -0
- data/lib/csvtool/interface/cli/menu_loop.rb +10 -5
- data/lib/csvtool/version.rb +1 -1
- data/test/csvtool/application/use_cases/run_row_extraction_test.rb +140 -0
- data/test/csvtool/cli_test.rb +132 -6
- data/test/csvtool/cli_unit_test.rb +12 -1
- data/test/csvtool/domain/{extraction_session → column_session}/column_selection_test.rb +2 -2
- data/test/csvtool/domain/column_session/column_session_test.rb +35 -0
- data/test/csvtool/domain/column_session/csv_source_test.rb +14 -0
- data/test/csvtool/domain/{extraction_session → column_session}/extraction_options_test.rb +3 -3
- data/test/csvtool/domain/{extraction_session → column_session}/extraction_value_test.rb +2 -2
- data/test/csvtool/domain/{extraction_session → column_session}/output_destination_test.rb +3 -3
- data/test/csvtool/domain/column_session/preview_test.rb +18 -0
- data/test/csvtool/domain/{extraction_session → column_session}/separator_test.rb +3 -3
- data/test/csvtool/domain/row_session/row_output_destination_test.rb +23 -0
- data/test/csvtool/domain/row_session/row_range_test.rb +30 -0
- data/test/csvtool/domain/row_session/row_session_test.rb +22 -0
- data/test/csvtool/domain/row_session/row_source_test.rb +12 -0
- data/test/csvtool/infrastructure/csv/row_streamer_test.rb +41 -0
- data/test/csvtool/infrastructure/output/csv_row_console_writer_test.rb +24 -0
- data/test/csvtool/infrastructure/output/csv_row_file_writer_test.rb +40 -0
- data/test/csvtool/interface/cli/errors/presenter_test.rb +8 -0
- data/test/csvtool/interface/cli/menu_loop_test.rb +37 -12
- data/test/fixtures/sample_people_bad_tail.csv +5 -0
- metadata +35 -17
- data/test/csvtool/domain/extraction_session/csv_source_test.rb +0 -14
- data/test/csvtool/domain/extraction_session/extraction_session_test.rb +0 -35
- data/test/csvtool/domain/extraction_session/preview_test.rb +0 -18
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
5
|
+
module Csvtool
|
|
6
|
+
module Infrastructure
|
|
7
|
+
module Output
|
|
8
|
+
class CsvRowConsoleWriter
|
|
9
|
+
def initialize(stdout:, row_streamer:)
|
|
10
|
+
@stdout = stdout
|
|
11
|
+
@row_streamer = row_streamer
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(file_path:, col_sep:, headers:, start_row:, end_row:)
|
|
15
|
+
wrote_header = false
|
|
16
|
+
stats = @row_streamer.each_in_range(
|
|
17
|
+
file_path: file_path,
|
|
18
|
+
col_sep: col_sep,
|
|
19
|
+
start_row: start_row,
|
|
20
|
+
end_row: end_row
|
|
21
|
+
) do |fields|
|
|
22
|
+
unless wrote_header
|
|
23
|
+
@stdout.puts ::CSV.generate_line(headers, row_sep: "").chomp
|
|
24
|
+
wrote_header = true
|
|
25
|
+
end
|
|
26
|
+
@stdout.puts ::CSV.generate_line(fields, row_sep: "").chomp
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
stats
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
5
|
+
module Csvtool
|
|
6
|
+
module Infrastructure
|
|
7
|
+
module Output
|
|
8
|
+
class CsvRowFileWriter
|
|
9
|
+
def initialize(stdout:, errors:, row_streamer:)
|
|
10
|
+
@stdout = stdout
|
|
11
|
+
@errors = errors
|
|
12
|
+
@row_streamer = row_streamer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(file_path:, col_sep:, headers:, start_row:, end_row:, output_path:)
|
|
16
|
+
csv = nil
|
|
17
|
+
wrote_rows = false
|
|
18
|
+
|
|
19
|
+
stats = @row_streamer.each_in_range(
|
|
20
|
+
file_path: file_path,
|
|
21
|
+
col_sep: col_sep,
|
|
22
|
+
start_row: start_row,
|
|
23
|
+
end_row: end_row
|
|
24
|
+
) do |fields|
|
|
25
|
+
unless wrote_rows
|
|
26
|
+
csv = ::CSV.open(output_path, "w")
|
|
27
|
+
csv << headers
|
|
28
|
+
wrote_rows = true
|
|
29
|
+
end
|
|
30
|
+
csv << fields
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
csv&.close
|
|
34
|
+
@stdout.puts "Wrote output to #{output_path}" if wrote_rows
|
|
35
|
+
stats
|
|
36
|
+
rescue Errno::EACCES, Errno::ENOENT => e
|
|
37
|
+
@errors.cannot_write_output_file(output_path, e.class)
|
|
38
|
+
nil
|
|
39
|
+
ensure
|
|
40
|
+
csv&.close unless csv&.closed?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -52,6 +52,22 @@ module Csvtool
|
|
|
52
52
|
def canceled
|
|
53
53
|
@stdout.puts "Canceled."
|
|
54
54
|
end
|
|
55
|
+
|
|
56
|
+
def invalid_start_row
|
|
57
|
+
@stdout.puts "Start row must be a positive integer."
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def invalid_end_row
|
|
61
|
+
@stdout.puts "End row must be a positive integer."
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def invalid_row_range_order
|
|
65
|
+
@stdout.puts "End row must be greater than or equal to start row."
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def row_range_out_of_bounds(total_rows)
|
|
69
|
+
@stdout.puts "Row range is out of bounds. File has #{total_rows} data rows."
|
|
70
|
+
end
|
|
55
71
|
end
|
|
56
72
|
end
|
|
57
73
|
end
|
|
@@ -4,25 +4,30 @@ module Csvtool
|
|
|
4
4
|
module Interface
|
|
5
5
|
module CLI
|
|
6
6
|
class MenuLoop
|
|
7
|
-
def initialize(stdin:, stdout:, menu_options:,
|
|
7
|
+
def initialize(stdin:, stdout:, menu_options:, extract_column_action:, extract_rows_action:)
|
|
8
8
|
@stdin = stdin
|
|
9
9
|
@stdout = stdout
|
|
10
10
|
@menu_options = menu_options
|
|
11
|
-
@
|
|
11
|
+
@extract_column_action = extract_column_action
|
|
12
|
+
@extract_rows_action = extract_rows_action
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def run
|
|
15
16
|
loop do
|
|
16
17
|
print_menu
|
|
17
18
|
@stdout.print "> "
|
|
19
|
+
choice = @stdin.gets
|
|
20
|
+
return 0 if choice.nil?
|
|
18
21
|
|
|
19
|
-
case
|
|
22
|
+
case choice.strip
|
|
20
23
|
when "1"
|
|
21
|
-
@
|
|
24
|
+
@extract_column_action.call
|
|
22
25
|
when "2"
|
|
26
|
+
@extract_rows_action.call
|
|
27
|
+
when "3"
|
|
23
28
|
return 0
|
|
24
29
|
else
|
|
25
|
-
@stdout.puts "Please choose 1 or
|
|
30
|
+
@stdout.puts "Please choose 1, 2, or 3."
|
|
26
31
|
end
|
|
27
32
|
end
|
|
28
33
|
end
|
data/lib/csvtool/version.rb
CHANGED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/application/use_cases/run_row_extraction"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
class RunRowExtractionTest < Minitest::Test
|
|
8
|
+
def test_use_case_prints_selected_row_range_with_header
|
|
9
|
+
out = StringIO.new
|
|
10
|
+
fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
|
|
11
|
+
input = [fixture, "", "2", "3", ""].join("\n") + "\n"
|
|
12
|
+
|
|
13
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
14
|
+
use_case.call
|
|
15
|
+
|
|
16
|
+
assert_includes out.string, "name,city"
|
|
17
|
+
assert_includes out.string, "Bob,Paris"
|
|
18
|
+
assert_includes out.string, "Cara,Berlin"
|
|
19
|
+
refute_includes out.string, "Alice,London"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_rejects_non_numeric_start_row
|
|
23
|
+
out = StringIO.new
|
|
24
|
+
fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
|
|
25
|
+
input = [fixture, "", "abc", "3", ""].join("\n") + "\n"
|
|
26
|
+
|
|
27
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
28
|
+
use_case.call
|
|
29
|
+
|
|
30
|
+
assert_includes out.string, "Start row must be a positive integer."
|
|
31
|
+
refute_includes out.string, "name,city"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_rejects_non_numeric_end_row
|
|
35
|
+
out = StringIO.new
|
|
36
|
+
fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
|
|
37
|
+
input = [fixture, "", "1", "xyz", ""].join("\n") + "\n"
|
|
38
|
+
|
|
39
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
40
|
+
use_case.call
|
|
41
|
+
|
|
42
|
+
assert_includes out.string, "End row must be a positive integer."
|
|
43
|
+
refute_includes out.string, "name,city"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_rejects_end_before_start
|
|
47
|
+
out = StringIO.new
|
|
48
|
+
fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
|
|
49
|
+
input = [fixture, "", "3", "2", ""].join("\n") + "\n"
|
|
50
|
+
|
|
51
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
52
|
+
use_case.call
|
|
53
|
+
|
|
54
|
+
assert_includes out.string, "End row must be greater than or equal to start row."
|
|
55
|
+
refute_includes out.string, "name,city"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_handles_out_of_bounds_start_row
|
|
59
|
+
out = StringIO.new
|
|
60
|
+
fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
|
|
61
|
+
input = [fixture, "", "10", "12", ""].join("\n") + "\n"
|
|
62
|
+
|
|
63
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
64
|
+
use_case.call
|
|
65
|
+
|
|
66
|
+
assert_includes out.string, "Row range is out of bounds. File has 3 data rows."
|
|
67
|
+
refute_includes out.string, "name,city"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_use_case_supports_tsv_separator
|
|
71
|
+
out = StringIO.new
|
|
72
|
+
fixture = File.expand_path("../../../fixtures/sample_people.tsv", __dir__)
|
|
73
|
+
input = [fixture, "2", "2", "3", ""].join("\n") + "\n"
|
|
74
|
+
|
|
75
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
76
|
+
use_case.call
|
|
77
|
+
|
|
78
|
+
assert_includes out.string, "name,city"
|
|
79
|
+
assert_includes out.string, "Bob,Paris"
|
|
80
|
+
assert_includes out.string, "Cara,Berlin"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_use_case_supports_custom_separator
|
|
84
|
+
out = StringIO.new
|
|
85
|
+
fixture = File.expand_path("../../../fixtures/sample_people_colon.txt", __dir__)
|
|
86
|
+
input = [fixture, "5", ":", "2", "3", ""].join("\n") + "\n"
|
|
87
|
+
|
|
88
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
89
|
+
use_case.call
|
|
90
|
+
|
|
91
|
+
assert_includes out.string, "name,city"
|
|
92
|
+
assert_includes out.string, "Bob,Paris"
|
|
93
|
+
assert_includes out.string, "Cara,Berlin"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_use_case_can_write_selected_rows_to_file
|
|
97
|
+
out = StringIO.new
|
|
98
|
+
fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
|
|
99
|
+
|
|
100
|
+
Dir.mktmpdir do |dir|
|
|
101
|
+
output_path = File.join(dir, "rows.csv")
|
|
102
|
+
input = [fixture, "", "2", "3", "2", output_path].join("\n") + "\n"
|
|
103
|
+
|
|
104
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
105
|
+
use_case.call
|
|
106
|
+
|
|
107
|
+
assert_equal "name,city\nBob,Paris\nCara,Berlin\n", File.read(output_path)
|
|
108
|
+
assert_includes out.string, "Wrote output to #{output_path}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_stops_parsing_after_end_row_for_console_output
|
|
113
|
+
out = StringIO.new
|
|
114
|
+
fixture = File.expand_path("../../../fixtures/sample_people_bad_tail.csv", __dir__)
|
|
115
|
+
input = [fixture, "", "1", "2", ""].join("\n") + "\n"
|
|
116
|
+
|
|
117
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
118
|
+
use_case.call
|
|
119
|
+
|
|
120
|
+
assert_includes out.string, "name,city"
|
|
121
|
+
assert_includes out.string, "Alice,London"
|
|
122
|
+
assert_includes out.string, "Bob,Paris"
|
|
123
|
+
refute_includes out.string, "Could not parse CSV file."
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_out_of_bounds_file_mode_reports_error
|
|
127
|
+
out = StringIO.new
|
|
128
|
+
fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
|
|
129
|
+
|
|
130
|
+
Dir.mktmpdir do |dir|
|
|
131
|
+
output_path = File.join(dir, "rows.csv")
|
|
132
|
+
input = [fixture, "", "10", "12", "2", output_path].join("\n") + "\n"
|
|
133
|
+
|
|
134
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
135
|
+
use_case.call
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
assert_includes out.string, "Row range is out of bounds. File has 3 data rows."
|
|
139
|
+
end
|
|
140
|
+
end
|
data/test/csvtool/cli_test.rb
CHANGED
|
@@ -11,7 +11,7 @@ class TestCli < Minitest::Test
|
|
|
11
11
|
|
|
12
12
|
def test_menu_can_exit_cleanly
|
|
13
13
|
output = StringIO.new
|
|
14
|
-
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("
|
|
14
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("3\n"), stdout: output, stderr: StringIO.new)
|
|
15
15
|
assert_equal 0, status
|
|
16
16
|
assert_includes output.string, "CSV Tool Menu"
|
|
17
17
|
end
|
|
@@ -26,7 +26,7 @@ class TestCli < Minitest::Test
|
|
|
26
26
|
"",
|
|
27
27
|
"y",
|
|
28
28
|
"",
|
|
29
|
-
"
|
|
29
|
+
"3"
|
|
30
30
|
].join("\n") + "\n"
|
|
31
31
|
|
|
32
32
|
output = StringIO.new
|
|
@@ -49,6 +49,132 @@ class TestCli < Minitest::Test
|
|
|
49
49
|
assert_equal "Alice\nBob\nCara\n", output.string
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def test_row_range_workflow_prints_selected_rows
|
|
53
|
+
output = StringIO.new
|
|
54
|
+
input = [
|
|
55
|
+
"2",
|
|
56
|
+
fixture_path("sample_people.csv"),
|
|
57
|
+
"",
|
|
58
|
+
"2",
|
|
59
|
+
"3",
|
|
60
|
+
"",
|
|
61
|
+
"3"
|
|
62
|
+
].join("\n") + "\n"
|
|
63
|
+
|
|
64
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
65
|
+
|
|
66
|
+
assert_equal 0, status
|
|
67
|
+
assert_includes output.string, "name,city"
|
|
68
|
+
assert_includes output.string, "Bob,Paris"
|
|
69
|
+
assert_includes output.string, "Cara,Berlin"
|
|
70
|
+
refute_includes output.string, "Alice,London"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_row_range_invalid_inputs_return_to_menu
|
|
74
|
+
output = StringIO.new
|
|
75
|
+
input = [
|
|
76
|
+
"2",
|
|
77
|
+
fixture_path("sample_people.csv"),
|
|
78
|
+
"",
|
|
79
|
+
"0",
|
|
80
|
+
"3",
|
|
81
|
+
"",
|
|
82
|
+
"3"
|
|
83
|
+
].join("\n") + "\n"
|
|
84
|
+
|
|
85
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
86
|
+
|
|
87
|
+
assert_equal 0, status
|
|
88
|
+
assert_includes output.string, "Start row must be a positive integer."
|
|
89
|
+
assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_row_range_workflow_supports_tsv_separator
|
|
93
|
+
output = StringIO.new
|
|
94
|
+
input = [
|
|
95
|
+
"2",
|
|
96
|
+
fixture_path("sample_people.tsv"),
|
|
97
|
+
"2",
|
|
98
|
+
"2",
|
|
99
|
+
"3",
|
|
100
|
+
"",
|
|
101
|
+
"3"
|
|
102
|
+
].join("\n") + "\n"
|
|
103
|
+
|
|
104
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
105
|
+
|
|
106
|
+
assert_equal 0, status
|
|
107
|
+
assert_includes output.string, "name,city"
|
|
108
|
+
assert_includes output.string, "Bob,Paris"
|
|
109
|
+
assert_includes output.string, "Cara,Berlin"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_row_range_workflow_supports_custom_separator
|
|
113
|
+
output = StringIO.new
|
|
114
|
+
input = [
|
|
115
|
+
"2",
|
|
116
|
+
fixture_path("sample_people_colon.txt"),
|
|
117
|
+
"5",
|
|
118
|
+
":",
|
|
119
|
+
"2",
|
|
120
|
+
"3",
|
|
121
|
+
"",
|
|
122
|
+
"3"
|
|
123
|
+
].join("\n") + "\n"
|
|
124
|
+
|
|
125
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
126
|
+
|
|
127
|
+
assert_equal 0, status
|
|
128
|
+
assert_includes output.string, "name,city"
|
|
129
|
+
assert_includes output.string, "Bob,Paris"
|
|
130
|
+
assert_includes output.string, "Cara,Berlin"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_row_range_workflow_can_write_selected_rows_to_file
|
|
134
|
+
output = StringIO.new
|
|
135
|
+
output_path = nil
|
|
136
|
+
|
|
137
|
+
Dir.mktmpdir do |dir|
|
|
138
|
+
output_path = File.join(dir, "row_range.csv")
|
|
139
|
+
input = [
|
|
140
|
+
"2",
|
|
141
|
+
fixture_path("sample_people.csv"),
|
|
142
|
+
"",
|
|
143
|
+
"2",
|
|
144
|
+
"3",
|
|
145
|
+
"2",
|
|
146
|
+
output_path,
|
|
147
|
+
"3"
|
|
148
|
+
].join("\n") + "\n"
|
|
149
|
+
|
|
150
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
151
|
+
assert_equal 0, status
|
|
152
|
+
assert_equal "name,city\nBob,Paris\nCara,Berlin\n", File.read(output_path)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
assert_includes output.string, "Wrote output to #{output_path}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def test_row_range_workflow_stops_before_malformed_tail
|
|
159
|
+
output = StringIO.new
|
|
160
|
+
input = [
|
|
161
|
+
"2",
|
|
162
|
+
fixture_path("sample_people_bad_tail.csv"),
|
|
163
|
+
"",
|
|
164
|
+
"1",
|
|
165
|
+
"2",
|
|
166
|
+
"",
|
|
167
|
+
"3"
|
|
168
|
+
].join("\n") + "\n"
|
|
169
|
+
|
|
170
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
171
|
+
|
|
172
|
+
assert_equal 0, status
|
|
173
|
+
assert_includes output.string, "Alice,London"
|
|
174
|
+
assert_includes output.string, "Bob,Paris"
|
|
175
|
+
refute_includes output.string, "Could not parse CSV file."
|
|
176
|
+
end
|
|
177
|
+
|
|
52
178
|
def test_end_to_end_file_output_writes_expected_csv
|
|
53
179
|
output = StringIO.new
|
|
54
180
|
output_path = nil
|
|
@@ -65,7 +191,7 @@ class TestCli < Minitest::Test
|
|
|
65
191
|
"y",
|
|
66
192
|
"2",
|
|
67
193
|
output_path,
|
|
68
|
-
"
|
|
194
|
+
"3"
|
|
69
195
|
].join("\n") + "\n"
|
|
70
196
|
|
|
71
197
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -85,7 +211,7 @@ class TestCli < Minitest::Test
|
|
|
85
211
|
"1",
|
|
86
212
|
"",
|
|
87
213
|
"n",
|
|
88
|
-
"
|
|
214
|
+
"3"
|
|
89
215
|
].join("\n") + "\n"
|
|
90
216
|
|
|
91
217
|
output = StringIO.new
|
|
@@ -100,7 +226,7 @@ class TestCli < Minitest::Test
|
|
|
100
226
|
output = StringIO.new
|
|
101
227
|
status = Csvtool::CLI.start(
|
|
102
228
|
["menu"],
|
|
103
|
-
stdin: StringIO.new("1\n/tmp/does-not-exist.csv\
|
|
229
|
+
stdin: StringIO.new("1\n/tmp/does-not-exist.csv\n3\n"),
|
|
104
230
|
stdout: output,
|
|
105
231
|
stderr: StringIO.new
|
|
106
232
|
)
|
|
@@ -121,7 +247,7 @@ class TestCli < Minitest::Test
|
|
|
121
247
|
"y",
|
|
122
248
|
"2",
|
|
123
249
|
"/tmp/not-a-dir/out.csv",
|
|
124
|
-
"
|
|
250
|
+
"3"
|
|
125
251
|
].join("\n") + "\n"
|
|
126
252
|
|
|
127
253
|
output = StringIO.new
|
|
@@ -16,7 +16,7 @@ class CliUnitTest < Minitest::Test
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def test_menu_command_can_exit_zero
|
|
19
|
-
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("
|
|
19
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("3\n"), stdout: StringIO.new, stderr: StringIO.new)
|
|
20
20
|
assert_equal 0, status
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -24,4 +24,15 @@ class CliUnitTest < Minitest::Test
|
|
|
24
24
|
status = Csvtool::CLI.start(["column"], stdin: StringIO.new, stdout: StringIO.new, stderr: StringIO.new)
|
|
25
25
|
assert_equal 1, status
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
def test_menu_routes_to_row_range_shell
|
|
29
|
+
stdout = StringIO.new
|
|
30
|
+
fixture = File.expand_path("../fixtures/sample_people.csv", __dir__)
|
|
31
|
+
input = ["2", fixture, "", "2", "3", "", "3"].join("\n") + "\n"
|
|
32
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: stdout, stderr: StringIO.new)
|
|
33
|
+
assert_equal 0, status
|
|
34
|
+
assert_includes stdout.string, "name,city"
|
|
35
|
+
assert_includes stdout.string, "Bob,Paris"
|
|
36
|
+
assert_includes stdout.string, "Cara,Berlin"
|
|
37
|
+
end
|
|
27
38
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../../test_helper"
|
|
4
|
-
require "csvtool/domain/
|
|
4
|
+
require "csvtool/domain/column_session/column_selection"
|
|
5
5
|
|
|
6
6
|
class ColumnSelectionTest < Minitest::Test
|
|
7
7
|
def test_stores_name
|
|
8
|
-
selection = Csvtool::Domain::
|
|
8
|
+
selection = Csvtool::Domain::ColumnSession::ColumnSelection.new(name: "city")
|
|
9
9
|
assert_equal "city", selection.name
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/column_session/column_session"
|
|
5
|
+
require "csvtool/domain/column_session/csv_source"
|
|
6
|
+
require "csvtool/domain/column_session/separator"
|
|
7
|
+
require "csvtool/domain/column_session/column_selection"
|
|
8
|
+
require "csvtool/domain/column_session/extraction_options"
|
|
9
|
+
require "csvtool/domain/column_session/preview"
|
|
10
|
+
require "csvtool/domain/column_session/extraction_value"
|
|
11
|
+
require "csvtool/domain/column_session/output_destination"
|
|
12
|
+
|
|
13
|
+
class ColumnSessionTest < Minitest::Test
|
|
14
|
+
def test_state_transitions
|
|
15
|
+
session = Csvtool::Domain::ColumnSession::ColumnSession.start(
|
|
16
|
+
source: Csvtool::Domain::ColumnSession::CsvSource.new(
|
|
17
|
+
path: "/tmp/in.csv",
|
|
18
|
+
separator: Csvtool::Domain::ColumnSession::Separator.new(",")
|
|
19
|
+
),
|
|
20
|
+
column_selection: Csvtool::Domain::ColumnSession::ColumnSelection.new(name: "name"),
|
|
21
|
+
options: Csvtool::Domain::ColumnSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 10)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
preview = Csvtool::Domain::ColumnSession::Preview.new(
|
|
25
|
+
values: [Csvtool::Domain::ColumnSession::ExtractionValue.new("Alice")]
|
|
26
|
+
)
|
|
27
|
+
session = session.with_preview(preview).confirm!.with_output_destination(
|
|
28
|
+
Csvtool::Domain::ColumnSession::OutputDestination.console
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
assert_equal true, session.confirmed?
|
|
32
|
+
assert_equal preview, session.preview
|
|
33
|
+
assert_equal true, session.output_destination.console?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/column_session/csv_source"
|
|
5
|
+
require "csvtool/domain/column_session/separator"
|
|
6
|
+
|
|
7
|
+
class CsvSourceTest < Minitest::Test
|
|
8
|
+
def test_stores_path_and_separator
|
|
9
|
+
separator = Csvtool::Domain::ColumnSession::Separator.new(",")
|
|
10
|
+
source = Csvtool::Domain::ColumnSession::CsvSource.new(path: "/tmp/a.csv", separator: separator)
|
|
11
|
+
assert_equal "/tmp/a.csv", source.path
|
|
12
|
+
assert_equal separator, source.separator
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../../test_helper"
|
|
4
|
-
require "csvtool/domain/
|
|
4
|
+
require "csvtool/domain/column_session/extraction_options"
|
|
5
5
|
|
|
6
6
|
class ExtractionOptionsTest < Minitest::Test
|
|
7
7
|
def test_exposes_options
|
|
8
|
-
options = Csvtool::Domain::
|
|
8
|
+
options = Csvtool::Domain::ColumnSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 10)
|
|
9
9
|
assert_equal true, options.skip_blanks?
|
|
10
10
|
assert_equal 10, options.preview_limit
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def test_non_positive_preview_limit_raises
|
|
14
14
|
assert_raises(ArgumentError) do
|
|
15
|
-
Csvtool::Domain::
|
|
15
|
+
Csvtool::Domain::ColumnSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 0)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../../test_helper"
|
|
4
|
-
require "csvtool/domain/
|
|
4
|
+
require "csvtool/domain/column_session/extraction_value"
|
|
5
5
|
|
|
6
6
|
class ExtractionValueTest < Minitest::Test
|
|
7
7
|
def test_stringifies_value
|
|
8
|
-
value = Csvtool::Domain::
|
|
8
|
+
value = Csvtool::Domain::ColumnSession::ExtractionValue.new(123)
|
|
9
9
|
assert_equal "123", value.value
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../../test_helper"
|
|
4
|
-
require "csvtool/domain/
|
|
4
|
+
require "csvtool/domain/column_session/output_destination"
|
|
5
5
|
|
|
6
6
|
class OutputDestinationTest < Minitest::Test
|
|
7
7
|
def test_console_factory
|
|
8
|
-
destination = Csvtool::Domain::
|
|
8
|
+
destination = Csvtool::Domain::ColumnSession::OutputDestination.console
|
|
9
9
|
assert_equal true, destination.console?
|
|
10
10
|
assert_equal false, destination.file?
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def test_file_factory
|
|
14
|
-
destination = Csvtool::Domain::
|
|
14
|
+
destination = Csvtool::Domain::ColumnSession::OutputDestination.file(path: "/tmp/out.csv")
|
|
15
15
|
assert_equal true, destination.file?
|
|
16
16
|
assert_equal "/tmp/out.csv", destination.path
|
|
17
17
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/column_session/preview"
|
|
5
|
+
require "csvtool/domain/column_session/extraction_value"
|
|
6
|
+
|
|
7
|
+
class PreviewTest < Minitest::Test
|
|
8
|
+
def test_exposes_size_and_string_values
|
|
9
|
+
values = [
|
|
10
|
+
Csvtool::Domain::ColumnSession::ExtractionValue.new("Alice"),
|
|
11
|
+
Csvtool::Domain::ColumnSession::ExtractionValue.new("Bob")
|
|
12
|
+
]
|
|
13
|
+
preview = Csvtool::Domain::ColumnSession::Preview.new(values: values)
|
|
14
|
+
|
|
15
|
+
assert_equal 2, preview.size
|
|
16
|
+
assert_equal %w[Alice Bob], preview.to_strings
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../../test_helper"
|
|
4
|
-
require "csvtool/domain/
|
|
4
|
+
require "csvtool/domain/column_session/separator"
|
|
5
5
|
|
|
6
6
|
class SeparatorTest < Minitest::Test
|
|
7
7
|
def test_stores_value
|
|
8
|
-
separator = Csvtool::Domain::
|
|
8
|
+
separator = Csvtool::Domain::ColumnSession::Separator.new(",")
|
|
9
9
|
assert_equal ",", separator.value
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def test_empty_value_raises
|
|
13
|
-
assert_raises(ArgumentError) { Csvtool::Domain::
|
|
13
|
+
assert_raises(ArgumentError) { Csvtool::Domain::ColumnSession::Separator.new("") }
|
|
14
14
|
end
|
|
15
15
|
end
|