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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -10
  3. data/docs/release-v0.2.0-alpha.md +80 -0
  4. data/docs/release-v0.3.0-alpha.md +74 -0
  5. data/lib/csvtool/application/use_cases/run_extraction.rb +17 -17
  6. data/lib/csvtool/application/use_cases/run_row_extraction.rb +111 -0
  7. data/lib/csvtool/application/use_cases/run_row_randomization.rb +105 -0
  8. data/lib/csvtool/cli.rb +10 -2
  9. data/lib/csvtool/domain/{extraction_session → column_session}/column_selection.rb +1 -1
  10. data/lib/csvtool/domain/{extraction_session/extraction_session.rb → column_session/column_session.rb} +2 -2
  11. data/lib/csvtool/domain/{extraction_session → column_session}/csv_source.rb +1 -1
  12. data/lib/csvtool/domain/{extraction_session → column_session}/extraction_options.rb +1 -1
  13. data/lib/csvtool/domain/{extraction_session → column_session}/extraction_value.rb +1 -1
  14. data/lib/csvtool/domain/{extraction_session → column_session}/output_destination.rb +1 -1
  15. data/lib/csvtool/domain/{extraction_session → column_session}/preview.rb +1 -1
  16. data/lib/csvtool/domain/{extraction_session → column_session}/separator.rb +1 -1
  17. data/lib/csvtool/domain/row_randomization_session/randomization_options.rb +17 -0
  18. data/lib/csvtool/domain/row_randomization_session/randomization_output_destination.rb +31 -0
  19. data/lib/csvtool/domain/row_randomization_session/randomization_session.rb +25 -0
  20. data/lib/csvtool/domain/row_randomization_session/randomization_source.rb +23 -0
  21. data/lib/csvtool/domain/row_session/row_output_destination.rb +31 -0
  22. data/lib/csvtool/domain/row_session/row_range.rb +39 -0
  23. data/lib/csvtool/domain/row_session/row_session.rb +25 -0
  24. data/lib/csvtool/domain/row_session/row_source.rb +16 -0
  25. data/lib/csvtool/infrastructure/csv/row_randomizer.rb +83 -0
  26. data/lib/csvtool/infrastructure/csv/row_streamer.rb +27 -0
  27. data/lib/csvtool/infrastructure/output/csv_row_console_writer.rb +34 -0
  28. data/lib/csvtool/infrastructure/output/csv_row_file_writer.rb +45 -0
  29. data/lib/csvtool/interface/cli/errors/presenter.rb +20 -0
  30. data/lib/csvtool/interface/cli/menu_loop.rb +13 -5
  31. data/lib/csvtool/interface/cli/prompts/headers_present_prompt.rb +22 -0
  32. data/lib/csvtool/interface/cli/prompts/seed_prompt.rb +29 -0
  33. data/lib/csvtool/version.rb +1 -1
  34. data/test/csvtool/application/use_cases/run_row_extraction_test.rb +140 -0
  35. data/test/csvtool/application/use_cases/run_row_randomization_test.rb +124 -0
  36. data/test/csvtool/cli_test.rb +237 -6
  37. data/test/csvtool/cli_unit_test.rb +24 -1
  38. data/test/csvtool/domain/{extraction_session → column_session}/column_selection_test.rb +2 -2
  39. data/test/csvtool/domain/column_session/column_session_test.rb +35 -0
  40. data/test/csvtool/domain/column_session/csv_source_test.rb +14 -0
  41. data/test/csvtool/domain/{extraction_session → column_session}/extraction_options_test.rb +3 -3
  42. data/test/csvtool/domain/{extraction_session → column_session}/extraction_value_test.rb +2 -2
  43. data/test/csvtool/domain/{extraction_session → column_session}/output_destination_test.rb +3 -3
  44. data/test/csvtool/domain/column_session/preview_test.rb +18 -0
  45. data/test/csvtool/domain/{extraction_session → column_session}/separator_test.rb +3 -3
  46. data/test/csvtool/domain/row_randomization_session/randomization_options_test.rb +20 -0
  47. data/test/csvtool/domain/row_randomization_session/randomization_output_destination_test.rb +21 -0
  48. data/test/csvtool/domain/row_randomization_session/randomization_session_test.rb +26 -0
  49. data/test/csvtool/domain/row_randomization_session/randomization_source_test.rb +28 -0
  50. data/test/csvtool/domain/row_session/row_output_destination_test.rb +23 -0
  51. data/test/csvtool/domain/row_session/row_range_test.rb +30 -0
  52. data/test/csvtool/domain/row_session/row_session_test.rb +22 -0
  53. data/test/csvtool/domain/row_session/row_source_test.rb +12 -0
  54. data/test/csvtool/infrastructure/csv/row_randomizer_test.rb +37 -0
  55. data/test/csvtool/infrastructure/csv/row_streamer_test.rb +41 -0
  56. data/test/csvtool/infrastructure/output/csv_row_console_writer_test.rb +24 -0
  57. data/test/csvtool/infrastructure/output/csv_row_file_writer_test.rb +40 -0
  58. data/test/csvtool/interface/cli/errors/presenter_test.rb +10 -0
  59. data/test/csvtool/interface/cli/menu_loop_test.rb +68 -12
  60. data/test/csvtool/interface/cli/prompts/headers_present_prompt_test.rb +14 -0
  61. data/test/csvtool/interface/cli/prompts/seed_prompt_test.rb +39 -0
  62. data/test/fixtures/sample_people_bad_tail.csv +5 -0
  63. data/test/fixtures/sample_people_no_headers.csv +3 -0
  64. metadata +53 -17
  65. data/test/csvtool/domain/extraction_session/csv_source_test.rb +0 -14
  66. data/test/csvtool/domain/extraction_session/extraction_session_test.rb +0 -35
  67. 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: 8621d11595ca64afe19513bbeb253a9568ef78566aff8eec116a2b169e899fce
4
- data.tar.gz: '009a655572331c7699ca8c6034d9bcf45db517e90da4ae4317d3b42c6b4aa7dd'
3
+ metadata.gz: 9663c50901b31a8073c4a5a0524e9e30c81c20bbe1b736af71649e60a7150a0e
4
+ data.tar.gz: a622dad35eb52afeded279726d2575db0cd210c8ed4aa07650506bfd2e2b6de5
5
5
  SHA512:
6
- metadata.gz: 542f540de68616558cfe40e39fecbb4a3b91fa7a2bf8fd59397e5a9aad855503795e928c1ddae18c3577e6e3558b34166e3c240e6b7744e11d0c6b8a11297aa6
7
- data.tar.gz: 04a5beb85795c5b10984a8a12980a1079a8a4f07f447d6621d6e6961ded35ee18edb5834e5e8fbad99b0f7fd0c76c2aae31f7d2ccc48f3b54a520d7bcbf91931
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. Exit
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.1.0.alpha`
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
- - `/Users/roberthall/Projects/csvops/docs/release-v0.1.0-alpha.md`
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 model and invariants (`ExtractionSession` aggregate + value objects/entities).
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 context: `Column Extraction`.
154
+ Bounded contexts: `Column Extraction`, `Row Extraction`, and `Row Randomization`.
154
155
 
155
- Core DDD structure:
156
+ ### Column Extraction
156
157
 
157
- - Aggregate root: `ExtractionSession`
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\nExtractionSession"]
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/extraction_session/*
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/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"
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::ExtractionSession::Separator.new(col_sep)
44
+ separator = Domain::ColumnSession::Separator.new(col_sep)
45
45
 
46
- source = Domain::ExtractionSession::CsvSource.new(path: file_path, separator: separator)
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::ExtractionSession::ColumnSelection.new(name: column_name)
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::ExtractionSession::ExtractionOptions.new(skip_blanks: skip_blanks, preview_limit: 10)
56
- session = Domain::ExtractionSession::ExtractionSession.start(
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::ExtractionSession::Preview.new(
70
- values: preview_values.map { |value| Domain::ExtractionSession::ExtractionValue.new(value) }
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::ExtractionSession::OutputDestination.file(path: output_destination[:path])
82
+ Domain::ColumnSession::OutputDestination.file(path: output_destination[:path])
83
83
  else
84
- Domain::ExtractionSession::OutputDestination.console
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
- extract_action = -> { Application::UseCases::RunExtraction.new(stdin: @stdin, stdout: @stdout).call }
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
- extract_action: extract_action
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,7 +2,7 @@
2
2
 
3
3
  module Csvtool
4
4
  module Domain
5
- module ExtractionSession
5
+ module ColumnSession
6
6
  class ColumnSelection
7
7
  attr_reader :name
8
8
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Csvtool
4
4
  module Domain
5
- module ExtractionSession
6
- class ExtractionSession
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:)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Csvtool
4
4
  module Domain
5
- module ExtractionSession
5
+ module ColumnSession
6
6
  class CsvSource
7
7
  attr_reader :path, :separator
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Csvtool
4
4
  module Domain
5
- module ExtractionSession
5
+ module ColumnSession
6
6
  class ExtractionOptions
7
7
  attr_reader :skip_blanks, :preview_limit
8
8