csvops 0.4.0.alpha → 0.6.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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -12
  3. data/docs/architecture.md +208 -21
  4. data/docs/release-v0.5.0-alpha.md +89 -0
  5. data/docs/release-v0.6.0-alpha.md +84 -0
  6. data/lib/csvtool/application/use_cases/run_cross_csv_dedupe.rb +17 -14
  7. data/lib/csvtool/application/use_cases/run_csv_parity.rb +70 -0
  8. data/lib/csvtool/application/use_cases/run_extraction.rb +63 -88
  9. data/lib/csvtool/application/use_cases/run_row_extraction.rb +45 -73
  10. data/lib/csvtool/application/use_cases/run_row_randomization.rb +56 -73
  11. data/lib/csvtool/cli.rb +11 -7
  12. data/lib/csvtool/domain/csv_parity_session/parity_options.rb +22 -0
  13. data/lib/csvtool/domain/csv_parity_session/parity_session.rb +20 -0
  14. data/lib/csvtool/domain/csv_parity_session/source_pair.rb +19 -0
  15. data/lib/csvtool/infrastructure/csv/csv_parity_comparator.rb +71 -0
  16. data/lib/csvtool/infrastructure/output/csv_cross_csv_dedupe_file_writer.rb +23 -0
  17. data/lib/csvtool/infrastructure/output/csv_file_writer.rb +1 -7
  18. data/lib/csvtool/infrastructure/output/csv_randomized_row_file_writer.rb +23 -0
  19. data/lib/csvtool/infrastructure/output/csv_row_file_writer.rb +2 -9
  20. data/lib/csvtool/interface/cli/errors/presenter.rb +4 -0
  21. data/lib/csvtool/interface/cli/menu_loop.rb +5 -2
  22. data/lib/csvtool/interface/cli/prompts/dedupe_key_selector_prompt.rb +30 -0
  23. data/lib/csvtool/interface/cli/prompts/file_path_prompt.rb +4 -2
  24. data/lib/csvtool/interface/cli/prompts/headers_present_prompt.rb +4 -2
  25. data/lib/csvtool/interface/cli/prompts/separator_prompt.rb +4 -2
  26. data/lib/csvtool/interface/cli/prompts/yes_no_prompt.rb +26 -0
  27. data/lib/csvtool/interface/cli/workflows/builders/column_session_builder.rb +32 -0
  28. data/lib/csvtool/interface/cli/workflows/builders/cross_csv_dedupe_session_builder.rb +35 -0
  29. data/lib/csvtool/interface/cli/workflows/builders/csv_parity_session_builder.rb +33 -0
  30. data/lib/csvtool/interface/cli/workflows/builders/row_extraction_session_builder.rb +22 -0
  31. data/lib/csvtool/interface/cli/workflows/builders/row_randomization_session_builder.rb +28 -0
  32. data/lib/csvtool/interface/cli/workflows/presenters/column_extraction_presenter.rb +25 -0
  33. data/lib/csvtool/interface/cli/workflows/presenters/cross_csv_dedupe_presenter.rb +39 -0
  34. data/lib/csvtool/interface/cli/workflows/presenters/csv_parity_presenter.rb +38 -0
  35. data/lib/csvtool/interface/cli/workflows/presenters/row_extraction_presenter.rb +34 -0
  36. data/lib/csvtool/interface/cli/workflows/presenters/row_randomization_presenter.rb +34 -0
  37. data/lib/csvtool/interface/cli/workflows/run_cross_csv_dedupe_workflow.rb +48 -125
  38. data/lib/csvtool/interface/cli/workflows/run_csv_parity_workflow.rb +66 -0
  39. data/lib/csvtool/interface/cli/workflows/run_extraction_workflow.rb +88 -0
  40. data/lib/csvtool/interface/cli/workflows/run_row_extraction_workflow.rb +86 -0
  41. data/lib/csvtool/interface/cli/workflows/run_row_randomization_workflow.rb +80 -0
  42. data/lib/csvtool/interface/cli/workflows/steps/cross_csv_dedupe/collect_options_step.rb +55 -0
  43. data/lib/csvtool/interface/cli/workflows/steps/cross_csv_dedupe/collect_profiles_step.rb +52 -0
  44. data/lib/csvtool/interface/cli/workflows/steps/cross_csv_dedupe/execute_step.rb +34 -0
  45. data/lib/csvtool/interface/cli/workflows/steps/extraction/build_preview_step.rb +40 -0
  46. data/lib/csvtool/interface/cli/workflows/steps/extraction/collect_destination_step.rb +28 -0
  47. data/lib/csvtool/interface/cli/workflows/steps/extraction/collect_inputs_step.rb +47 -0
  48. data/lib/csvtool/interface/cli/workflows/steps/extraction/execute_step.rb +32 -0
  49. data/lib/csvtool/interface/cli/workflows/steps/parity/build_session_step.rb +25 -0
  50. data/lib/csvtool/interface/cli/workflows/steps/parity/collect_inputs_step.rb +32 -0
  51. data/lib/csvtool/interface/cli/workflows/steps/parity/execute_step.rb +26 -0
  52. data/lib/csvtool/interface/cli/workflows/steps/row_extraction/collect_destination_step.rb +33 -0
  53. data/lib/csvtool/interface/cli/workflows/steps/row_extraction/collect_range_step.rb +35 -0
  54. data/lib/csvtool/interface/cli/workflows/steps/row_extraction/collect_source_step.rb +32 -0
  55. data/lib/csvtool/interface/cli/workflows/steps/row_extraction/execute_step.rb +43 -0
  56. data/lib/csvtool/interface/cli/workflows/steps/row_extraction/read_headers_step.rb +29 -0
  57. data/lib/csvtool/interface/cli/workflows/steps/row_randomization/collect_destination_step.rb +34 -0
  58. data/lib/csvtool/interface/cli/workflows/steps/row_randomization/collect_inputs_step.rb +49 -0
  59. data/lib/csvtool/interface/cli/workflows/steps/row_randomization/execute_step.rb +37 -0
  60. data/lib/csvtool/interface/cli/workflows/steps/workflow_step_pipeline.rb +25 -0
  61. data/lib/csvtool/interface/cli/workflows/support/output_destination_mapper.rb +23 -0
  62. data/lib/csvtool/interface/cli/workflows/support/result_error_handler.rb +22 -0
  63. data/lib/csvtool/version.rb +1 -1
  64. data/test/csvtool/application/use_cases/io_boundary_test.rb +26 -0
  65. data/test/csvtool/application/use_cases/run_cross_csv_dedupe_test.rb +28 -0
  66. data/test/csvtool/application/use_cases/run_csv_parity_test.rb +160 -0
  67. data/test/csvtool/application/use_cases/run_extraction_test.rb +72 -16
  68. data/test/csvtool/application/use_cases/run_row_extraction_test.rb +82 -102
  69. data/test/csvtool/application/use_cases/run_row_randomization_test.rb +96 -86
  70. data/test/csvtool/cli_test.rb +175 -21
  71. data/test/csvtool/cli_unit_test.rb +4 -4
  72. data/test/csvtool/domain/csv_parity_session/parity_options_test.rb +17 -0
  73. data/test/csvtool/domain/csv_parity_session/parity_session_test.rb +18 -0
  74. data/test/csvtool/domain/csv_parity_session/source_pair_test.rb +11 -0
  75. data/test/csvtool/infrastructure/csv/csv_parity_comparator_test.rb +78 -0
  76. data/test/csvtool/infrastructure/output/csv_cross_csv_dedupe_file_writer_test.rb +32 -0
  77. data/test/csvtool/infrastructure/output/csv_file_writer_test.rb +0 -4
  78. data/test/csvtool/infrastructure/output/csv_randomized_row_file_writer_test.rb +32 -0
  79. data/test/csvtool/infrastructure/output/csv_row_file_writer_test.rb +1 -4
  80. data/test/csvtool/interface/cli/errors/presenter_test.rb +2 -0
  81. data/test/csvtool/interface/cli/menu_loop_test.rb +59 -16
  82. data/test/csvtool/interface/cli/prompts/dedupe_key_selector_prompt_test.rb +30 -0
  83. data/test/csvtool/interface/cli/prompts/file_path_prompt_test.rb +9 -0
  84. data/test/csvtool/interface/cli/prompts/headers_present_prompt_test.rb +10 -0
  85. data/test/csvtool/interface/cli/prompts/separator_prompt_test.rb +10 -0
  86. data/test/csvtool/interface/cli/prompts/yes_no_prompt_test.rb +22 -0
  87. data/test/csvtool/interface/cli/workflows/builders/column_session_builder_test.rb +17 -0
  88. data/test/csvtool/interface/cli/workflows/builders/cross_csv_dedupe_session_builder_test.rb +36 -0
  89. data/test/csvtool/interface/cli/workflows/builders/csv_parity_session_builder_test.rb +20 -0
  90. data/test/csvtool/interface/cli/workflows/builders/row_extraction_session_builder_test.rb +21 -0
  91. data/test/csvtool/interface/cli/workflows/builders/row_randomization_session_builder_test.rb +26 -0
  92. data/test/csvtool/interface/cli/workflows/presenters/column_extraction_presenter_test.rb +24 -0
  93. data/test/csvtool/interface/cli/workflows/presenters/cross_csv_dedupe_presenter_test.rb +30 -0
  94. data/test/csvtool/interface/cli/workflows/presenters/csv_parity_presenter_test.rb +43 -0
  95. data/test/csvtool/interface/cli/workflows/presenters/row_extraction_presenter_test.rb +33 -0
  96. data/test/csvtool/interface/cli/workflows/presenters/row_randomization_presenter_test.rb +33 -0
  97. data/test/csvtool/interface/cli/workflows/run_csv_parity_workflow_test.rb +94 -0
  98. data/test/csvtool/interface/cli/workflows/run_extraction_workflow_test.rb +56 -0
  99. data/test/csvtool/interface/cli/workflows/run_row_extraction_workflow_test.rb +83 -0
  100. data/test/csvtool/interface/cli/workflows/run_row_randomization_workflow_test.rb +69 -0
  101. data/test/csvtool/interface/cli/workflows/steps/cross_csv_dedupe/collect_options_step_test.rb +41 -0
  102. data/test/csvtool/interface/cli/workflows/steps/extraction/collect_inputs_step_test.rb +66 -0
  103. data/test/csvtool/interface/cli/workflows/steps/parity/build_session_step_test.rb +41 -0
  104. data/test/csvtool/interface/cli/workflows/steps/parity/collect_inputs_step_test.rb +30 -0
  105. data/test/csvtool/interface/cli/workflows/steps/parity/execute_step_test.rb +40 -0
  106. data/test/csvtool/interface/cli/workflows/steps/row_extraction/collect_source_step_test.rb +39 -0
  107. data/test/csvtool/interface/cli/workflows/steps/row_extraction/execute_step_test.rb +91 -0
  108. data/test/csvtool/interface/cli/workflows/steps/row_extraction/read_headers_step_test.rb +57 -0
  109. data/test/csvtool/interface/cli/workflows/steps/row_randomization/collect_inputs_step_test.rb +37 -0
  110. data/test/csvtool/interface/cli/workflows/steps/workflow_step_pipeline_test.rb +30 -0
  111. data/test/csvtool/interface/cli/workflows/support/output_destination_mapper_test.rb +23 -0
  112. data/test/csvtool/interface/cli/workflows/support/result_error_handler_test.rb +34 -0
  113. data/test/fixtures/parity_duplicates_left.csv +4 -0
  114. data/test/fixtures/parity_duplicates_right.csv +3 -0
  115. data/test/fixtures/parity_people_header_mismatch.csv +4 -0
  116. data/test/fixtures/parity_people_many_reordered.csv +13 -0
  117. data/test/fixtures/parity_people_mismatch.csv +4 -0
  118. data/test/fixtures/parity_people_reordered.csv +4 -0
  119. data/test/fixtures/parity_people_reordered.tsv +4 -0
  120. metadata +90 -1
@@ -4,6 +4,7 @@ require "csv"
4
4
  require "csvtool/infrastructure/csv/header_reader"
5
5
  require "csvtool/infrastructure/csv/cross_csv_deduper"
6
6
  require "csvtool/infrastructure/csv/selector_validator"
7
+ require "csvtool/infrastructure/output/csv_cross_csv_dedupe_file_writer"
7
8
 
8
9
  module Csvtool
9
10
  module Application
@@ -18,11 +19,15 @@ module Csvtool
18
19
  def initialize(
19
20
  header_reader: Infrastructure::CSV::HeaderReader.new,
20
21
  deduper: Infrastructure::CSV::CrossCsvDeduper.new,
21
- selector_validator: Infrastructure::CSV::SelectorValidator.new(header_reader: header_reader)
22
+ selector_validator: Infrastructure::CSV::SelectorValidator.new(header_reader: header_reader),
23
+ csv_cross_csv_dedupe_file_writer: nil
22
24
  )
23
25
  @header_reader = header_reader
24
26
  @deduper = deduper
25
27
  @selector_validator = selector_validator
28
+ @csv_cross_csv_dedupe_file_writer = csv_cross_csv_dedupe_file_writer || Infrastructure::Output::CsvCrossCsvDedupeFileWriter.new(
29
+ deduper: @deduper
30
+ )
26
31
  end
27
32
 
28
33
  def call(session:, on_header: nil, on_row: nil)
@@ -46,26 +51,24 @@ module Csvtool
46
51
  end
47
52
  rescue CSV::MalformedCSVError
48
53
  failure(:could_not_parse_csv)
49
- rescue Errno::EACCES
50
- failure(:cannot_read_file, path: current_read_path || session.source.path)
54
+ rescue Errno::EACCES, Errno::ENOENT => e
55
+ if session.output_destination.file?
56
+ failure(:cannot_write_output_file, path: session.output_destination.path, error_class: e.class)
57
+ else
58
+ failure(:cannot_read_file, path: current_read_path || session.source.path)
59
+ end
51
60
  end
52
61
 
53
62
  private
54
63
 
55
64
  def write_file(session:, source_headers:)
56
- stats = nil
57
- ::CSV.open(
58
- session.output_destination.path,
59
- "w",
60
- write_headers: !source_headers.nil?,
65
+ stats = @csv_cross_csv_dedupe_file_writer.call(
66
+ path: session.output_destination.path,
61
67
  headers: source_headers,
62
- col_sep: session.source.separator
63
- ) do |csv|
64
- stats = @deduper.each_retained(**dedupe_options(session)) { |fields| csv << fields }
65
- end
68
+ col_sep: session.source.separator,
69
+ dedupe_options: dedupe_options(session)
70
+ )
66
71
  success(stats: stats, output_path: session.output_destination.path)
67
- rescue Errno::EACCES, Errno::ENOENT => e
68
- failure(:cannot_write_output_file, path: session.output_destination.path, error_class: e.class)
69
72
  end
70
73
 
71
74
  def dedupe_options(session)
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "csvtool/infrastructure/csv/csv_parity_comparator"
5
+ require "csvtool/infrastructure/csv/header_reader"
6
+
7
+ module Csvtool
8
+ module Application
9
+ module UseCases
10
+ class RunCsvParity
11
+ Result = Struct.new(:ok, :error, :data, keyword_init: true) do
12
+ def ok?
13
+ ok
14
+ end
15
+ end
16
+
17
+ def initialize(
18
+ comparator: Infrastructure::CSV::CsvParityComparator.new,
19
+ header_reader: Infrastructure::CSV::HeaderReader.new
20
+ )
21
+ @comparator = comparator
22
+ @header_reader = header_reader
23
+ end
24
+
25
+ def call(session:)
26
+ left_path = session.source_pair.left_path
27
+ right_path = session.source_pair.right_path
28
+ col_sep = session.options.separator
29
+ headers_present = session.options.headers_present?
30
+
31
+ return failure(:file_not_found, path: left_path) unless File.file?(left_path)
32
+ return failure(:file_not_found, path: right_path) unless File.file?(right_path)
33
+
34
+ if headers_present
35
+ left_headers = @header_reader.call(file_path: left_path, col_sep: col_sep)
36
+ return failure(:no_headers, path: left_path) if left_headers.empty?
37
+
38
+ right_headers = @header_reader.call(file_path: right_path, col_sep: col_sep)
39
+ return failure(:no_headers, path: right_path) if right_headers.empty?
40
+
41
+ return failure(:header_mismatch, left_headers: left_headers, right_headers: right_headers) unless left_headers == right_headers
42
+ end
43
+
44
+ stats = @comparator.call(
45
+ left_path: left_path,
46
+ right_path: right_path,
47
+ col_sep: col_sep,
48
+ headers_present: headers_present
49
+ )
50
+
51
+ success(stats)
52
+ rescue CSV::MalformedCSVError
53
+ failure(:could_not_parse_csv)
54
+ rescue Errno::EACCES => e
55
+ failure(:cannot_read_file, path: e.respond_to?(:path) ? e.path : left_path)
56
+ end
57
+
58
+ private
59
+
60
+ def success(data)
61
+ Result.new(ok: true, error: nil, data: data)
62
+ end
63
+
64
+ def failure(code, data = {})
65
+ Result.new(ok: false, error: code, data: data)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,64 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "csv"
4
- require "csvtool/interface/cli/errors/presenter"
5
- require "csvtool/interface/cli/prompts/file_path_prompt"
6
- require "csvtool/interface/cli/prompts/separator_prompt"
7
- require "csvtool/interface/cli/prompts/column_selector_prompt"
8
- require "csvtool/interface/cli/prompts/skip_blanks_prompt"
9
- require "csvtool/interface/cli/prompts/confirm_prompt"
10
- require "csvtool/interface/cli/prompts/output_destination_prompt"
11
4
  require "csvtool/infrastructure/csv/header_reader"
12
5
  require "csvtool/infrastructure/csv/value_streamer"
13
- require "csvtool/services/preview_builder"
14
- require "csvtool/infrastructure/output/console_writer"
15
6
  require "csvtool/infrastructure/output/csv_file_writer"
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/column_session"
23
- require "csvtool/domain/shared/output_destination"
7
+ require "csvtool/services/preview_builder"
24
8
 
25
9
  module Csvtool
26
10
  module Application
27
11
  module UseCases
28
12
  class RunExtraction
29
- def initialize(stdin:, stdout:)
30
- @stdin = stdin
31
- @stdout = stdout
32
- @errors = Interface::CLI::Errors::Presenter.new(stdout: stdout)
33
- @header_reader = Infrastructure::CSV::HeaderReader.new
34
- @value_streamer = Infrastructure::CSV::ValueStreamer.new
35
- @preview_builder = Services::PreviewBuilder.new(value_streamer: @value_streamer)
13
+ Result = Struct.new(:ok, :error, :data, keyword_init: true) do
14
+ def ok?
15
+ ok
16
+ end
36
17
  end
37
18
 
38
- def call
39
- file_path = Interface::CLI::Prompts::FilePathPrompt.new(stdin: @stdin, stdout: @stdout).call
40
- return @errors.file_not_found(file_path) unless File.file?(file_path)
41
-
42
- col_sep = Interface::CLI::Prompts::SeparatorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call
43
- return if col_sep.nil?
44
- separator = Domain::ColumnSession::Separator.new(col_sep)
19
+ def initialize(
20
+ header_reader: Infrastructure::CSV::HeaderReader.new,
21
+ value_streamer: Infrastructure::CSV::ValueStreamer.new,
22
+ preview_builder: nil,
23
+ csv_file_writer: nil
24
+ )
25
+ @header_reader = header_reader
26
+ @value_streamer = value_streamer
27
+ @preview_builder = preview_builder || Services::PreviewBuilder.new(value_streamer: value_streamer)
28
+ @csv_file_writer = csv_file_writer || Infrastructure::Output::CsvFileWriter.new(value_streamer: @value_streamer)
29
+ end
45
30
 
46
- source = Domain::ColumnSession::CsvSource.new(path: file_path, separator: separator)
47
- headers = @header_reader.call(file_path: source.path, col_sep: source.separator.value)
48
- return @errors.no_headers if headers.empty?
31
+ def read_headers(file_path:, col_sep:)
32
+ return failure(:file_not_found, path: file_path) unless File.file?(file_path)
49
33
 
50
- column_name = Interface::CLI::Prompts::ColumnSelectorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call(headers)
51
- return if column_name.nil?
52
- column_selection = Domain::ColumnSession::ColumnSelection.new(name: column_name)
34
+ headers = @header_reader.call(file_path: file_path, col_sep: col_sep)
35
+ return failure(:no_headers) if headers.empty?
53
36
 
54
- skip_blanks = Interface::CLI::Prompts::SkipBlanksPrompt.new(stdin: @stdin, stdout: @stdout).call
55
- options = Domain::ColumnSession::ExtractionOptions.new(skip_blanks: skip_blanks, preview_limit: 10)
56
- session = Domain::ColumnSession::ColumnSession.start(
57
- source: source,
58
- column_selection: column_selection,
59
- options: options
60
- )
37
+ success(headers: headers)
38
+ rescue CSV::MalformedCSVError
39
+ failure(:could_not_parse_csv)
40
+ rescue Errno::EACCES
41
+ failure(:cannot_read_file, path: file_path)
42
+ end
61
43
 
44
+ def preview(session:)
62
45
  preview_values = @preview_builder.call(
63
46
  file_path: session.source.path,
64
47
  column_name: session.column_selection.name,
@@ -66,58 +49,50 @@ module Csvtool
66
49
  skip_blanks: session.options.skip_blanks?,
67
50
  limit: session.options.preview_limit
68
51
  )
69
- preview = Domain::ColumnSession::Preview.new(
70
- values: preview_values.map { |value| Domain::ColumnSession::ExtractionValue.new(value) }
71
- )
72
- session = session.with_preview(preview)
73
-
74
- confirmed = Interface::CLI::Prompts::ConfirmPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call(session.preview.to_strings)
75
- return unless confirmed
76
- session = session.confirm!
77
-
78
- output_destination = Interface::CLI::Prompts::OutputDestinationPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call
79
- return if output_destination.nil?
80
- domain_destination =
81
- if output_destination[:mode] == :file
82
- Domain::Shared::OutputDestination.file(path: output_destination[:path])
83
- else
84
- Domain::Shared::OutputDestination.console
85
- end
86
- session = session.with_output_destination(domain_destination)
87
-
88
- write_output(
89
- session.output_destination,
90
- file_path: session.source.path,
91
- column_name: session.column_selection.name,
92
- col_sep: session.source.separator.value,
93
- skip_blanks: session.options.skip_blanks?
94
- )
52
+ success(preview_values: preview_values)
95
53
  rescue CSV::MalformedCSVError
96
- @errors.could_not_parse_csv
54
+ failure(:could_not_parse_csv)
97
55
  rescue Errno::EACCES
98
- @errors.cannot_read_file(file_path)
56
+ failure(:cannot_read_file, path: session.source.path)
99
57
  end
100
58
 
101
- private
102
-
103
- def writer_for(output_destination)
104
- if output_destination.file?
105
- Infrastructure::Output::CsvFileWriter.new(stdout: @stdout, errors: @errors, value_streamer: @value_streamer)
59
+ def extract(session:, on_value: nil)
60
+ if session.output_destination.file?
61
+ @csv_file_writer.call(
62
+ output_path: session.output_destination.path,
63
+ file_path: session.source.path,
64
+ column_name: session.column_selection.name,
65
+ col_sep: session.source.separator.value,
66
+ skip_blanks: session.options.skip_blanks?
67
+ )
68
+ success(output_path: session.output_destination.path)
106
69
  else
107
- Infrastructure::Output::ConsoleWriter.new(stdout: @stdout, value_streamer: @value_streamer)
70
+ @value_streamer.each(
71
+ file_path: session.source.path,
72
+ column_name: session.column_selection.name,
73
+ col_sep: session.source.separator.value,
74
+ skip_blanks: session.options.skip_blanks?
75
+ ) { |value| on_value.call(value) if on_value }
76
+ success({})
108
77
  end
78
+ rescue CSV::MalformedCSVError
79
+ failure(:could_not_parse_csv)
80
+ rescue Errno::EACCES, Errno::ENOENT => e
81
+ if session.output_destination.file?
82
+ failure(:cannot_write_output_file, path: session.output_destination.path, error_class: e.class)
83
+ else
84
+ failure(:cannot_read_file, path: session.source.path)
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def success(data)
91
+ Result.new(ok: true, error: nil, data: data)
109
92
  end
110
93
 
111
- def write_output(output_destination, file_path:, column_name:, col_sep:, skip_blanks:)
112
- writer = writer_for(output_destination)
113
- args = {
114
- file_path: file_path,
115
- column_name: column_name,
116
- col_sep: col_sep,
117
- skip_blanks: skip_blanks
118
- }
119
- args[:output_path] = output_destination.path if output_destination.file?
120
- writer.call(**args)
94
+ def failure(code, data = {})
95
+ Result.new(ok: false, error: code, data: data)
121
96
  end
122
97
  end
123
98
  end
@@ -1,74 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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
4
  require "csvtool/infrastructure/csv/header_reader"
9
5
  require "csvtool/infrastructure/csv/row_streamer"
10
- require "csvtool/infrastructure/output/csv_row_console_writer"
11
6
  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_session"
15
- require "csvtool/domain/shared/output_destination"
16
7
 
17
8
  module Csvtool
18
9
  module Application
19
10
  module UseCases
20
11
  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
12
+ Result = Struct.new(:ok, :error, :data, keyword_init: true) do
13
+ def ok?
14
+ ok
15
+ end
27
16
  end
28
17
 
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
18
+ def initialize(
19
+ header_reader: Infrastructure::CSV::HeaderReader.new,
20
+ row_streamer: Infrastructure::CSV::RowStreamer.new,
21
+ csv_row_file_writer: nil
22
+ )
23
+ @header_reader = header_reader
24
+ @row_streamer = row_streamer
25
+ @csv_row_file_writer = csv_row_file_writer || Infrastructure::Output::CsvRowFileWriter.new(row_streamer: @row_streamer)
26
+ end
41
27
 
42
- headers = @header_reader.call(file_path: source.path, col_sep: source.separator)
43
- return @errors.no_headers if headers.empty?
28
+ def read_headers(file_path:, col_sep:)
29
+ return failure(:file_not_found, path: file_path) unless File.file?(file_path)
44
30
 
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)
31
+ headers = @header_reader.call(file_path: file_path, col_sep: col_sep)
32
+ return failure(:no_headers) if headers.empty?
50
33
 
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::Shared::OutputDestination.file(path: output_destination[:path])
60
- else
61
- Domain::Shared::OutputDestination.console
62
- end
63
- session = session.with_output_destination(destination)
34
+ success(headers: headers)
35
+ rescue CSV::MalformedCSVError
36
+ failure(:could_not_parse_csv)
37
+ rescue Errno::EACCES
38
+ failure(:cannot_read_file, path: file_path)
39
+ end
64
40
 
65
- stats =
41
+ def extract(session:, headers:, on_row: nil)
66
42
  if session.output_destination.file?
67
- Infrastructure::Output::CsvRowFileWriter.new(
68
- stdout: @stdout,
69
- errors: @errors,
70
- row_streamer: @row_streamer
71
- ).call(
43
+ stats = @csv_row_file_writer.call(
72
44
  output_path: session.output_destination.path,
73
45
  file_path: session.source.path,
74
46
  col_sep: session.source.separator,
@@ -76,35 +48,35 @@ module Csvtool
76
48
  start_row: session.row_range.start_row,
77
49
  end_row: session.row_range.end_row
78
50
  )
51
+ success(stats.merge(output_path: session.output_destination.path))
79
52
  else
80
- Infrastructure::Output::CsvRowConsoleWriter.new(stdout: @stdout, row_streamer: @row_streamer).call(
53
+ stats = @row_streamer.each_in_range(
81
54
  file_path: session.source.path,
82
55
  col_sep: session.source.separator,
83
- headers: headers,
84
56
  start_row: session.row_range.start_row,
85
57
  end_row: session.row_range.end_row
86
- )
58
+ ) { |fields| on_row.call(fields) if on_row }
59
+ success(stats)
87
60
  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
61
  rescue CSV::MalformedCSVError
102
- @errors.could_not_parse_csv
103
- rescue Errno::EACCES
104
- @errors.cannot_read_file(file_path)
62
+ failure(:could_not_parse_csv)
63
+ rescue Errno::EACCES, Errno::ENOENT => e
64
+ if session.output_destination.file?
65
+ failure(:cannot_write_output_file, path: session.output_destination.path, error_class: e.class)
66
+ else
67
+ failure(:cannot_read_file, path: session.source.path)
68
+ end
105
69
  end
106
-
70
+
107
71
  private
72
+
73
+ def success(data)
74
+ Result.new(ok: true, error: nil, data: data)
75
+ end
76
+
77
+ def failure(code, data = {})
78
+ Result.new(ok: false, error: code, data: data)
79
+ end
108
80
  end
109
81
  end
110
82
  end
@@ -1,103 +1,86 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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
4
  require "csvtool/infrastructure/csv/header_reader"
11
5
  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_session"
15
- require "csvtool/domain/shared/output_destination"
6
+ require "csvtool/infrastructure/output/csv_randomized_row_file_writer"
16
7
 
17
8
  module Csvtool
18
9
  module Application
19
10
  module UseCases
20
11
  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
12
+ Result = Struct.new(:ok, :error, :data, keyword_init: true) do
13
+ def ok?
14
+ ok
15
+ end
27
16
  end
28
17
 
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
18
+ def initialize(
19
+ header_reader: Infrastructure::CSV::HeaderReader.new,
20
+ row_randomizer: Infrastructure::CSV::RowRandomizer.new,
21
+ csv_randomized_row_file_writer: nil
22
+ )
23
+ @header_reader = header_reader
24
+ @row_randomizer = row_randomizer
25
+ @csv_randomized_row_file_writer = csv_randomized_row_file_writer || Infrastructure::Output::CsvRandomizedRowFileWriter.new(
26
+ row_randomizer: @row_randomizer
41
27
  )
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?
28
+ end
44
29
 
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)
30
+ def read_headers(file_path:, col_sep:, headers_present:)
31
+ return failure(:file_not_found, path: file_path) unless File.file?(file_path)
49
32
 
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::Shared::OutputDestination.file(path: output_destination[:path])
59
- else
60
- Domain::Shared::OutputDestination.console
61
- end
62
- session = session.with_output_destination(destination)
33
+ headers = nil
34
+ if headers_present
35
+ headers = @header_reader.call(file_path: file_path, col_sep: col_sep)
36
+ return failure(:no_headers) if headers.empty?
37
+ end
63
38
 
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
- )
39
+ success(headers: headers)
40
+ rescue CSV::MalformedCSVError
41
+ failure(:could_not_parse_csv)
42
+ rescue Errno::EACCES
43
+ failure(:cannot_read_file, path: file_path)
44
+ end
70
45
 
46
+ def randomize(session:, headers:, on_row: nil)
71
47
  if session.output_destination.file?
72
- write_output_file(session.output_destination.path, headers, randomized_rows, col_sep: session.source.separator)
48
+ @csv_randomized_row_file_writer.call(
49
+ path: session.output_destination.path,
50
+ headers: headers,
51
+ file_path: session.source.path,
52
+ col_sep: session.source.separator,
53
+ headers_present: session.source.headers_present?,
54
+ seed: session.options.seed
55
+ )
56
+ success(output_path: session.output_destination.path)
73
57
  else
74
- print_to_console(headers, randomized_rows, col_sep: session.source.separator)
58
+ @row_randomizer.each(
59
+ file_path: session.source.path,
60
+ col_sep: session.source.separator,
61
+ headers: session.source.headers_present?,
62
+ seed: session.options.seed
63
+ ) { |fields| on_row.call(fields) if on_row }
64
+ success({})
75
65
  end
76
66
  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)
67
+ failure(:could_not_parse_csv)
68
+ rescue Errno::EACCES, Errno::ENOENT => e
69
+ if session.output_destination.file?
70
+ failure(:cannot_write_output_file, path: session.output_destination.path, error_class: e.class)
71
+ else
72
+ failure(:cannot_read_file, path: session.source.path)
73
+ end
84
74
  end
85
75
 
86
76
  private
87
77
 
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 }
78
+ def success(data)
79
+ Result.new(ok: true, error: nil, data: data)
92
80
  end
93
81
 
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)
82
+ def failure(code, data = {})
83
+ Result.new(ok: false, error: code, data: data)
101
84
  end
102
85
  end
103
86
  end