csvops 0.1.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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +8 -0
  3. data/README.md +209 -0
  4. data/Rakefile +10 -0
  5. data/bin/csvtool +8 -0
  6. data/bin/tool +8 -0
  7. data/csvops.gemspec +25 -0
  8. data/docs/release-v0.1.0-alpha.md +73 -0
  9. data/exe/csvtool +8 -0
  10. data/lib/csvtool/application/use_cases/run_extraction.rb +125 -0
  11. data/lib/csvtool/cli.rb +87 -0
  12. data/lib/csvtool/domain/extraction_session/column_selection.rb +17 -0
  13. data/lib/csvtool/domain/extraction_session/csv_source.rb +18 -0
  14. data/lib/csvtool/domain/extraction_session/extraction_options.rb +22 -0
  15. data/lib/csvtool/domain/extraction_session/extraction_session.rb +61 -0
  16. data/lib/csvtool/domain/extraction_session/extraction_value.rb +15 -0
  17. data/lib/csvtool/domain/extraction_session/output_destination.rb +35 -0
  18. data/lib/csvtool/domain/extraction_session/preview.rb +23 -0
  19. data/lib/csvtool/domain/extraction_session/separator.rb +17 -0
  20. data/lib/csvtool/infrastructure/csv/header_reader.rb +17 -0
  21. data/lib/csvtool/infrastructure/csv/value_streamer.rb +20 -0
  22. data/lib/csvtool/infrastructure/output/console_writer.rb +20 -0
  23. data/lib/csvtool/infrastructure/output/csv_file_writer.rb +30 -0
  24. data/lib/csvtool/interface/cli/errors/presenter.rb +59 -0
  25. data/lib/csvtool/interface/cli/menu_loop.rb +41 -0
  26. data/lib/csvtool/interface/cli/prompts/column_selector_prompt.rb +54 -0
  27. data/lib/csvtool/interface/cli/prompts/confirm_prompt.rb +29 -0
  28. data/lib/csvtool/interface/cli/prompts/file_path_prompt.rb +21 -0
  29. data/lib/csvtool/interface/cli/prompts/output_destination_prompt.rb +40 -0
  30. data/lib/csvtool/interface/cli/prompts/separator_prompt.rb +44 -0
  31. data/lib/csvtool/interface/cli/prompts/skip_blanks_prompt.rb +22 -0
  32. data/lib/csvtool/services/preview_builder.rb +20 -0
  33. data/lib/csvtool/version.rb +5 -0
  34. data/test/csvtool/application/use_cases/run_extraction_test.rb +31 -0
  35. data/test/csvtool/cli_test.rb +134 -0
  36. data/test/csvtool/cli_unit_test.rb +27 -0
  37. data/test/csvtool/domain/extraction_session/column_selection_test.rb +11 -0
  38. data/test/csvtool/domain/extraction_session/csv_source_test.rb +14 -0
  39. data/test/csvtool/domain/extraction_session/extraction_options_test.rb +18 -0
  40. data/test/csvtool/domain/extraction_session/extraction_session_test.rb +35 -0
  41. data/test/csvtool/domain/extraction_session/extraction_value_test.rb +11 -0
  42. data/test/csvtool/domain/extraction_session/output_destination_test.rb +18 -0
  43. data/test/csvtool/domain/extraction_session/preview_test.rb +18 -0
  44. data/test/csvtool/domain/extraction_session/separator_test.rb +15 -0
  45. data/test/csvtool/infrastructure/csv/header_reader_test.rb +16 -0
  46. data/test/csvtool/infrastructure/csv/value_streamer_test.rb +22 -0
  47. data/test/csvtool/infrastructure/output/console_writer_test.rb +19 -0
  48. data/test/csvtool/infrastructure/output/csv_file_writer_test.rb +35 -0
  49. data/test/csvtool/interface/cli/errors/presenter_test.rb +36 -0
  50. data/test/csvtool/interface/cli/menu_loop_test.rb +51 -0
  51. data/test/csvtool/interface/cli/prompts/column_selector_prompt_test.rb +23 -0
  52. data/test/csvtool/interface/cli/prompts/confirm_prompt_test.rb +23 -0
  53. data/test/csvtool/interface/cli/prompts/file_path_prompt_test.rb +11 -0
  54. data/test/csvtool/interface/cli/prompts/output_destination_prompt_test.rb +28 -0
  55. data/test/csvtool/interface/cli/prompts/separator_prompt_test.rb +31 -0
  56. data/test/csvtool/interface/cli/prompts/skip_blanks_prompt_test.rb +13 -0
  57. data/test/csvtool/services/preview_builder_test.rb +22 -0
  58. data/test/fixtures/empty.csv +0 -0
  59. data/test/fixtures/sample_people.csv +4 -0
  60. data/test/fixtures/sample_people.tsv +4 -0
  61. data/test/fixtures/sample_people_blanks.csv +6 -0
  62. data/test/fixtures/sample_people_colon.txt +4 -0
  63. data/test/fixtures/sample_people_many.csv +13 -0
  64. data/test/test_helper.rb +6 -0
  65. metadata +150 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8621d11595ca64afe19513bbeb253a9568ef78566aff8eec116a2b169e899fce
4
+ data.tar.gz: '009a655572331c7699ca8c6034d9bcf45db517e90da4ae4317d3b42c6b4aa7dd'
5
+ SHA512:
6
+ metadata.gz: 542f540de68616558cfe40e39fecbb4a3b91fa7a2bf8fd59397e5a9aad855503795e928c1ddae18c3577e6e3558b34166e3c240e6b7744e11d0c6b8a11297aa6
7
+ data.tar.gz: 04a5beb85795c5b10984a8a12980a1079a8a4f07f447d6621d6e6961ded35ee18edb5834e5e8fbad99b0f7fd0c76c2aae31f7d2ccc48f3b54a520d7bcbf91931
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ ruby "3.3.0"
4
+
5
+ gemspec
6
+
7
+ gem "minitest"
8
+ gem "rake"
data/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # CSV Ops CLI
2
+
3
+ `csvops` is a small Ruby CLI for interactive CSV workflows.
4
+
5
+ ## Requirements
6
+
7
+ - Ruby 3.3.0
8
+ - Bundler
9
+ - `rake`
10
+ - `minitest`
11
+
12
+ Install dependencies:
13
+
14
+ ```bash
15
+ bundle install
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### 1. Start the CLI
21
+
22
+ ```bash
23
+ csvtool menu
24
+ ```
25
+
26
+ With Bundler:
27
+
28
+ ```bash
29
+ bundle exec csvtool menu
30
+ ```
31
+
32
+ ### 2. Choose an action
33
+
34
+ ```text
35
+ CSV Tool Menu
36
+ 1. Extract column
37
+ 2. Exit
38
+ >
39
+ ```
40
+
41
+ Select `1` to run extraction.
42
+
43
+ ### 3. Follow prompts
44
+
45
+ Prompt flow:
46
+
47
+ - CSV file path
48
+ - Separator (`comma`, `tab`, `semicolon`, `pipe`, or `custom`)
49
+ - Optional header filter + column selection
50
+ - Skip blanks (`Y/n`, default `Y`)
51
+ - Preview + confirmation
52
+ - Output destination (`console` or `file`)
53
+
54
+ ### 4. Example interaction (console output)
55
+
56
+ Legend: ` ` = prompt/menu, `+` = user input, `-` = tool output
57
+
58
+ ```diff
59
+ CSV file path: /path/to/file.csv
60
+ Choose separator:
61
+ 1. comma (,)
62
+ 2. tab (\t)
63
+ 3. semicolon (;)
64
+ 4. pipe (|)
65
+ 5. custom
66
+ +Separator choice [1]: 1
67
+ Filter columns (optional):
68
+ Select column:
69
+ 1. name
70
+ 2. city
71
+ +Column number: 1
72
+ Skip blank values? [Y/n]:
73
+ Preview (first 3 values):
74
+ -Alice
75
+ -Bob
76
+ -Cara
77
+ Print all values? [y/N]:
78
+ +y
79
+ Output destination:
80
+ 1. console
81
+ 2. file
82
+ +Output destination [1]: 1
83
+ -Alice
84
+ -Bob
85
+ -Cara
86
+ ```
87
+
88
+ ### 5. Example interaction (file output)
89
+
90
+ ```diff
91
+ Output destination:
92
+ 1. console
93
+ 2. file
94
+ +Output destination [1]: 2
95
+ +Output file path: /tmp/names.csv
96
+ -Wrote output to /tmp/names.csv
97
+ ```
98
+
99
+ ### 6. Direct command mode
100
+
101
+ Extract a column without using the interactive menu:
102
+
103
+ ```bash
104
+ csvtool column /path/to/file.csv column_name
105
+ ```
106
+
107
+ With Bundler:
108
+
109
+ ```bash
110
+ bundle exec csvtool column /path/to/file.csv column_name
111
+ ```
112
+
113
+ ## Testing
114
+
115
+ Run tests:
116
+
117
+ ```bash
118
+ rake test
119
+ ```
120
+
121
+ Or:
122
+
123
+ ```bash
124
+ bundle exec rake test
125
+ ```
126
+
127
+ ## Alpha release
128
+
129
+ Current prerelease version: `0.1.0.alpha`
130
+
131
+ Install prerelease from RubyGems:
132
+
133
+ ```bash
134
+ gem install csvops --pre
135
+ ```
136
+
137
+ Release runbook:
138
+
139
+ - `/Users/roberthall/Projects/csvops/docs/release-v0.1.0-alpha.md`
140
+
141
+ ## Architecture
142
+
143
+ The codebase follows a DDD-lite layered structure:
144
+
145
+ - `domain/`: core domain model and invariants (`ExtractionSession` aggregate + value objects/entities).
146
+ - `application/`: use-case orchestration (`RunExtraction`).
147
+ - `infrastructure/`: CSV reading/streaming and output adapters (console/file).
148
+ - `interface/cli/`: menu, prompts, and user-facing error presentation.
149
+ - `Csvtool::CLI`: entrypoint wiring from command args to interface/application flow.
150
+
151
+ ## Domain model
152
+
153
+ Bounded context: `Column Extraction`.
154
+
155
+ Core DDD structure:
156
+
157
+ - Aggregate root: `ExtractionSession`
158
+ - Controls extraction state transitions (`start`, `with_preview`, `confirm!`, `with_output_destination`).
159
+ - Enforces session-level invariants.
160
+ - Entities:
161
+ - `CsvSource` (file path + `Separator`)
162
+ - `ColumnSelection` (chosen header)
163
+ - Value objects:
164
+ - `Separator`
165
+ - `ExtractionOptions` (`skip_blanks`, `preview_limit`)
166
+ - `Preview` (list of `ExtractionValue`)
167
+ - `ExtractionValue`
168
+ - `OutputDestination` (`console` or `file(path)`)
169
+ - Application service:
170
+ - `Application::UseCases::RunExtraction` orchestrates one extraction request.
171
+ - Infrastructure adapters:
172
+ - `Infrastructure::CSV::HeaderReader`
173
+ - `Infrastructure::CSV::ValueStreamer`
174
+ - `Infrastructure::Output::ConsoleWriter`
175
+ - `Infrastructure::Output::CsvFileWriter`
176
+ - Interface adapters:
177
+ - `Interface::CLI::MenuLoop`
178
+ - `Interface::CLI::Prompts::*`
179
+ - `Interface::CLI::Errors::Presenter`
180
+
181
+ ```mermaid
182
+ flowchart LR
183
+ UI["Interface CLI\n(Menu + Prompts + Errors)"] --> APP["Application Use Case\nRunExtraction"]
184
+ APP --> AGG["Domain Aggregate\nExtractionSession"]
185
+
186
+ AGG --> E1["Entity\nCsvSource"]
187
+ AGG --> E2["Entity\nColumnSelection"]
188
+ AGG --> V1["Value Objects\nSeparator / ExtractionOptions / Preview / OutputDestination / ExtractionValue"]
189
+
190
+ APP --> INFCSV["Infrastructure CSV\nHeaderReader + ValueStreamer"]
191
+ APP --> INFOUT["Infrastructure Output\nConsoleWriter + CsvFileWriter"]
192
+ ```
193
+
194
+ ## Project layout
195
+
196
+ ```text
197
+ bin/tool # CLI entrypoint
198
+ lib/csvtool/cli.rb
199
+ lib/csvtool/domain/extraction_session/*
200
+ lib/csvtool/application/use_cases/run_extraction.rb
201
+ lib/csvtool/infrastructure/csv/*
202
+ lib/csvtool/infrastructure/output/*
203
+ lib/csvtool/interface/cli/menu_loop.rb
204
+ lib/csvtool/interface/cli/prompts/*
205
+ lib/csvtool/interface/cli/errors/presenter.rb
206
+ test/csvtool/cli_test.rb # end-to-end workflow tests
207
+ test/csvtool/**/*_test.rb # focused unit tests by component folder
208
+ test/test_helper.rb
209
+ ```
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "test"
7
+ t.pattern = "test/**/*_test.rb"
8
+ end
9
+
10
+ task default: :test
data/bin/csvtool ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require "csvtool/cli"
7
+
8
+ exit Csvtool::CLI.start(ARGV, stdin: $stdin, stdout: $stdout, stderr: $stderr)
data/bin/tool ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require "csvtool/cli"
7
+
8
+ exit Csvtool::CLI.start(ARGV, stdin: $stdin, stdout: $stdout, stderr: $stderr)
data/csvops.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/csvtool/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "csvops"
7
+ spec.version = Csvtool::VERSION
8
+ spec.authors = ["Robert Hall"]
9
+ spec.email = [""]
10
+
11
+ spec.summary = "Interactive CSV column extraction CLI"
12
+ spec.description = "A small Ruby CLI for extracting CSV columns interactively or via direct command."
13
+ spec.homepage = "https://github.com/RobertAndrewHall/csvops"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.3"
16
+
17
+ spec.files = Dir.glob("{lib,exe,bin,test,docs}/**/*") + %w[README.md Gemfile Rakefile csvops.gemspec]
18
+ spec.bindir = "exe"
19
+ spec.executables = ["csvtool"]
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "csv", "~> 3.3"
23
+ spec.add_development_dependency "minitest", "~> 6.0"
24
+ spec.add_development_dependency "rake", "~> 13.0"
25
+ end
@@ -0,0 +1,73 @@
1
+ # Release Checklist: v0.1.0-alpha
2
+
3
+ ## 1. Verify environment
4
+
5
+ ```bash
6
+ ruby -v
7
+ bundle -v
8
+ ```
9
+
10
+ Expected:
11
+ - Ruby `3.3.0`
12
+
13
+ ## 2. Install dependencies
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ ## 3. Run quality checks
20
+
21
+ ```bash
22
+ bundle exec rake test
23
+ ```
24
+
25
+ ## 4. Smoke test CLI commands
26
+
27
+ ```bash
28
+ printf '2\n' | bundle exec csvtool menu
29
+ bundle exec csvtool column test/fixtures/sample_people.csv name
30
+ ```
31
+
32
+ Expected output for `column` command:
33
+
34
+ ```text
35
+ Alice
36
+ Bob
37
+ Cara
38
+ ```
39
+
40
+ ## 5. Build and validate gem package
41
+
42
+ ```bash
43
+ gem build csvops.gemspec
44
+ gem install ./csvops-0.1.0.alpha.gem
45
+ csvtool column test/fixtures/sample_people.csv name
46
+ ```
47
+
48
+ ## 6. Commit release prep
49
+
50
+ ```bash
51
+ git add -A
52
+ git commit -m "chore(release): prepare v0.1.0-alpha"
53
+ ```
54
+
55
+ ## 7. Tag release
56
+
57
+ ```bash
58
+ git tag -a v0.1.0-alpha -m "v0.1.0-alpha"
59
+ git push origin main --tags
60
+ ```
61
+
62
+ ## 8. Publish gem (optional for alpha)
63
+
64
+ ```bash
65
+ gem push csvops-0.1.0.alpha.gem
66
+ ```
67
+
68
+ ## 9. Create GitHub release
69
+
70
+ Create release `v0.1.0-alpha` with:
71
+ - Summary of supported commands (`menu`, `column`)
72
+ - Known limitations
73
+ - Install instructions (`gem install csvops --pre`)
data/exe/csvtool ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require "csvtool/cli"
7
+
8
+ exit Csvtool::CLI.start(ARGV, stdin: $stdin, stdout: $stdout, stderr: $stderr)
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "csvtool/interface/cli/errors/presenter"
5
+ require "csvtool/interface/cli/prompts/file_path_prompt"
6
+ require "csvtool/interface/cli/prompts/separator_prompt"
7
+ require "csvtool/interface/cli/prompts/column_selector_prompt"
8
+ require "csvtool/interface/cli/prompts/skip_blanks_prompt"
9
+ require "csvtool/interface/cli/prompts/confirm_prompt"
10
+ require "csvtool/interface/cli/prompts/output_destination_prompt"
11
+ require "csvtool/infrastructure/csv/header_reader"
12
+ require "csvtool/infrastructure/csv/value_streamer"
13
+ require "csvtool/services/preview_builder"
14
+ require "csvtool/infrastructure/output/console_writer"
15
+ require "csvtool/infrastructure/output/csv_file_writer"
16
+ require "csvtool/domain/extraction_session/separator"
17
+ require "csvtool/domain/extraction_session/csv_source"
18
+ require "csvtool/domain/extraction_session/column_selection"
19
+ require "csvtool/domain/extraction_session/extraction_options"
20
+ require "csvtool/domain/extraction_session/extraction_value"
21
+ require "csvtool/domain/extraction_session/preview"
22
+ require "csvtool/domain/extraction_session/output_destination"
23
+ require "csvtool/domain/extraction_session/extraction_session"
24
+
25
+ module Csvtool
26
+ module Application
27
+ module UseCases
28
+ class RunExtraction
29
+ def initialize(stdin:, stdout:)
30
+ @stdin = stdin
31
+ @stdout = stdout
32
+ @errors = Interface::CLI::Errors::Presenter.new(stdout: stdout)
33
+ @header_reader = Infrastructure::CSV::HeaderReader.new
34
+ @value_streamer = Infrastructure::CSV::ValueStreamer.new
35
+ @preview_builder = Services::PreviewBuilder.new(value_streamer: @value_streamer)
36
+ end
37
+
38
+ def call
39
+ file_path = Interface::CLI::Prompts::FilePathPrompt.new(stdin: @stdin, stdout: @stdout).call
40
+ return @errors.file_not_found(file_path) unless File.file?(file_path)
41
+
42
+ col_sep = Interface::CLI::Prompts::SeparatorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call
43
+ return if col_sep.nil?
44
+ separator = Domain::ExtractionSession::Separator.new(col_sep)
45
+
46
+ source = Domain::ExtractionSession::CsvSource.new(path: file_path, separator: separator)
47
+ headers = @header_reader.call(file_path: source.path, col_sep: source.separator.value)
48
+ return @errors.no_headers if headers.empty?
49
+
50
+ column_name = Interface::CLI::Prompts::ColumnSelectorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call(headers)
51
+ return if column_name.nil?
52
+ column_selection = Domain::ExtractionSession::ColumnSelection.new(name: column_name)
53
+
54
+ skip_blanks = Interface::CLI::Prompts::SkipBlanksPrompt.new(stdin: @stdin, stdout: @stdout).call
55
+ options = Domain::ExtractionSession::ExtractionOptions.new(skip_blanks: skip_blanks, preview_limit: 10)
56
+ session = Domain::ExtractionSession::ExtractionSession.start(
57
+ source: source,
58
+ column_selection: column_selection,
59
+ options: options
60
+ )
61
+
62
+ preview_values = @preview_builder.call(
63
+ file_path: session.source.path,
64
+ column_name: session.column_selection.name,
65
+ col_sep: session.source.separator.value,
66
+ skip_blanks: session.options.skip_blanks?,
67
+ limit: session.options.preview_limit
68
+ )
69
+ preview = Domain::ExtractionSession::Preview.new(
70
+ values: preview_values.map { |value| Domain::ExtractionSession::ExtractionValue.new(value) }
71
+ )
72
+ session = session.with_preview(preview)
73
+
74
+ confirmed = Interface::CLI::Prompts::ConfirmPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call(session.preview.to_strings)
75
+ return unless confirmed
76
+ session = session.confirm!
77
+
78
+ output_destination = Interface::CLI::Prompts::OutputDestinationPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call
79
+ return if output_destination.nil?
80
+ domain_destination =
81
+ if output_destination[:mode] == :file
82
+ Domain::ExtractionSession::OutputDestination.file(path: output_destination[:path])
83
+ else
84
+ Domain::ExtractionSession::OutputDestination.console
85
+ end
86
+ session = session.with_output_destination(domain_destination)
87
+
88
+ write_output(
89
+ session.output_destination,
90
+ file_path: session.source.path,
91
+ column_name: session.column_selection.name,
92
+ col_sep: session.source.separator.value,
93
+ skip_blanks: session.options.skip_blanks?
94
+ )
95
+ rescue CSV::MalformedCSVError
96
+ @errors.could_not_parse_csv
97
+ rescue Errno::EACCES
98
+ @errors.cannot_read_file(file_path)
99
+ end
100
+
101
+ private
102
+
103
+ def writer_for(output_destination)
104
+ if output_destination.file?
105
+ Infrastructure::Output::CsvFileWriter.new(stdout: @stdout, errors: @errors, value_streamer: @value_streamer)
106
+ else
107
+ Infrastructure::Output::ConsoleWriter.new(stdout: @stdout, value_streamer: @value_streamer)
108
+ end
109
+ end
110
+
111
+ def write_output(output_destination, file_path:, column_name:, col_sep:, skip_blanks:)
112
+ writer = writer_for(output_destination)
113
+ args = {
114
+ file_path: file_path,
115
+ column_name: column_name,
116
+ col_sep: col_sep,
117
+ skip_blanks: skip_blanks
118
+ }
119
+ args[:output_path] = output_destination.path if output_destination.file?
120
+ writer.call(**args)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "csvtool/interface/cli/menu_loop"
5
+ require "csvtool/application/use_cases/run_extraction"
6
+ require "csvtool/interface/cli/errors/presenter"
7
+ require "csvtool/infrastructure/csv/header_reader"
8
+ require "csvtool/infrastructure/csv/value_streamer"
9
+ require "csvtool/infrastructure/output/console_writer"
10
+
11
+ module Csvtool
12
+ class CLI
13
+ MENU_OPTIONS = [
14
+ "Extract column",
15
+ "Exit"
16
+ ].freeze
17
+
18
+ def self.start(argv, stdin:, stdout:, stderr:)
19
+ new(argv, stdin: stdin, stdout: stdout, stderr: stderr).run
20
+ end
21
+
22
+ def initialize(argv, stdin:, stdout:, stderr:)
23
+ @argv = argv
24
+ @stdin = stdin
25
+ @stdout = stdout
26
+ @stderr = stderr
27
+ end
28
+
29
+ def run
30
+ case @argv.first
31
+ when "menu"
32
+ run_menu_loop
33
+ when "column"
34
+ run_column_command
35
+ else
36
+ print_usage
37
+ 1
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def run_menu_loop
44
+ extract_action = -> { Application::UseCases::RunExtraction.new(stdin: @stdin, stdout: @stdout).call }
45
+ Interface::CLI::MenuLoop.new(
46
+ stdin: @stdin,
47
+ stdout: @stdout,
48
+ menu_options: MENU_OPTIONS,
49
+ extract_action: extract_action
50
+ ).run
51
+ end
52
+
53
+ def print_usage
54
+ @stderr.puts "Usage:"
55
+ @stderr.puts " csvtool menu"
56
+ @stderr.puts " csvtool column <file> <column>"
57
+ end
58
+
59
+ def run_column_command
60
+ file_path = @argv[1]
61
+ column_name = @argv[2]
62
+ unless file_path && column_name
63
+ print_usage
64
+ return 1
65
+ end
66
+
67
+ errors = Interface::CLI::Errors::Presenter.new(stdout: @stdout)
68
+ return errors.file_not_found(file_path) || 1 unless File.file?(file_path)
69
+
70
+ header_reader = Infrastructure::CSV::HeaderReader.new
71
+ headers = header_reader.call(file_path: file_path, col_sep: ",")
72
+ return errors.no_headers || 1 if headers.empty?
73
+ return errors.column_not_found || 1 unless headers.include?(column_name)
74
+
75
+ value_streamer = Infrastructure::CSV::ValueStreamer.new
76
+ writer = Infrastructure::Output::ConsoleWriter.new(stdout: @stdout, value_streamer: value_streamer)
77
+ writer.call(file_path: file_path, column_name: column_name, col_sep: ",", skip_blanks: true)
78
+ 0
79
+ rescue CSV::MalformedCSVError
80
+ errors.could_not_parse_csv
81
+ 1
82
+ rescue Errno::EACCES
83
+ errors.cannot_read_file(file_path)
84
+ 1
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Csvtool
4
+ module Domain
5
+ module ExtractionSession
6
+ class ColumnSelection
7
+ attr_reader :name
8
+
9
+ def initialize(name:)
10
+ raise ArgumentError, "column name cannot be empty" if name.to_s.empty?
11
+
12
+ @name = name
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Csvtool
4
+ module Domain
5
+ module ExtractionSession
6
+ class CsvSource
7
+ attr_reader :path, :separator
8
+
9
+ def initialize(path:, separator:)
10
+ raise ArgumentError, "path cannot be empty" if path.to_s.empty?
11
+
12
+ @path = path
13
+ @separator = separator
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Csvtool
4
+ module Domain
5
+ module ExtractionSession
6
+ class ExtractionOptions
7
+ attr_reader :skip_blanks, :preview_limit
8
+
9
+ def initialize(skip_blanks:, preview_limit:)
10
+ raise ArgumentError, "preview_limit must be positive" unless preview_limit.to_i.positive?
11
+
12
+ @skip_blanks = !!skip_blanks
13
+ @preview_limit = preview_limit
14
+ end
15
+
16
+ def skip_blanks?
17
+ @skip_blanks
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Csvtool
4
+ module Domain
5
+ module ExtractionSession
6
+ class ExtractionSession
7
+ attr_reader :source, :column_selection, :options, :preview, :output_destination
8
+
9
+ def self.start(source:, column_selection:, options:)
10
+ new(source: source, column_selection: column_selection, options: options)
11
+ end
12
+
13
+ def initialize(source:, column_selection:, options:, preview: nil, output_destination: nil, confirmed: false)
14
+ @source = source
15
+ @column_selection = column_selection
16
+ @options = options
17
+ @preview = preview
18
+ @output_destination = output_destination
19
+ @confirmed = confirmed
20
+ end
21
+
22
+ def with_preview(preview)
23
+ self.class.new(
24
+ source: @source,
25
+ column_selection: @column_selection,
26
+ options: @options,
27
+ preview: preview,
28
+ output_destination: @output_destination,
29
+ confirmed: @confirmed
30
+ )
31
+ end
32
+
33
+ def confirm!
34
+ self.class.new(
35
+ source: @source,
36
+ column_selection: @column_selection,
37
+ options: @options,
38
+ preview: @preview,
39
+ output_destination: @output_destination,
40
+ confirmed: true
41
+ )
42
+ end
43
+
44
+ def with_output_destination(destination)
45
+ self.class.new(
46
+ source: @source,
47
+ column_selection: @column_selection,
48
+ options: @options,
49
+ preview: @preview,
50
+ output_destination: destination,
51
+ confirmed: @confirmed
52
+ )
53
+ end
54
+
55
+ def confirmed?
56
+ @confirmed
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end