csvops 0.1.0.alpha → 0.3.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 +83 -10
- data/docs/release-v0.2.0-alpha.md +80 -0
- data/docs/release-v0.3.0-alpha.md +74 -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/application/use_cases/run_row_randomization.rb +105 -0
- data/lib/csvtool/cli.rb +10 -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_randomization_session/randomization_options.rb +17 -0
- data/lib/csvtool/domain/row_randomization_session/randomization_output_destination.rb +31 -0
- data/lib/csvtool/domain/row_randomization_session/randomization_session.rb +25 -0
- data/lib/csvtool/domain/row_randomization_session/randomization_source.rb +23 -0
- 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_randomizer.rb +83 -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 +20 -0
- data/lib/csvtool/interface/cli/menu_loop.rb +13 -5
- data/lib/csvtool/interface/cli/prompts/headers_present_prompt.rb +22 -0
- data/lib/csvtool/interface/cli/prompts/seed_prompt.rb +29 -0
- data/lib/csvtool/version.rb +1 -1
- data/test/csvtool/application/use_cases/run_row_extraction_test.rb +140 -0
- data/test/csvtool/application/use_cases/run_row_randomization_test.rb +124 -0
- data/test/csvtool/cli_test.rb +237 -6
- data/test/csvtool/cli_unit_test.rb +24 -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_randomization_session/randomization_options_test.rb +20 -0
- data/test/csvtool/domain/row_randomization_session/randomization_output_destination_test.rb +21 -0
- data/test/csvtool/domain/row_randomization_session/randomization_session_test.rb +26 -0
- data/test/csvtool/domain/row_randomization_session/randomization_source_test.rb +28 -0
- 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_randomizer_test.rb +37 -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 +10 -0
- data/test/csvtool/interface/cli/menu_loop_test.rb +68 -12
- data/test/csvtool/interface/cli/prompts/headers_present_prompt_test.rb +14 -0
- data/test/csvtool/interface/cli/prompts/seed_prompt_test.rb +39 -0
- data/test/fixtures/sample_people_bad_tail.csv +5 -0
- data/test/fixtures/sample_people_no_headers.csv +3 -0
- metadata +53 -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: 9663c50901b31a8073c4a5a0524e9e30c81c20bbe1b736af71649e60a7150a0e
|
|
4
|
+
data.tar.gz: a622dad35eb52afeded279726d2575db0cd210c8ed4aa07650506bfd2e2b6de5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28726bb66d05881caead074ce529d79db5424b85a7552f8b56cca44e891b3bb0c34cd850ff22351d6d93a5ef725e3891ccc8b7ac7e1e62d24d4e4c4d7d9b344d
|
|
7
|
+
data.tar.gz: 0fb96011d8737fb757b30e6226b9086453aa3ef6e5def1e95b77bb8bdbd414e25b72adfe6a8e65ee0498bbb88f2a809777a0d2405057689d00b3858c73014b93
|
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.3.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.3.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`, `RowSession`, and `RandomizationSession` aggregates + supporting entities/value objects).
|
|
147
|
+
- `application/`: use-case orchestration (`RunExtraction`, `RunRowExtraction`, `RunRowRandomization`).
|
|
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`, `Row Extraction`, and `Row Randomization`.
|
|
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,85 @@ 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
|
+
|
|
231
|
+
### Row Randomization
|
|
232
|
+
|
|
233
|
+
Core DDD structure:
|
|
234
|
+
|
|
235
|
+
- Aggregate root: `RandomizationSession`
|
|
236
|
+
- Captures one randomization request from source + options + output destination.
|
|
237
|
+
- Entity:
|
|
238
|
+
- `RandomizationSource` (file path + separator + header mode)
|
|
239
|
+
- Value objects:
|
|
240
|
+
- `RandomizationOptions` (optional deterministic `seed`)
|
|
241
|
+
- `RandomizationOutputDestination` (`console` or `file(path)`)
|
|
242
|
+
- Application service:
|
|
243
|
+
- `Application::UseCases::RunRowRandomization` orchestrates row randomization.
|
|
244
|
+
- Infrastructure adapters:
|
|
245
|
+
- `Infrastructure::CSV::HeaderReader`
|
|
246
|
+
- `Infrastructure::CSV::RowRandomizer` (external chunked `RAND + sort` + merge)
|
|
247
|
+
- Interface adapters:
|
|
248
|
+
- `Interface::CLI::MenuLoop`
|
|
249
|
+
- `Interface::CLI::Prompts::*`
|
|
250
|
+
- `Interface::CLI::Errors::Presenter`
|
|
251
|
+
|
|
252
|
+
```mermaid
|
|
253
|
+
flowchart LR
|
|
254
|
+
UI3["Interface CLI\n(Menu + Prompts + Errors)"] --> APP3["Application Use Case\nRunRowRandomization"]
|
|
255
|
+
APP3 --> AGG3["Domain Aggregate\nRandomizationSession"]
|
|
256
|
+
|
|
257
|
+
AGG3 --> E4["Entity\nRandomizationSource"]
|
|
258
|
+
AGG3 --> V3["Value Objects\nRandomizationOptions / RandomizationOutputDestination"]
|
|
259
|
+
|
|
260
|
+
APP3 --> INFCSV3["Infrastructure CSV\nHeaderReader + RowRandomizer"]
|
|
261
|
+
```
|
|
262
|
+
|
|
194
263
|
## Project layout
|
|
195
264
|
|
|
196
265
|
```text
|
|
197
266
|
bin/tool # CLI entrypoint
|
|
198
267
|
lib/csvtool/cli.rb
|
|
199
|
-
lib/csvtool/domain/
|
|
268
|
+
lib/csvtool/domain/column_session/*
|
|
269
|
+
lib/csvtool/domain/row_session/*
|
|
270
|
+
lib/csvtool/domain/row_randomization_session/*
|
|
200
271
|
lib/csvtool/application/use_cases/run_extraction.rb
|
|
272
|
+
lib/csvtool/application/use_cases/run_row_extraction.rb
|
|
273
|
+
lib/csvtool/application/use_cases/run_row_randomization.rb
|
|
201
274
|
lib/csvtool/infrastructure/csv/*
|
|
202
275
|
lib/csvtool/infrastructure/output/*
|
|
203
276
|
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`)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Release Checklist: v0.3.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
|
+
## 5. Smoke test row randomization workflow
|
|
33
|
+
|
|
34
|
+
Use menu option `3` (`Randomize rows`) and verify:
|
|
35
|
+
- headered CSV output keeps header in first row
|
|
36
|
+
- seeded mode is reproducible
|
|
37
|
+
- file output path writes valid CSV
|
|
38
|
+
- headerless mode randomizes all rows
|
|
39
|
+
|
|
40
|
+
## 6. Build and validate gem package
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
gem build csvops.gemspec
|
|
44
|
+
gem install ./csvops-0.3.0.alpha.gem
|
|
45
|
+
csvtool menu
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 7. Commit release prep
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git add -A
|
|
52
|
+
git commit -m "chore(release): prepare v0.3.0-alpha"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 8. Tag release
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git tag -a v0.3.0-alpha -m "v0.3.0-alpha"
|
|
59
|
+
git push origin main --tags
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 9. Publish gem (optional for alpha)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
gem push csvops-0.3.0.alpha.gem
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 10. Create GitHub release
|
|
69
|
+
|
|
70
|
+
Create release `v0.3.0-alpha` with:
|
|
71
|
+
- Randomize rows workflow support
|
|
72
|
+
- Seeded deterministic randomization
|
|
73
|
+
- External chunked randomization strategy for large files
|
|
74
|
+
- Updated domain model (`RowRandomizationSession`)
|
|
@@ -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
|
|
@@ -0,0 +1,105 @@
|
|
|
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/headers_present_prompt"
|
|
8
|
+
require "csvtool/interface/cli/prompts/seed_prompt"
|
|
9
|
+
require "csvtool/interface/cli/prompts/output_destination_prompt"
|
|
10
|
+
require "csvtool/infrastructure/csv/header_reader"
|
|
11
|
+
require "csvtool/infrastructure/csv/row_randomizer"
|
|
12
|
+
require "csvtool/domain/row_randomization_session/randomization_source"
|
|
13
|
+
require "csvtool/domain/row_randomization_session/randomization_options"
|
|
14
|
+
require "csvtool/domain/row_randomization_session/randomization_output_destination"
|
|
15
|
+
require "csvtool/domain/row_randomization_session/randomization_session"
|
|
16
|
+
|
|
17
|
+
module Csvtool
|
|
18
|
+
module Application
|
|
19
|
+
module UseCases
|
|
20
|
+
class RunRowRandomization
|
|
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_randomizer = Infrastructure::CSV::RowRandomizer.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
|
+
|
|
36
|
+
headers_present = Interface::CLI::Prompts::HeadersPresentPrompt.new(stdin: @stdin, stdout: @stdout).call
|
|
37
|
+
source = Domain::RowRandomizationSession::RandomizationSource.new(
|
|
38
|
+
path: file_path,
|
|
39
|
+
separator: col_sep,
|
|
40
|
+
headers_present: headers_present
|
|
41
|
+
)
|
|
42
|
+
headers = source.headers_present? ? @header_reader.call(file_path: source.path, col_sep: source.separator) : nil
|
|
43
|
+
return @errors.no_headers if source.headers_present? && headers.empty?
|
|
44
|
+
|
|
45
|
+
seed = Interface::CLI::Prompts::SeedPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call
|
|
46
|
+
return if seed == Interface::CLI::Prompts::SeedPrompt::INVALID
|
|
47
|
+
options = Domain::RowRandomizationSession::RandomizationOptions.new(seed: seed)
|
|
48
|
+
session = Domain::RowRandomizationSession::RandomizationSession.start(source: source, options: options)
|
|
49
|
+
|
|
50
|
+
output_destination = Interface::CLI::Prompts::OutputDestinationPrompt.new(
|
|
51
|
+
stdin: @stdin,
|
|
52
|
+
stdout: @stdout,
|
|
53
|
+
errors: @errors
|
|
54
|
+
).call
|
|
55
|
+
return if output_destination.nil?
|
|
56
|
+
destination =
|
|
57
|
+
if output_destination[:mode] == :file
|
|
58
|
+
Domain::RowRandomizationSession::RandomizationOutputDestination.file(path: output_destination[:path])
|
|
59
|
+
else
|
|
60
|
+
Domain::RowRandomizationSession::RandomizationOutputDestination.console
|
|
61
|
+
end
|
|
62
|
+
session = session.with_output_destination(destination)
|
|
63
|
+
|
|
64
|
+
randomized_rows = @row_randomizer.each(
|
|
65
|
+
file_path: session.source.path,
|
|
66
|
+
col_sep: session.source.separator,
|
|
67
|
+
headers: session.source.headers_present?,
|
|
68
|
+
seed: session.options.seed
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if session.output_destination.file?
|
|
72
|
+
write_output_file(session.output_destination.path, headers, randomized_rows, col_sep: session.source.separator)
|
|
73
|
+
else
|
|
74
|
+
print_to_console(headers, randomized_rows, col_sep: session.source.separator)
|
|
75
|
+
end
|
|
76
|
+
rescue CSV::MalformedCSVError
|
|
77
|
+
@errors.could_not_parse_csv
|
|
78
|
+
rescue ArgumentError => e
|
|
79
|
+
return @errors.empty_output_path if e.message == "file output path cannot be empty"
|
|
80
|
+
|
|
81
|
+
raise e
|
|
82
|
+
rescue Errno::EACCES
|
|
83
|
+
@errors.cannot_read_file(file_path)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def print_to_console(headers, rows, col_sep:)
|
|
89
|
+
@stdout.puts
|
|
90
|
+
@stdout.puts ::CSV.generate_line(headers, row_sep: "", col_sep: col_sep).chomp if headers
|
|
91
|
+
rows.each { |fields| @stdout.puts ::CSV.generate_line(fields, row_sep: "", col_sep: col_sep).chomp }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def write_output_file(path, headers, rows, col_sep:)
|
|
95
|
+
::CSV.open(path, "w", write_headers: !headers.nil?, headers: headers, col_sep: col_sep) do |csv|
|
|
96
|
+
rows.each { |fields| csv << fields }
|
|
97
|
+
end
|
|
98
|
+
@stdout.puts "Wrote output to #{path}"
|
|
99
|
+
rescue Errno::EACCES, Errno::ENOENT => e
|
|
100
|
+
@errors.cannot_write_output_file(path, e.class)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/csvtool/cli.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
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"
|
|
7
|
+
require "csvtool/application/use_cases/run_row_randomization"
|
|
6
8
|
require "csvtool/interface/cli/errors/presenter"
|
|
7
9
|
require "csvtool/infrastructure/csv/header_reader"
|
|
8
10
|
require "csvtool/infrastructure/csv/value_streamer"
|
|
@@ -12,6 +14,8 @@ module Csvtool
|
|
|
12
14
|
class CLI
|
|
13
15
|
MENU_OPTIONS = [
|
|
14
16
|
"Extract column",
|
|
17
|
+
"Extract rows (range)",
|
|
18
|
+
"Randomize rows",
|
|
15
19
|
"Exit"
|
|
16
20
|
].freeze
|
|
17
21
|
|
|
@@ -41,12 +45,16 @@ module Csvtool
|
|
|
41
45
|
private
|
|
42
46
|
|
|
43
47
|
def run_menu_loop
|
|
44
|
-
|
|
48
|
+
extract_column_action = -> { Application::UseCases::RunExtraction.new(stdin: @stdin, stdout: @stdout).call }
|
|
49
|
+
extract_rows_action = -> { Application::UseCases::RunRowExtraction.new(stdin: @stdin, stdout: @stdout).call }
|
|
50
|
+
randomize_rows_action = -> { Application::UseCases::RunRowRandomization.new(stdin: @stdin, stdout: @stdout).call }
|
|
45
51
|
Interface::CLI::MenuLoop.new(
|
|
46
52
|
stdin: @stdin,
|
|
47
53
|
stdout: @stdout,
|
|
48
54
|
menu_options: MENU_OPTIONS,
|
|
49
|
-
|
|
55
|
+
extract_column_action: extract_column_action,
|
|
56
|
+
extract_rows_action: extract_rows_action,
|
|
57
|
+
randomize_rows_action: randomize_rows_action
|
|
50
58
|
).run
|
|
51
59
|
end
|
|
52
60
|
|
|
@@ -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:)
|