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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +49 -10
  3. data/docs/release-v0.2.0-alpha.md +80 -0
  4. data/lib/csvtool/application/use_cases/run_extraction.rb +17 -17
  5. data/lib/csvtool/application/use_cases/run_row_extraction.rb +111 -0
  6. data/lib/csvtool/cli.rb +6 -2
  7. data/lib/csvtool/domain/{extraction_session → column_session}/column_selection.rb +1 -1
  8. data/lib/csvtool/domain/{extraction_session/extraction_session.rb → column_session/column_session.rb} +2 -2
  9. data/lib/csvtool/domain/{extraction_session → column_session}/csv_source.rb +1 -1
  10. data/lib/csvtool/domain/{extraction_session → column_session}/extraction_options.rb +1 -1
  11. data/lib/csvtool/domain/{extraction_session → column_session}/extraction_value.rb +1 -1
  12. data/lib/csvtool/domain/{extraction_session → column_session}/output_destination.rb +1 -1
  13. data/lib/csvtool/domain/{extraction_session → column_session}/preview.rb +1 -1
  14. data/lib/csvtool/domain/{extraction_session → column_session}/separator.rb +1 -1
  15. data/lib/csvtool/domain/row_session/row_output_destination.rb +31 -0
  16. data/lib/csvtool/domain/row_session/row_range.rb +39 -0
  17. data/lib/csvtool/domain/row_session/row_session.rb +25 -0
  18. data/lib/csvtool/domain/row_session/row_source.rb +16 -0
  19. data/lib/csvtool/infrastructure/csv/row_streamer.rb +27 -0
  20. data/lib/csvtool/infrastructure/output/csv_row_console_writer.rb +34 -0
  21. data/lib/csvtool/infrastructure/output/csv_row_file_writer.rb +45 -0
  22. data/lib/csvtool/interface/cli/errors/presenter.rb +16 -0
  23. data/lib/csvtool/interface/cli/menu_loop.rb +10 -5
  24. data/lib/csvtool/version.rb +1 -1
  25. data/test/csvtool/application/use_cases/run_row_extraction_test.rb +140 -0
  26. data/test/csvtool/cli_test.rb +132 -6
  27. data/test/csvtool/cli_unit_test.rb +12 -1
  28. data/test/csvtool/domain/{extraction_session → column_session}/column_selection_test.rb +2 -2
  29. data/test/csvtool/domain/column_session/column_session_test.rb +35 -0
  30. data/test/csvtool/domain/column_session/csv_source_test.rb +14 -0
  31. data/test/csvtool/domain/{extraction_session → column_session}/extraction_options_test.rb +3 -3
  32. data/test/csvtool/domain/{extraction_session → column_session}/extraction_value_test.rb +2 -2
  33. data/test/csvtool/domain/{extraction_session → column_session}/output_destination_test.rb +3 -3
  34. data/test/csvtool/domain/column_session/preview_test.rb +18 -0
  35. data/test/csvtool/domain/{extraction_session → column_session}/separator_test.rb +3 -3
  36. data/test/csvtool/domain/row_session/row_output_destination_test.rb +23 -0
  37. data/test/csvtool/domain/row_session/row_range_test.rb +30 -0
  38. data/test/csvtool/domain/row_session/row_session_test.rb +22 -0
  39. data/test/csvtool/domain/row_session/row_source_test.rb +12 -0
  40. data/test/csvtool/infrastructure/csv/row_streamer_test.rb +41 -0
  41. data/test/csvtool/infrastructure/output/csv_row_console_writer_test.rb +24 -0
  42. data/test/csvtool/infrastructure/output/csv_row_file_writer_test.rb +40 -0
  43. data/test/csvtool/interface/cli/errors/presenter_test.rb +8 -0
  44. data/test/csvtool/interface/cli/menu_loop_test.rb +37 -12
  45. data/test/fixtures/sample_people_bad_tail.csv +5 -0
  46. metadata +35 -17
  47. data/test/csvtool/domain/extraction_session/csv_source_test.rb +0 -14
  48. data/test/csvtool/domain/extraction_session/extraction_session_test.rb +0 -35
  49. 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: 5a04bb138984be7581485f1db2ac6b145ca446846d54179e47ee42f28e4bcf48
4
+ data.tar.gz: 2cb0f7ae0e9dea000f9422c922bae2842649eec253752c362cd4301ecf60a29d
5
5
  SHA512:
6
- metadata.gz: 542f540de68616558cfe40e39fecbb4a3b91fa7a2bf8fd59397e5a9aad855503795e928c1ddae18c3577e6e3558b34166e3c240e6b7744e11d0c6b8a11297aa6
7
- data.tar.gz: 04a5beb85795c5b10984a8a12980a1079a8a4f07f447d6621d6e6961ded35ee18edb5834e5e8fbad99b0f7fd0c76c2aae31f7d2ccc48f3b54a520d7bcbf91931
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. 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.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
- - `/Users/roberthall/Projects/csvops/docs/release-v0.1.0-alpha.md`
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 model and invariants (`ExtractionSession` aggregate + value objects/entities).
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 context: `Column Extraction`.
154
+ Bounded contexts: `Column Extraction` and `Row Extraction`.
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,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/extraction_session/*
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/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
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
- extract_action = -> { Application::UseCases::RunExtraction.new(stdin: @stdin, stdout: @stdout).call }
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
- extract_action: extract_action
52
+ extract_column_action: extract_column_action,
53
+ extract_rows_action: extract_rows_action
50
54
  ).run
51
55
  end
52
56
 
@@ -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
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Csvtool
4
4
  module Domain
5
- module ExtractionSession
5
+ module ColumnSession
6
6
  class ExtractionValue
7
7
  attr_reader :value
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 OutputDestination
7
7
  attr_reader :mode, :path
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 Preview
7
7
  attr_reader :values
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 Separator
7
7
  attr_reader :value
8
8
 
@@ -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