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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5a04bb138984be7581485f1db2ac6b145ca446846d54179e47ee42f28e4bcf48
|
|
4
|
+
data.tar.gz: 2cb0f7ae0e9dea000f9422c922bae2842649eec253752c362cd4301ecf60a29d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea2610440ae2db2052b4e317ad6ec066ed7641fe5a09cf3b5e9a5ea8e80249474f636d3899cf5cc99ae07f23da279148c301296a5f0a24af61aae2d35605717c
|
|
7
|
+
data.tar.gz: 60da56d279491e8e3b9401d2bca7910beac884eb9bab38ee67b18dc48e3880caf1c9a82454905eb0d4d2dd71611ad6141b2b708bd962e13045faf569bd67540c
|
data/README.md
CHANGED
|
@@ -34,7 +34,8 @@ bundle exec csvtool menu
|
|
|
34
34
|
```text
|
|
35
35
|
CSV Tool Menu
|
|
36
36
|
1. Extract column
|
|
37
|
-
2.
|
|
37
|
+
2. Extract rows (range)
|
|
38
|
+
3. Exit
|
|
38
39
|
>
|
|
39
40
|
```
|
|
40
41
|
|
|
@@ -126,7 +127,7 @@ bundle exec rake test
|
|
|
126
127
|
|
|
127
128
|
## Alpha release
|
|
128
129
|
|
|
129
|
-
Current prerelease version: `0.
|
|
130
|
+
Current prerelease version: `0.2.0.alpha`
|
|
130
131
|
|
|
131
132
|
Install prerelease from RubyGems:
|
|
132
133
|
|
|
@@ -136,25 +137,25 @@ gem install csvops --pre
|
|
|
136
137
|
|
|
137
138
|
Release runbook:
|
|
138
139
|
|
|
139
|
-
-
|
|
140
|
+
- `docs/release-v0.2.0-alpha.md`
|
|
140
141
|
|
|
141
142
|
## Architecture
|
|
142
143
|
|
|
143
144
|
The codebase follows a DDD-lite layered structure:
|
|
144
145
|
|
|
145
|
-
- `domain/`: core domain
|
|
146
|
-
- `application/`: use-case orchestration (`RunExtraction`).
|
|
146
|
+
- `domain/`: core domain models and invariants (`ColumnSession` and `RowSession` aggregates + supporting entities/value objects).
|
|
147
|
+
- `application/`: use-case orchestration (`RunExtraction`, `RunRowExtraction`).
|
|
147
148
|
- `infrastructure/`: CSV reading/streaming and output adapters (console/file).
|
|
148
149
|
- `interface/cli/`: menu, prompts, and user-facing error presentation.
|
|
149
150
|
- `Csvtool::CLI`: entrypoint wiring from command args to interface/application flow.
|
|
150
151
|
|
|
151
152
|
## Domain model
|
|
152
153
|
|
|
153
|
-
Bounded
|
|
154
|
+
Bounded contexts: `Column Extraction` and `Row Extraction`.
|
|
154
155
|
|
|
155
|
-
|
|
156
|
+
### Column Extraction
|
|
156
157
|
|
|
157
|
-
- Aggregate root: `
|
|
158
|
+
- Aggregate root: `ColumnSession`
|
|
158
159
|
- Controls extraction state transitions (`start`, `with_preview`, `confirm!`, `with_output_destination`).
|
|
159
160
|
- Enforces session-level invariants.
|
|
160
161
|
- Entities:
|
|
@@ -181,7 +182,7 @@ Core DDD structure:
|
|
|
181
182
|
```mermaid
|
|
182
183
|
flowchart LR
|
|
183
184
|
UI["Interface CLI\n(Menu + Prompts + Errors)"] --> APP["Application Use Case\nRunExtraction"]
|
|
184
|
-
APP --> AGG["Domain Aggregate\
|
|
185
|
+
APP --> AGG["Domain Aggregate\nColumnSession"]
|
|
185
186
|
|
|
186
187
|
AGG --> E1["Entity\nCsvSource"]
|
|
187
188
|
AGG --> E2["Entity\nColumnSelection"]
|
|
@@ -191,13 +192,51 @@ flowchart LR
|
|
|
191
192
|
APP --> INFOUT["Infrastructure Output\nConsoleWriter + CsvFileWriter"]
|
|
192
193
|
```
|
|
193
194
|
|
|
195
|
+
### Row Extraction
|
|
196
|
+
|
|
197
|
+
Core DDD structure:
|
|
198
|
+
|
|
199
|
+
- Aggregate root: `RowSession`
|
|
200
|
+
- Captures one row-range extraction request.
|
|
201
|
+
- Holds selected source, requested range, and output destination.
|
|
202
|
+
- Entity:
|
|
203
|
+
- `RowSource` (file path + separator)
|
|
204
|
+
- Value objects:
|
|
205
|
+
- `RowRange` (`start_row`, `end_row`) plus row-range validation errors
|
|
206
|
+
- `RowOutputDestination` (`console` or `file(path)`)
|
|
207
|
+
- Application service:
|
|
208
|
+
- `Application::UseCases::RunRowExtraction` orchestrates row-range extraction.
|
|
209
|
+
- Infrastructure adapters:
|
|
210
|
+
- `Infrastructure::CSV::HeaderReader`
|
|
211
|
+
- `Infrastructure::CSV::RowStreamer`
|
|
212
|
+
- `Infrastructure::Output::CsvRowConsoleWriter`
|
|
213
|
+
- `Infrastructure::Output::CsvRowFileWriter`
|
|
214
|
+
- Interface adapters:
|
|
215
|
+
- `Interface::CLI::MenuLoop`
|
|
216
|
+
- `Interface::CLI::Prompts::*`
|
|
217
|
+
- `Interface::CLI::Errors::Presenter`
|
|
218
|
+
|
|
219
|
+
```mermaid
|
|
220
|
+
flowchart LR
|
|
221
|
+
UI2["Interface CLI\n(Menu + Prompts + Errors)"] --> APP2["Application Use Case\nRunRowExtraction"]
|
|
222
|
+
APP2 --> AGG2["Domain Aggregate\nRowSession"]
|
|
223
|
+
|
|
224
|
+
AGG2 --> E3["Entity\nRowSource"]
|
|
225
|
+
AGG2 --> V2["Value Objects\nRowRange / RowOutputDestination"]
|
|
226
|
+
|
|
227
|
+
APP2 --> INFCSV2["Infrastructure CSV\nHeaderReader + RowStreamer"]
|
|
228
|
+
APP2 --> INFOUT2["Infrastructure Output\nCsvRowConsoleWriter + CsvRowFileWriter"]
|
|
229
|
+
```
|
|
230
|
+
|
|
194
231
|
## Project layout
|
|
195
232
|
|
|
196
233
|
```text
|
|
197
234
|
bin/tool # CLI entrypoint
|
|
198
235
|
lib/csvtool/cli.rb
|
|
199
|
-
lib/csvtool/domain/
|
|
236
|
+
lib/csvtool/domain/column_session/*
|
|
237
|
+
lib/csvtool/domain/row_session/*
|
|
200
238
|
lib/csvtool/application/use_cases/run_extraction.rb
|
|
239
|
+
lib/csvtool/application/use_cases/run_row_extraction.rb
|
|
201
240
|
lib/csvtool/infrastructure/csv/*
|
|
202
241
|
lib/csvtool/infrastructure/output/*
|
|
203
242
|
lib/csvtool/interface/cli/menu_loop.rb
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Release Checklist: v0.2.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
|
+
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. Smoke test row extraction workflow
|
|
41
|
+
|
|
42
|
+
Use menu option `2` (`Extract rows (range)`), then verify:
|
|
43
|
+
- console output path works
|
|
44
|
+
- file output path works
|
|
45
|
+
- row-range validation errors are handled cleanly
|
|
46
|
+
|
|
47
|
+
## 6. Build and validate gem package
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
gem build csvops.gemspec
|
|
51
|
+
gem install ./csvops-0.2.0.alpha.gem
|
|
52
|
+
csvtool menu
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 7. Commit release prep
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git add -A
|
|
59
|
+
git commit -m "chore(release): prepare v0.2.0-alpha"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 8. Tag release
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git tag -a v0.2.0-alpha -m "v0.2.0-alpha"
|
|
66
|
+
git push origin main --tags
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 9. Publish gem (optional for alpha)
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
gem push csvops-0.2.0.alpha.gem
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 10. Create GitHub release
|
|
76
|
+
|
|
77
|
+
Create release `v0.2.0-alpha` with:
|
|
78
|
+
- Summary of supported commands (`menu`, `column`, `row range`)
|
|
79
|
+
- Notes on output destination support (`console`, `file`)
|
|
80
|
+
- Install instructions (`gem install csvops --pre`)
|
|
@@ -13,14 +13,14 @@ require "csvtool/infrastructure/csv/value_streamer"
|
|
|
13
13
|
require "csvtool/services/preview_builder"
|
|
14
14
|
require "csvtool/infrastructure/output/console_writer"
|
|
15
15
|
require "csvtool/infrastructure/output/csv_file_writer"
|
|
16
|
-
require "csvtool/domain/
|
|
17
|
-
require "csvtool/domain/
|
|
18
|
-
require "csvtool/domain/
|
|
19
|
-
require "csvtool/domain/
|
|
20
|
-
require "csvtool/domain/
|
|
21
|
-
require "csvtool/domain/
|
|
22
|
-
require "csvtool/domain/
|
|
23
|
-
require "csvtool/domain/
|
|
16
|
+
require "csvtool/domain/column_session/separator"
|
|
17
|
+
require "csvtool/domain/column_session/csv_source"
|
|
18
|
+
require "csvtool/domain/column_session/column_selection"
|
|
19
|
+
require "csvtool/domain/column_session/extraction_options"
|
|
20
|
+
require "csvtool/domain/column_session/extraction_value"
|
|
21
|
+
require "csvtool/domain/column_session/preview"
|
|
22
|
+
require "csvtool/domain/column_session/output_destination"
|
|
23
|
+
require "csvtool/domain/column_session/column_session"
|
|
24
24
|
|
|
25
25
|
module Csvtool
|
|
26
26
|
module Application
|
|
@@ -41,19 +41,19 @@ module Csvtool
|
|
|
41
41
|
|
|
42
42
|
col_sep = Interface::CLI::Prompts::SeparatorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call
|
|
43
43
|
return if col_sep.nil?
|
|
44
|
-
separator = Domain::
|
|
44
|
+
separator = Domain::ColumnSession::Separator.new(col_sep)
|
|
45
45
|
|
|
46
|
-
source = Domain::
|
|
46
|
+
source = Domain::ColumnSession::CsvSource.new(path: file_path, separator: separator)
|
|
47
47
|
headers = @header_reader.call(file_path: source.path, col_sep: source.separator.value)
|
|
48
48
|
return @errors.no_headers if headers.empty?
|
|
49
49
|
|
|
50
50
|
column_name = Interface::CLI::Prompts::ColumnSelectorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call(headers)
|
|
51
51
|
return if column_name.nil?
|
|
52
|
-
column_selection = Domain::
|
|
52
|
+
column_selection = Domain::ColumnSession::ColumnSelection.new(name: column_name)
|
|
53
53
|
|
|
54
54
|
skip_blanks = Interface::CLI::Prompts::SkipBlanksPrompt.new(stdin: @stdin, stdout: @stdout).call
|
|
55
|
-
options = Domain::
|
|
56
|
-
session = Domain::
|
|
55
|
+
options = Domain::ColumnSession::ExtractionOptions.new(skip_blanks: skip_blanks, preview_limit: 10)
|
|
56
|
+
session = Domain::ColumnSession::ColumnSession.start(
|
|
57
57
|
source: source,
|
|
58
58
|
column_selection: column_selection,
|
|
59
59
|
options: options
|
|
@@ -66,8 +66,8 @@ module Csvtool
|
|
|
66
66
|
skip_blanks: session.options.skip_blanks?,
|
|
67
67
|
limit: session.options.preview_limit
|
|
68
68
|
)
|
|
69
|
-
preview = Domain::
|
|
70
|
-
values: preview_values.map { |value| Domain::
|
|
69
|
+
preview = Domain::ColumnSession::Preview.new(
|
|
70
|
+
values: preview_values.map { |value| Domain::ColumnSession::ExtractionValue.new(value) }
|
|
71
71
|
)
|
|
72
72
|
session = session.with_preview(preview)
|
|
73
73
|
|
|
@@ -79,9 +79,9 @@ module Csvtool
|
|
|
79
79
|
return if output_destination.nil?
|
|
80
80
|
domain_destination =
|
|
81
81
|
if output_destination[:mode] == :file
|
|
82
|
-
Domain::
|
|
82
|
+
Domain::ColumnSession::OutputDestination.file(path: output_destination[:path])
|
|
83
83
|
else
|
|
84
|
-
Domain::
|
|
84
|
+
Domain::ColumnSession::OutputDestination.console
|
|
85
85
|
end
|
|
86
86
|
session = session.with_output_destination(domain_destination)
|
|
87
87
|
|
|
@@ -0,0 +1,111 @@
|
|
|
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/output_destination_prompt"
|
|
8
|
+
require "csvtool/infrastructure/csv/header_reader"
|
|
9
|
+
require "csvtool/infrastructure/csv/row_streamer"
|
|
10
|
+
require "csvtool/infrastructure/output/csv_row_console_writer"
|
|
11
|
+
require "csvtool/infrastructure/output/csv_row_file_writer"
|
|
12
|
+
require "csvtool/domain/row_session/row_range"
|
|
13
|
+
require "csvtool/domain/row_session/row_source"
|
|
14
|
+
require "csvtool/domain/row_session/row_output_destination"
|
|
15
|
+
require "csvtool/domain/row_session/row_session"
|
|
16
|
+
|
|
17
|
+
module Csvtool
|
|
18
|
+
module Application
|
|
19
|
+
module UseCases
|
|
20
|
+
class RunRowExtraction
|
|
21
|
+
def initialize(stdin:, stdout:)
|
|
22
|
+
@stdin = stdin
|
|
23
|
+
@stdout = stdout
|
|
24
|
+
@errors = Interface::CLI::Errors::Presenter.new(stdout: stdout)
|
|
25
|
+
@header_reader = Infrastructure::CSV::HeaderReader.new
|
|
26
|
+
@row_streamer = Infrastructure::CSV::RowStreamer.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call
|
|
30
|
+
file_path = Interface::CLI::Prompts::FilePathPrompt.new(stdin: @stdin, stdout: @stdout).call
|
|
31
|
+
return @errors.file_not_found(file_path) unless File.file?(file_path)
|
|
32
|
+
|
|
33
|
+
col_sep = Interface::CLI::Prompts::SeparatorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call
|
|
34
|
+
return if col_sep.nil?
|
|
35
|
+
source = Domain::RowSession::RowSource.new(path: file_path, separator: col_sep)
|
|
36
|
+
|
|
37
|
+
@stdout.print "Start row (1-based, inclusive): "
|
|
38
|
+
start_row_input = @stdin.gets&.strip.to_s
|
|
39
|
+
@stdout.print "End row (1-based, inclusive): "
|
|
40
|
+
end_row_input = @stdin.gets&.strip.to_s
|
|
41
|
+
|
|
42
|
+
headers = @header_reader.call(file_path: source.path, col_sep: source.separator)
|
|
43
|
+
return @errors.no_headers if headers.empty?
|
|
44
|
+
|
|
45
|
+
row_range = Domain::RowSession::RowRange.from_inputs(
|
|
46
|
+
start_row_input: start_row_input,
|
|
47
|
+
end_row_input: end_row_input
|
|
48
|
+
)
|
|
49
|
+
session = Domain::RowSession::RowSession.start(source: source, row_range: row_range)
|
|
50
|
+
|
|
51
|
+
output_destination = Interface::CLI::Prompts::OutputDestinationPrompt.new(
|
|
52
|
+
stdin: @stdin,
|
|
53
|
+
stdout: @stdout,
|
|
54
|
+
errors: @errors
|
|
55
|
+
).call
|
|
56
|
+
return if output_destination.nil?
|
|
57
|
+
destination =
|
|
58
|
+
if output_destination[:mode] == :file
|
|
59
|
+
Domain::RowSession::RowOutputDestination.file(path: output_destination[:path])
|
|
60
|
+
else
|
|
61
|
+
Domain::RowSession::RowOutputDestination.console
|
|
62
|
+
end
|
|
63
|
+
session = session.with_output_destination(destination)
|
|
64
|
+
|
|
65
|
+
stats =
|
|
66
|
+
if session.output_destination.file?
|
|
67
|
+
Infrastructure::Output::CsvRowFileWriter.new(
|
|
68
|
+
stdout: @stdout,
|
|
69
|
+
errors: @errors,
|
|
70
|
+
row_streamer: @row_streamer
|
|
71
|
+
).call(
|
|
72
|
+
output_path: session.output_destination.path,
|
|
73
|
+
file_path: session.source.path,
|
|
74
|
+
col_sep: session.source.separator,
|
|
75
|
+
headers: headers,
|
|
76
|
+
start_row: session.row_range.start_row,
|
|
77
|
+
end_row: session.row_range.end_row
|
|
78
|
+
)
|
|
79
|
+
else
|
|
80
|
+
Infrastructure::Output::CsvRowConsoleWriter.new(stdout: @stdout, row_streamer: @row_streamer).call(
|
|
81
|
+
file_path: session.source.path,
|
|
82
|
+
col_sep: session.source.separator,
|
|
83
|
+
headers: headers,
|
|
84
|
+
start_row: session.row_range.start_row,
|
|
85
|
+
end_row: session.row_range.end_row
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
return if stats.nil?
|
|
89
|
+
|
|
90
|
+
@errors.row_range_out_of_bounds(stats[:row_count]) unless stats[:matched]
|
|
91
|
+
rescue Domain::RowSession::InvalidStartRowError
|
|
92
|
+
@errors.invalid_start_row
|
|
93
|
+
rescue Domain::RowSession::InvalidEndRowError
|
|
94
|
+
@errors.invalid_end_row
|
|
95
|
+
rescue Domain::RowSession::InvalidRowRangeOrderError
|
|
96
|
+
@errors.invalid_row_range_order
|
|
97
|
+
rescue ArgumentError => e
|
|
98
|
+
return @errors.empty_output_path if e.message == "file output path cannot be empty"
|
|
99
|
+
|
|
100
|
+
raise e
|
|
101
|
+
rescue CSV::MalformedCSVError
|
|
102
|
+
@errors.could_not_parse_csv
|
|
103
|
+
rescue Errno::EACCES
|
|
104
|
+
@errors.cannot_read_file(file_path)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/csvtool/cli.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "csv"
|
|
4
4
|
require "csvtool/interface/cli/menu_loop"
|
|
5
5
|
require "csvtool/application/use_cases/run_extraction"
|
|
6
|
+
require "csvtool/application/use_cases/run_row_extraction"
|
|
6
7
|
require "csvtool/interface/cli/errors/presenter"
|
|
7
8
|
require "csvtool/infrastructure/csv/header_reader"
|
|
8
9
|
require "csvtool/infrastructure/csv/value_streamer"
|
|
@@ -12,6 +13,7 @@ module Csvtool
|
|
|
12
13
|
class CLI
|
|
13
14
|
MENU_OPTIONS = [
|
|
14
15
|
"Extract column",
|
|
16
|
+
"Extract rows (range)",
|
|
15
17
|
"Exit"
|
|
16
18
|
].freeze
|
|
17
19
|
|
|
@@ -41,12 +43,14 @@ module Csvtool
|
|
|
41
43
|
private
|
|
42
44
|
|
|
43
45
|
def run_menu_loop
|
|
44
|
-
|
|
46
|
+
extract_column_action = -> { Application::UseCases::RunExtraction.new(stdin: @stdin, stdout: @stdout).call }
|
|
47
|
+
extract_rows_action = -> { Application::UseCases::RunRowExtraction.new(stdin: @stdin, stdout: @stdout).call }
|
|
45
48
|
Interface::CLI::MenuLoop.new(
|
|
46
49
|
stdin: @stdin,
|
|
47
50
|
stdout: @stdout,
|
|
48
51
|
menu_options: MENU_OPTIONS,
|
|
49
|
-
|
|
52
|
+
extract_column_action: extract_column_action,
|
|
53
|
+
extract_rows_action: extract_rows_action
|
|
50
54
|
).run
|
|
51
55
|
end
|
|
52
56
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Csvtool
|
|
4
4
|
module Domain
|
|
5
|
-
module
|
|
6
|
-
class
|
|
5
|
+
module ColumnSession
|
|
6
|
+
class ColumnSession
|
|
7
7
|
attr_reader :source, :column_selection, :options, :preview, :output_destination
|
|
8
8
|
|
|
9
9
|
def self.start(source:, column_selection:, options:)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csvtool
|
|
4
|
+
module Domain
|
|
5
|
+
module RowSession
|
|
6
|
+
class RowOutputDestination
|
|
7
|
+
attr_reader :mode, :path
|
|
8
|
+
|
|
9
|
+
def self.console
|
|
10
|
+
new(mode: :console)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.file(path:)
|
|
14
|
+
new(mode: :file, path: path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(mode:, path: nil)
|
|
18
|
+
raise ArgumentError, "invalid output mode" unless %i[console file].include?(mode)
|
|
19
|
+
raise ArgumentError, "file output path cannot be empty" if mode == :file && path.to_s.empty?
|
|
20
|
+
|
|
21
|
+
@mode = mode
|
|
22
|
+
@path = path
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def file?
|
|
26
|
+
@mode == :file
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csvtool
|
|
4
|
+
module Domain
|
|
5
|
+
module RowSession
|
|
6
|
+
class InvalidStartRowError < StandardError; end
|
|
7
|
+
class InvalidEndRowError < StandardError; end
|
|
8
|
+
class InvalidRowRangeOrderError < StandardError; end
|
|
9
|
+
|
|
10
|
+
class RowRange
|
|
11
|
+
attr_reader :start_row, :end_row
|
|
12
|
+
|
|
13
|
+
def self.from_inputs(start_row_input:, end_row_input:)
|
|
14
|
+
unless /\A[1-9]\d*\z/.match?(start_row_input.to_s)
|
|
15
|
+
raise InvalidStartRowError, "invalid start row"
|
|
16
|
+
end
|
|
17
|
+
unless /\A[1-9]\d*\z/.match?(end_row_input.to_s)
|
|
18
|
+
raise InvalidEndRowError, "invalid end row"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
start_row = start_row_input.to_i
|
|
22
|
+
end_row = end_row_input.to_i
|
|
23
|
+
raise InvalidRowRangeOrderError, "end row before start row" if end_row < start_row
|
|
24
|
+
|
|
25
|
+
new(start_row: start_row, end_row: end_row)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(start_row:, end_row:)
|
|
29
|
+
raise InvalidStartRowError, "invalid start row" unless start_row.to_i >= 1
|
|
30
|
+
raise InvalidEndRowError, "invalid end row" unless end_row.to_i >= 1
|
|
31
|
+
raise InvalidRowRangeOrderError, "end row before start row" if end_row < start_row
|
|
32
|
+
|
|
33
|
+
@start_row = start_row
|
|
34
|
+
@end_row = end_row
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csvtool
|
|
4
|
+
module Domain
|
|
5
|
+
module RowSession
|
|
6
|
+
class RowSession
|
|
7
|
+
attr_reader :source, :row_range, :output_destination
|
|
8
|
+
|
|
9
|
+
def self.start(source:, row_range:)
|
|
10
|
+
new(source: source, row_range: row_range)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(source:, row_range:, output_destination: nil)
|
|
14
|
+
@source = source
|
|
15
|
+
@row_range = row_range
|
|
16
|
+
@output_destination = output_destination
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def with_output_destination(destination)
|
|
20
|
+
self.class.new(source: @source, row_range: @row_range, output_destination: destination)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csvtool
|
|
4
|
+
module Domain
|
|
5
|
+
module RowSession
|
|
6
|
+
class RowSource
|
|
7
|
+
attr_reader :path, :separator
|
|
8
|
+
|
|
9
|
+
def initialize(path:, separator:)
|
|
10
|
+
@path = path
|
|
11
|
+
@separator = separator
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
5
|
+
module Csvtool
|
|
6
|
+
module Infrastructure
|
|
7
|
+
module CSV
|
|
8
|
+
class RowStreamer
|
|
9
|
+
def each_in_range(file_path:, col_sep:, start_row:, end_row:)
|
|
10
|
+
row_index = 0
|
|
11
|
+
matched = false
|
|
12
|
+
|
|
13
|
+
::CSV.foreach(file_path, headers: true, col_sep: col_sep) do |row|
|
|
14
|
+
row_index += 1
|
|
15
|
+
next if row_index < start_row
|
|
16
|
+
break if row_index > end_row
|
|
17
|
+
|
|
18
|
+
matched = true
|
|
19
|
+
yield row.fields
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
{ matched: matched, row_count: row_index }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|