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
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Csvtool
4
+ module Interface
5
+ module CLI
6
+ module Workflows
7
+ module Presenters
8
+ class CsvParityPresenter
9
+ def initialize(stdout:)
10
+ @stdout = stdout
11
+ end
12
+
13
+ def print_summary(data)
14
+ @stdout.puts(data[:match] ? "MATCH" : "MISMATCH")
15
+ @stdout.puts "Summary: left_rows=#{data[:left_rows]} right_rows=#{data[:right_rows]} " \
16
+ "left_only=#{data[:left_only_count]} right_only=#{data[:right_only_count]}"
17
+ return if data[:match]
18
+
19
+ print_examples("Left-only examples", data[:left_only_examples])
20
+ print_examples("Right-only examples", data[:right_only_examples])
21
+ end
22
+
23
+ private
24
+
25
+ def print_examples(label, examples)
26
+ return if examples.nil? || examples.empty?
27
+
28
+ @stdout.puts "#{label}:"
29
+ examples.each do |example|
30
+ @stdout.puts " #{example[:row]} (count +#{example[:count_delta]})"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ module Csvtool
6
+ module Interface
7
+ module CLI
8
+ module Workflows
9
+ module Presenters
10
+ class RowExtractionPresenter
11
+ def initialize(stdout:, headers:, col_sep:)
12
+ @stdout = stdout
13
+ @headers = headers
14
+ @col_sep = col_sep
15
+ @printed_header = false
16
+ end
17
+
18
+ def print_row(fields)
19
+ unless @printed_header
20
+ @stdout.puts ::CSV.generate_line(@headers, row_sep: "", col_sep: @col_sep).chomp
21
+ @printed_header = true
22
+ end
23
+ @stdout.puts ::CSV.generate_line(fields, row_sep: "", col_sep: @col_sep).chomp
24
+ end
25
+
26
+ def print_file_written(path)
27
+ @stdout.puts "Wrote output to #{path}"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ module Csvtool
6
+ module Interface
7
+ module CLI
8
+ module Workflows
9
+ module Presenters
10
+ class RowRandomizationPresenter
11
+ def initialize(stdout:, headers:, col_sep:)
12
+ @stdout = stdout
13
+ @headers = headers
14
+ @col_sep = col_sep
15
+ end
16
+
17
+ def print_console_start
18
+ @stdout.puts
19
+ @stdout.puts ::CSV.generate_line(@headers, row_sep: "", col_sep: @col_sep).chomp if @headers
20
+ end
21
+
22
+ def print_row(fields)
23
+ @stdout.puts ::CSV.generate_line(fields, row_sep: "", col_sep: @col_sep).chomp
24
+ end
25
+
26
+ def print_file_written(path)
27
+ @stdout.puts "Wrote output to #{path}"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,18 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "csv"
4
3
  require "csvtool/application/use_cases/run_cross_csv_dedupe"
5
4
  require "csvtool/interface/cli/errors/presenter"
6
5
  require "csvtool/interface/cli/prompts/file_path_prompt"
7
6
  require "csvtool/interface/cli/prompts/separator_prompt"
8
7
  require "csvtool/interface/cli/prompts/output_destination_prompt"
8
+ require "csvtool/interface/cli/prompts/headers_present_prompt"
9
+ require "csvtool/interface/cli/prompts/yes_no_prompt"
10
+ require "csvtool/interface/cli/prompts/dedupe_key_selector_prompt"
11
+ require "csvtool/interface/cli/workflows/builders/cross_csv_dedupe_session_builder"
12
+ require "csvtool/interface/cli/workflows/presenters/cross_csv_dedupe_presenter"
13
+ require "csvtool/interface/cli/workflows/support/output_destination_mapper"
14
+ require "csvtool/interface/cli/workflows/support/result_error_handler"
15
+ require "csvtool/interface/cli/workflows/steps/workflow_step_pipeline"
16
+ require "csvtool/interface/cli/workflows/steps/cross_csv_dedupe/collect_profiles_step"
17
+ require "csvtool/interface/cli/workflows/steps/cross_csv_dedupe/collect_options_step"
18
+ require "csvtool/interface/cli/workflows/steps/cross_csv_dedupe/execute_step"
9
19
  require "csvtool/domain/cross_csv_dedupe_session/csv_profile"
10
20
  require "csvtool/domain/cross_csv_dedupe_session/column_selector"
11
- require "csvtool/domain/cross_csv_dedupe_session/key_mapping"
12
- require "csvtool/domain/cross_csv_dedupe_session/match_options"
13
- require "csvtool/domain/cross_csv_dedupe_session/cross_csv_dedupe_session"
14
- require "csvtool/domain/shared/output_destination"
15
-
16
21
  module Csvtool
17
22
  module Interface
18
23
  module CLI
@@ -23,89 +28,40 @@ module Csvtool
23
28
  @stdout = stdout
24
29
  @use_case = use_case
25
30
  @errors = Interface::CLI::Errors::Presenter.new(stdout: stdout)
31
+ @session_builder = Builders::CrossCsvDedupeSessionBuilder.new
32
+ @output_destination_mapper = Support::OutputDestinationMapper.new
33
+ @result_error_handler = Support::ResultErrorHandler.new(errors: @errors)
26
34
  end
27
35
 
28
36
  def call
29
- source_path = Interface::CLI::Prompts::FilePathPrompt.new(stdin: @stdin, stdout: @stdout).call
30
- return @errors.file_not_found(source_path) unless File.file?(source_path)
31
-
32
- @stdout.puts "Source CSV separator:"
33
- source_col_sep = Interface::CLI::Prompts::SeparatorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call
34
- return if source_col_sep.nil?
35
- @stdout.print "Source headers present? [Y/n]: "
36
- source_headers_present = !%w[n no].include?(@stdin.gets&.strip.to_s.downcase)
37
- source = Domain::CrossCsvDedupeSession::CsvProfile.new(
38
- path: source_path,
39
- separator: source_col_sep,
40
- headers_present: source_headers_present
41
- )
42
-
43
- @stdout.print "Reference CSV file path: "
44
- reference_path = @stdin.gets&.strip.to_s
45
- return @errors.file_not_found(reference_path) unless File.file?(reference_path)
46
-
47
- @stdout.puts "Reference CSV separator:"
48
- reference_col_sep = Interface::CLI::Prompts::SeparatorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors).call
49
- return if reference_col_sep.nil?
50
- @stdout.print "Reference headers present? [Y/n]: "
51
- reference_headers_present = !%w[n no].include?(@stdin.gets&.strip.to_s.downcase)
52
- reference = Domain::CrossCsvDedupeSession::CsvProfile.new(
53
- path: reference_path,
54
- separator: reference_col_sep,
55
- headers_present: reference_headers_present
56
- )
57
-
58
- source_selector = prompt_selector("Source", source.headers_present?)
59
- return @errors.column_not_found if source_selector.nil?
60
- reference_selector = prompt_selector("Reference", reference.headers_present?)
61
- return @errors.column_not_found if reference_selector.nil?
62
-
63
- @stdout.print "Trim whitespace before matching? [Y/n]: "
64
- trim_whitespace = read_yes_no(default: true)
65
- @stdout.print "Case-insensitive matching? [y/N]: "
66
- case_insensitive = read_yes_no(default: false)
67
-
68
- key_mapping = Domain::CrossCsvDedupeSession::KeyMapping.new(
69
- source_selector: source_selector,
70
- reference_selector: reference_selector
71
- )
72
- match_options = Domain::CrossCsvDedupeSession::MatchOptions.new(
73
- trim_whitespace: trim_whitespace,
74
- case_insensitive: case_insensitive
75
- )
76
- session = Domain::CrossCsvDedupeSession::CrossCsvDedupeSession.start(
77
- source: source,
78
- reference: reference,
79
- key_mapping: key_mapping,
80
- match_options: match_options
81
- )
82
-
83
- output_destination = Interface::CLI::Prompts::OutputDestinationPrompt.new(
84
- stdin: @stdin,
85
- stdout: @stdout,
86
- errors: @errors
87
- ).call
88
- return if output_destination.nil?
89
- session = session.with_output_destination(
90
- if output_destination[:mode] == :file
91
- Domain::Shared::OutputDestination.file(path: output_destination[:path])
92
- else
93
- Domain::Shared::OutputDestination.console
94
- end
95
- )
96
-
97
- result = @use_case.call(
98
- session: session,
99
- on_header: ->(headers) { print_header(headers, col_sep: session.source.separator) },
100
- on_row: ->(fields) { print_row(fields, col_sep: session.source.separator) }
101
- )
102
- return handle_error(result) unless result.ok?
103
-
104
- @stdout.puts "Wrote output to #{result.data[:output_path]}" if session.output_destination.file?
105
- stats = result.data[:stats]
106
- @stdout.puts "Summary: source_rows=#{stats[:source_rows]} removed_rows=#{stats[:removed_rows]} kept_rows=#{stats[:kept_rows_count]}"
107
- @stdout.puts "No rows removed; no matching keys found." if stats[:removed_rows].zero?
108
- @stdout.puts "All source rows were removed by dedupe." if stats[:source_rows].positive? && stats[:kept_rows_count].zero?
37
+ context = {
38
+ use_case: @use_case,
39
+ session_builder: @session_builder,
40
+ output_destination_mapper: @output_destination_mapper,
41
+ presenter_factory: ->(col_sep:) { Presenters::CrossCsvDedupePresenter.new(stdout: @stdout, col_sep: col_sep) },
42
+ handle_error: method(:handle_error)
43
+ }
44
+
45
+ pipeline = Steps::WorkflowStepPipeline.new(steps: [
46
+ Steps::CrossCsvDedupe::CollectProfilesStep.new(
47
+ file_path_prompt: Interface::CLI::Prompts::FilePathPrompt.new(stdin: @stdin, stdout: @stdout),
48
+ separator_prompt: Interface::CLI::Prompts::SeparatorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors),
49
+ headers_present_prompt: Interface::CLI::Prompts::HeadersPresentPrompt.new(stdin: @stdin, stdout: @stdout),
50
+ errors: @errors
51
+ ),
52
+ Steps::CrossCsvDedupe::CollectOptionsStep.new(
53
+ selector_prompt: Interface::CLI::Prompts::DedupeKeySelectorPrompt.new(stdin: @stdin, stdout: @stdout),
54
+ yes_no_prompt: Interface::CLI::Prompts::YesNoPrompt.new(stdin: @stdin, stdout: @stdout),
55
+ output_destination_prompt: Interface::CLI::Prompts::OutputDestinationPrompt.new(
56
+ stdin: @stdin,
57
+ stdout: @stdout,
58
+ errors: @errors
59
+ ),
60
+ errors: @errors
61
+ ),
62
+ Steps::CrossCsvDedupe::ExecuteStep.new
63
+ ])
64
+ pipeline.call(context)
109
65
  rescue ArgumentError => e
110
66
  return @errors.empty_output_path if e.message == "file output path cannot be empty"
111
67
 
@@ -114,48 +70,15 @@ module Csvtool
114
70
 
115
71
  private
116
72
 
117
- def prompt_selector(label, headers_present)
118
- if headers_present
119
- @stdout.print "#{label} key column name: "
120
- else
121
- @stdout.print "#{label} key column index (1-based): "
122
- end
123
- input = @stdin.gets&.strip.to_s
124
- Domain::CrossCsvDedupeSession::ColumnSelector.from_input(headers_present: headers_present, input: input)
125
- rescue ArgumentError
126
- nil
127
- end
128
-
129
- def print_header(headers, col_sep:)
130
- @stdout.puts
131
- @stdout.puts ::CSV.generate_line(headers, row_sep: "", col_sep: col_sep).chomp
132
- end
133
-
134
- def print_row(fields, col_sep:)
135
- @stdout.puts ::CSV.generate_line(fields, row_sep: "", col_sep: col_sep).chomp
136
- end
137
-
138
73
  def handle_error(result)
139
- case result.error
140
- when :column_not_found
141
- @errors.column_not_found
142
- when :could_not_parse_csv
143
- @errors.could_not_parse_csv
144
- when :cannot_read_file
145
- @errors.cannot_read_file(result.data[:path])
146
- when :cannot_write_output_file
147
- @errors.cannot_write_output_file(result.data[:path], result.data[:error_class])
148
- end
74
+ @result_error_handler.call(result, {
75
+ column_not_found: ->(_r, errors) { errors.column_not_found },
76
+ could_not_parse_csv: ->(_r, errors) { errors.could_not_parse_csv },
77
+ cannot_read_file: ->(r, errors) { errors.cannot_read_file(r.data[:path]) },
78
+ cannot_write_output_file: ->(r, errors) { errors.cannot_write_output_file(r.data[:path], r.data[:error_class]) }
79
+ })
149
80
  end
150
81
 
151
- def read_yes_no(default:)
152
- answer = @stdin.gets&.strip.to_s.downcase
153
- return default if answer.empty?
154
- return true if %w[y yes].include?(answer)
155
- return false if %w[n no].include?(answer)
156
-
157
- default
158
- end
159
82
  end
160
83
  end
161
84
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csvtool/application/use_cases/run_csv_parity"
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/workflows/builders/csv_parity_session_builder"
9
+ require "csvtool/interface/cli/workflows/presenters/csv_parity_presenter"
10
+ require "csvtool/interface/cli/workflows/support/result_error_handler"
11
+ require "csvtool/interface/cli/workflows/steps/workflow_step_pipeline"
12
+ require "csvtool/interface/cli/workflows/steps/parity/collect_inputs_step"
13
+ require "csvtool/interface/cli/workflows/steps/parity/build_session_step"
14
+ require "csvtool/interface/cli/workflows/steps/parity/execute_step"
15
+
16
+ module Csvtool
17
+ module Interface
18
+ module CLI
19
+ module Workflows
20
+ class RunCsvParityWorkflow
21
+ def initialize(stdin:, stdout:, use_case: Application::UseCases::RunCsvParity.new)
22
+ @stdin = stdin
23
+ @stdout = stdout
24
+ @use_case = use_case
25
+ @errors = Interface::CLI::Errors::Presenter.new(stdout: stdout)
26
+ @session_builder = Builders::CsvParitySessionBuilder.new
27
+ @presenter = Presenters::CsvParityPresenter.new(stdout: stdout)
28
+ @result_error_handler = Support::ResultErrorHandler.new(errors: @errors)
29
+ end
30
+
31
+ def call
32
+ context = {
33
+ use_case: @use_case,
34
+ session_builder: @session_builder,
35
+ presenter: @presenter,
36
+ handle_error: method(:handle_error)
37
+ }
38
+ pipeline = Steps::WorkflowStepPipeline.new(steps: [
39
+ Steps::Parity::CollectInputsStep.new(
40
+ file_path_prompt: Interface::CLI::Prompts::FilePathPrompt.new(stdin: @stdin, stdout: @stdout),
41
+ separator_prompt: Interface::CLI::Prompts::SeparatorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors),
42
+ headers_present_prompt: Interface::CLI::Prompts::HeadersPresentPrompt.new(stdin: @stdin, stdout: @stdout)
43
+ ),
44
+ Steps::Parity::BuildSessionStep.new,
45
+ Steps::Parity::ExecuteStep.new
46
+ ])
47
+ pipeline.call(context)
48
+ nil
49
+ end
50
+
51
+ private
52
+
53
+ def handle_error(result)
54
+ @result_error_handler.call(result, {
55
+ file_not_found: ->(r, errors) { errors.file_not_found(r.data[:path]) },
56
+ could_not_parse_csv: ->(_r, errors) { errors.could_not_parse_csv },
57
+ cannot_read_file: ->(r, errors) { errors.cannot_read_file(r.data[:path]) },
58
+ no_headers: ->(_r, errors) { errors.no_headers },
59
+ header_mismatch: ->(_r, errors) { errors.header_mismatch }
60
+ })
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csvtool/application/use_cases/run_extraction"
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
+ require "csvtool/interface/cli/workflows/builders/column_session_builder"
12
+ require "csvtool/interface/cli/workflows/presenters/column_extraction_presenter"
13
+ require "csvtool/interface/cli/workflows/support/output_destination_mapper"
14
+ require "csvtool/interface/cli/workflows/support/result_error_handler"
15
+ require "csvtool/interface/cli/workflows/steps/workflow_step_pipeline"
16
+ require "csvtool/interface/cli/workflows/steps/extraction/collect_inputs_step"
17
+ require "csvtool/interface/cli/workflows/steps/extraction/build_preview_step"
18
+ require "csvtool/interface/cli/workflows/steps/extraction/collect_destination_step"
19
+ require "csvtool/interface/cli/workflows/steps/extraction/execute_step"
20
+
21
+ module Csvtool
22
+ module Interface
23
+ module CLI
24
+ module Workflows
25
+ class RunExtractionWorkflow
26
+ def initialize(stdin:, stdout:, use_case: Application::UseCases::RunExtraction.new)
27
+ @stdin = stdin
28
+ @stdout = stdout
29
+ @use_case = use_case
30
+ @errors = Interface::CLI::Errors::Presenter.new(stdout: stdout)
31
+ @session_builder = Builders::ColumnSessionBuilder.new
32
+ @presenter = Presenters::ColumnExtractionPresenter.new(stdout: @stdout)
33
+ @output_destination_mapper = Support::OutputDestinationMapper.new
34
+ @result_error_handler = Support::ResultErrorHandler.new(errors: @errors)
35
+ end
36
+
37
+ def call
38
+ context = {
39
+ use_case: @use_case,
40
+ session_builder: @session_builder,
41
+ output_destination_mapper: @output_destination_mapper,
42
+ presenter: @presenter,
43
+ handle_error: method(:handle_error)
44
+ }
45
+
46
+ pipeline = Steps::WorkflowStepPipeline.new(steps: [
47
+ Steps::Extraction::CollectInputsStep.new(
48
+ file_path_prompt: Interface::CLI::Prompts::FilePathPrompt.new(stdin: @stdin, stdout: @stdout),
49
+ separator_prompt: Interface::CLI::Prompts::SeparatorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors),
50
+ column_selector_prompt: Interface::CLI::Prompts::ColumnSelectorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors),
51
+ skip_blanks_prompt: Interface::CLI::Prompts::SkipBlanksPrompt.new(stdin: @stdin, stdout: @stdout)
52
+ ),
53
+ Steps::Extraction::BuildPreviewStep.new(
54
+ confirm_prompt: Interface::CLI::Prompts::ConfirmPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors)
55
+ ),
56
+ Steps::Extraction::CollectDestinationStep.new(
57
+ output_destination_prompt: Interface::CLI::Prompts::OutputDestinationPrompt.new(
58
+ stdin: @stdin,
59
+ stdout: @stdout,
60
+ errors: @errors
61
+ )
62
+ ),
63
+ Steps::Extraction::ExecuteStep.new
64
+ ])
65
+ pipeline.call(context)
66
+ rescue ArgumentError => e
67
+ return @errors.empty_output_path if e.message == "file output path cannot be empty"
68
+
69
+ raise e
70
+ end
71
+
72
+ private
73
+
74
+ def handle_error(result)
75
+ @result_error_handler.call(result, {
76
+ file_not_found: ->(r, errors) { errors.file_not_found(r.data[:path]) },
77
+ no_headers: ->(_r, errors) { errors.no_headers },
78
+ column_not_found: ->(_r, errors) { errors.column_not_found },
79
+ could_not_parse_csv: ->(_r, errors) { errors.could_not_parse_csv },
80
+ cannot_read_file: ->(r, errors) { errors.cannot_read_file(r.data[:path]) },
81
+ cannot_write_output_file: ->(r, errors) { errors.cannot_write_output_file(r.data[:path], r.data[:error_class]) }
82
+ })
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csvtool/application/use_cases/run_row_extraction"
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/interface/cli/workflows/builders/row_extraction_session_builder"
9
+ require "csvtool/interface/cli/workflows/presenters/row_extraction_presenter"
10
+ require "csvtool/interface/cli/workflows/support/output_destination_mapper"
11
+ require "csvtool/interface/cli/workflows/support/result_error_handler"
12
+ require "csvtool/interface/cli/workflows/steps/workflow_step_pipeline"
13
+ require "csvtool/interface/cli/workflows/steps/row_extraction/collect_source_step"
14
+ require "csvtool/interface/cli/workflows/steps/row_extraction/read_headers_step"
15
+ require "csvtool/interface/cli/workflows/steps/row_extraction/collect_range_step"
16
+ require "csvtool/interface/cli/workflows/steps/row_extraction/collect_destination_step"
17
+ require "csvtool/interface/cli/workflows/steps/row_extraction/execute_step"
18
+ require "csvtool/domain/row_session/row_range"
19
+ module Csvtool
20
+ module Interface
21
+ module CLI
22
+ module Workflows
23
+ class RunRowExtractionWorkflow
24
+ def initialize(stdin:, stdout:, use_case: Application::UseCases::RunRowExtraction.new)
25
+ @stdin = stdin
26
+ @stdout = stdout
27
+ @use_case = use_case
28
+ @errors = Interface::CLI::Errors::Presenter.new(stdout: stdout)
29
+ @session_builder = Builders::RowExtractionSessionBuilder.new
30
+ @output_destination_mapper = Support::OutputDestinationMapper.new
31
+ @result_error_handler = Support::ResultErrorHandler.new(errors: @errors)
32
+ end
33
+
34
+ def call
35
+ context = {
36
+ use_case: @use_case,
37
+ session_builder: @session_builder,
38
+ output_destination_mapper: @output_destination_mapper,
39
+ handle_error: method(:handle_error)
40
+ }
41
+
42
+ pipeline = Steps::WorkflowStepPipeline.new(steps: [
43
+ Steps::RowExtraction::CollectSourceStep.new(
44
+ file_path_prompt: Interface::CLI::Prompts::FilePathPrompt.new(stdin: @stdin, stdout: @stdout),
45
+ separator_prompt: Interface::CLI::Prompts::SeparatorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors)
46
+ ),
47
+ Steps::RowExtraction::ReadHeadersStep.new,
48
+ Steps::RowExtraction::CollectRangeStep.new(stdin: @stdin, stdout: @stdout),
49
+ Steps::RowExtraction::CollectDestinationStep.new(
50
+ output_destination_prompt: Interface::CLI::Prompts::OutputDestinationPrompt.new(
51
+ stdin: @stdin,
52
+ stdout: @stdout,
53
+ errors: @errors
54
+ )
55
+ ),
56
+ Steps::RowExtraction::ExecuteStep.new(stdout: @stdout, errors: @errors)
57
+ ])
58
+ pipeline.call(context)
59
+ rescue Domain::RowSession::InvalidStartRowError
60
+ @errors.invalid_start_row
61
+ rescue Domain::RowSession::InvalidEndRowError
62
+ @errors.invalid_end_row
63
+ rescue Domain::RowSession::InvalidRowRangeOrderError
64
+ @errors.invalid_row_range_order
65
+ rescue ArgumentError => e
66
+ return @errors.empty_output_path if e.message == "file output path cannot be empty"
67
+
68
+ raise e
69
+ end
70
+
71
+ private
72
+
73
+ def handle_error(result)
74
+ @result_error_handler.call(result, {
75
+ file_not_found: ->(r, errors) { errors.file_not_found(r.data[:path]) },
76
+ no_headers: ->(_r, errors) { errors.no_headers },
77
+ could_not_parse_csv: ->(_r, errors) { errors.could_not_parse_csv },
78
+ cannot_read_file: ->(r, errors) { errors.cannot_read_file(r.data[:path]) },
79
+ cannot_write_output_file: ->(r, errors) { errors.cannot_write_output_file(r.data[:path], r.data[:error_class]) }
80
+ })
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csvtool/application/use_cases/run_row_randomization"
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/interface/cli/workflows/builders/row_randomization_session_builder"
11
+ require "csvtool/interface/cli/workflows/presenters/row_randomization_presenter"
12
+ require "csvtool/interface/cli/workflows/support/output_destination_mapper"
13
+ require "csvtool/interface/cli/workflows/support/result_error_handler"
14
+ require "csvtool/interface/cli/workflows/steps/workflow_step_pipeline"
15
+ require "csvtool/interface/cli/workflows/steps/row_randomization/collect_inputs_step"
16
+ require "csvtool/interface/cli/workflows/steps/row_randomization/collect_destination_step"
17
+ require "csvtool/interface/cli/workflows/steps/row_randomization/execute_step"
18
+ module Csvtool
19
+ module Interface
20
+ module CLI
21
+ module Workflows
22
+ class RunRowRandomizationWorkflow
23
+ def initialize(stdin:, stdout:, use_case: Application::UseCases::RunRowRandomization.new)
24
+ @stdin = stdin
25
+ @stdout = stdout
26
+ @use_case = use_case
27
+ @errors = Interface::CLI::Errors::Presenter.new(stdout: stdout)
28
+ @session_builder = Builders::RowRandomizationSessionBuilder.new
29
+ @output_destination_mapper = Support::OutputDestinationMapper.new
30
+ @result_error_handler = Support::ResultErrorHandler.new(errors: @errors)
31
+ end
32
+
33
+ def call
34
+ context = {
35
+ use_case: @use_case,
36
+ session_builder: @session_builder,
37
+ output_destination_mapper: @output_destination_mapper,
38
+ presenter_factory: ->(headers:, col_sep:) { Presenters::RowRandomizationPresenter.new(stdout: @stdout, headers: headers, col_sep: col_sep) },
39
+ handle_error: method(:handle_error)
40
+ }
41
+
42
+ pipeline = Steps::WorkflowStepPipeline.new(steps: [
43
+ Steps::RowRandomization::CollectInputsStep.new(
44
+ file_path_prompt: Interface::CLI::Prompts::FilePathPrompt.new(stdin: @stdin, stdout: @stdout),
45
+ separator_prompt: Interface::CLI::Prompts::SeparatorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors),
46
+ headers_present_prompt: Interface::CLI::Prompts::HeadersPresentPrompt.new(stdin: @stdin, stdout: @stdout),
47
+ seed_prompt: Interface::CLI::Prompts::SeedPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors)
48
+ ),
49
+ Steps::RowRandomization::CollectDestinationStep.new(
50
+ output_destination_prompt: Interface::CLI::Prompts::OutputDestinationPrompt.new(
51
+ stdin: @stdin,
52
+ stdout: @stdout,
53
+ errors: @errors
54
+ )
55
+ ),
56
+ Steps::RowRandomization::ExecuteStep.new
57
+ ])
58
+ pipeline.call(context)
59
+ rescue ArgumentError => e
60
+ return @errors.empty_output_path if e.message == "file output path cannot be empty"
61
+
62
+ raise e
63
+ end
64
+
65
+ private
66
+
67
+ def handle_error(result)
68
+ @result_error_handler.call(result, {
69
+ file_not_found: ->(r, errors) { errors.file_not_found(r.data[:path]) },
70
+ no_headers: ->(_r, errors) { errors.no_headers },
71
+ could_not_parse_csv: ->(_r, errors) { errors.could_not_parse_csv },
72
+ cannot_read_file: ->(r, errors) { errors.cannot_read_file(r.data[:path]) },
73
+ cannot_write_output_file: ->(r, errors) { errors.cannot_write_output_file(r.data[:path], r.data[:error_class]) }
74
+ })
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end