dsu 1.2.1 → 2.0.1

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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -21
  3. data/Gemfile.lock +7 -7
  4. data/README.md +28 -35
  5. data/bin/console +23 -1
  6. data/bin/dsu +3 -0
  7. data/bin/setup +14 -3
  8. data/exe/dsu +23 -1
  9. data/exe/dsu_migrate.rb +43 -0
  10. data/lib/core/ruby/color_theme_colors.rb +16 -0
  11. data/lib/core/ruby/color_theme_mode.rb +42 -0
  12. data/lib/core/ruby/not_today.rb +7 -0
  13. data/lib/core/ruby/wrap_and_join.rb +31 -0
  14. data/lib/dsu/base_cli.rb +19 -23
  15. data/lib/dsu/cli.rb +47 -37
  16. data/lib/dsu/command_services/add_entry_service.rb +10 -21
  17. data/lib/dsu/crud/json_file.rb +139 -0
  18. data/lib/dsu/crud/raw_json_file.rb +51 -0
  19. data/lib/dsu/env.rb +21 -0
  20. data/lib/dsu/migration/service.rb +196 -0
  21. data/lib/dsu/migration/version.rb +7 -0
  22. data/lib/dsu/models/color_theme.rb +270 -0
  23. data/lib/dsu/models/configuration.rb +160 -0
  24. data/lib/dsu/models/entry.rb +6 -2
  25. data/lib/dsu/models/entry_group.rb +143 -42
  26. data/lib/dsu/models/migration_version.rb +48 -0
  27. data/lib/dsu/presenters/base_presenter.rb +32 -0
  28. data/lib/dsu/presenters/color_theme_presenter.rb +50 -0
  29. data/lib/dsu/presenters/color_theme_show_presenter.rb +49 -0
  30. data/lib/dsu/presenters/configuration_presenter.rb +45 -0
  31. data/lib/dsu/presenters/entry_group_presenter.rb +35 -0
  32. data/lib/dsu/presenters/entry_presenter.rb +25 -0
  33. data/lib/dsu/services/color_theme/hydrator_service.rb +42 -0
  34. data/lib/dsu/services/configuration/hydrator_service.rb +42 -0
  35. data/lib/dsu/services/entry/hydrator_service.rb +33 -0
  36. data/lib/dsu/services/entry_group/editor_service.rb +107 -0
  37. data/lib/dsu/services/entry_group/hydrator_service.rb +37 -0
  38. data/lib/dsu/services/migration_version/hydrator_service.rb +36 -0
  39. data/lib/dsu/services/stderr_redirector_service.rb +27 -0
  40. data/lib/dsu/services/temp_file/reader_service.rb +33 -0
  41. data/lib/dsu/services/temp_file/writer_service.rb +35 -0
  42. data/lib/dsu/subcommands/base_subcommand.rb +14 -0
  43. data/lib/dsu/subcommands/config.rb +92 -32
  44. data/lib/dsu/subcommands/edit.rb +3 -3
  45. data/lib/dsu/subcommands/list.rb +70 -93
  46. data/lib/dsu/subcommands/theme.rb +159 -0
  47. data/lib/dsu/support/ask.rb +14 -19
  48. data/lib/dsu/support/color_themable.rb +34 -0
  49. data/lib/dsu/support/command_help_colorizeable.rb +27 -0
  50. data/lib/dsu/support/command_hookable.rb +60 -0
  51. data/lib/dsu/support/command_options/dsu_times.rb +32 -21
  52. data/lib/dsu/support/command_options/time.rb +7 -1
  53. data/lib/dsu/support/command_options/time_mneumonic.rb +7 -1
  54. data/lib/dsu/support/descriptable.rb +6 -4
  55. data/lib/dsu/support/entry_group_viewable.rb +28 -4
  56. data/lib/dsu/support/fileable.rb +94 -0
  57. data/lib/dsu/support/presentable.rb +11 -0
  58. data/lib/dsu/support/subcommand_help_colorizeable.rb +27 -0
  59. data/lib/dsu/support/time_comparable.rb +19 -0
  60. data/lib/dsu/support/time_formatable.rb +12 -0
  61. data/lib/dsu/support/times_sortable.rb +48 -14
  62. data/lib/dsu/support/utils.rb +11 -0
  63. data/lib/dsu/validators/color_theme_validator.rb +74 -0
  64. data/lib/dsu/validators/entries_validator.rb +4 -8
  65. data/lib/dsu/validators/version_validator.rb +29 -0
  66. data/lib/dsu/version.rb +2 -1
  67. data/lib/dsu/views/color_theme/index.rb +62 -0
  68. data/lib/dsu/views/color_theme/show.rb +106 -0
  69. data/lib/dsu/views/configuration/show.rb +41 -0
  70. data/lib/dsu/views/entry_group/edit.rb +3 -5
  71. data/lib/dsu/views/entry_group/shared/no_entries_to_display.rb +41 -0
  72. data/lib/dsu/views/entry_group/show.rb +16 -15
  73. data/lib/dsu/views/shared/error.rb +17 -0
  74. data/lib/dsu/views/shared/info.rb +17 -0
  75. data/lib/dsu/views/shared/message.rb +85 -0
  76. data/lib/dsu/views/shared/model_errors.rb +31 -0
  77. data/lib/dsu/views/shared/success.rb +17 -0
  78. data/lib/dsu/views/shared/warning.rb +17 -0
  79. data/lib/dsu.rb +22 -1
  80. data/lib/seed_data/themes/cherry.json +79 -0
  81. data/lib/seed_data/themes/default.json +79 -0
  82. data/lib/seed_data/themes/lemon.json +79 -0
  83. data/lib/seed_data/themes/matrix.json +79 -0
  84. data/lib/seed_data/themes/whiteout.json +79 -0
  85. metadata +68 -23
  86. data/lib/dsu/core/ruby/not_today.rb +0 -11
  87. data/lib/dsu/services/ai/tense_translator_service.rb +0 -63
  88. data/lib/dsu/services/configuration_loader_service.rb +0 -55
  89. data/lib/dsu/services/entry_group_deleter_service.rb +0 -31
  90. data/lib/dsu/services/entry_group_editor_service.rb +0 -96
  91. data/lib/dsu/services/entry_group_hydrator_service.rb +0 -43
  92. data/lib/dsu/services/entry_group_reader_service.rb +0 -36
  93. data/lib/dsu/services/entry_group_writer_service.rb +0 -46
  94. data/lib/dsu/services/entry_hydrator_service.rb +0 -35
  95. data/lib/dsu/services/temp_file_reader_service.rb +0 -31
  96. data/lib/dsu/services/temp_file_writer_service.rb +0 -33
  97. data/lib/dsu/support/colorable.rb +0 -14
  98. data/lib/dsu/support/configurable.rb +0 -15
  99. data/lib/dsu/support/configuration.rb +0 -112
  100. data/lib/dsu/support/entry_group_fileable.rb +0 -49
  101. data/lib/dsu/support/entry_group_loadable.rb +0 -49
  102. data/lib/dsu/support/folder_locations.rb +0 -21
  103. data/lib/dsu/support/say.rb +0 -40
  104. data/lib/dsu/views/edited_entries/shared/errors.rb +0 -39
  105. data/lib/dsu/views/shared/messages.rb +0 -56
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require_relative '../crud/json_file'
5
+ require_relative '../services/migration_version/hydrator_service'
6
+ require_relative '../validators/version_validator'
7
+
8
+ module Dsu
9
+ module Models
10
+ # This class represents a dsu migration_version.
11
+ class MigrationVersion < Crud::JsonFile
12
+ include Support::Fileable
13
+
14
+ attr_reader :options
15
+
16
+ def initialize(version: nil, options: {})
17
+ super(migration_version_path)
18
+
19
+ FileUtils.mkdir_p migration_version_folder
20
+
21
+ @options = options || {}
22
+ @version = version and return if version
23
+
24
+ file_hash = if exist?
25
+ read do |migration_version_hash|
26
+ hydrated_hash =
27
+ Services::MigrationVersion::HydratorService.new(migration_version_hash: migration_version_hash).call
28
+ migration_version_hash.merge!(hydrated_hash)
29
+ end
30
+ end
31
+
32
+ self.version = file_hash.try(:[], :version) || 0
33
+ end
34
+
35
+ # Returns true if the current dsu install is the
36
+ # current migration version.
37
+ def current_migration?
38
+ version == Dsu::Migration::VERSION
39
+ end
40
+
41
+ def to_h
42
+ {
43
+ version: version
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+ require_relative '../models/color_theme'
5
+ require_relative '../support/color_themable'
6
+
7
+ module Dsu
8
+ module Presenters
9
+ class BasePresenter < SimpleDelegator
10
+ include Support::ColorThemable
11
+
12
+ attr_reader :color_theme
13
+
14
+ def initialize(object, options: {})
15
+ super(object)
16
+
17
+ @options = options || {}
18
+ theme_name = options.fetch(:theme_name, Models::Configuration.new.theme_name)
19
+ @color_theme = Models::ColorTheme.find(theme_name: theme_name)
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :options
25
+
26
+ def formatted_index(index:)
27
+ apply_theme("#{format('%02s', index + 1)}. ",
28
+ theme_color: color_theme.index)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_presenter'
4
+
5
+ module Dsu
6
+ module Presenters
7
+ class ColorThemePresenter < BasePresenter
8
+ attr_reader :color_theme
9
+
10
+ def initialize(color_theme, options: {})
11
+ super
12
+
13
+ @color_theme = color_theme
14
+ end
15
+
16
+ def header
17
+ apply_theme('Color Themes', theme_color: color_theme.subheader)
18
+ end
19
+
20
+ def footer
21
+ apply_theme('* current theme', theme_color: color_theme.footer)
22
+ end
23
+
24
+ def detail
25
+ "#{apply_theme(theme_name_formatted, theme_color: color_theme.body)} - " \
26
+ "#{apply_theme(description, theme_color: color_theme.body)}"
27
+ end
28
+
29
+ def detail_with_index(index:)
30
+ "#{formatted_index(index: index)} #{detail}"
31
+ end
32
+
33
+ private
34
+
35
+ def theme_name_formatted
36
+ return theme_name unless default_color_theme?
37
+
38
+ "*#{theme_name}"
39
+ end
40
+
41
+ def default_color_theme?
42
+ theme_name == default_color_theme.theme_name
43
+ end
44
+
45
+ def default_color_theme
46
+ @default_color_theme ||= Models::ColorTheme.current_or_default
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_presenter'
4
+
5
+ module Dsu
6
+ module Presenters
7
+ class ColorThemeShowPresenter < BasePresenter
8
+ def initialize(color_theme, options: {})
9
+ super(color_theme, options: options.merge(theme_name: color_theme.theme_name))
10
+ end
11
+
12
+ def detail
13
+ puts_detail('No.', 'Color', 'Values', header: true)
14
+
15
+ Models::ColorTheme::DEFAULT_THEME_COLORS.keys.each_with_index do |color_key, index|
16
+ index = formatted_index(index: index)
17
+ color_hash = color_theme.public_send(color_key)
18
+ puts_detail(index, color_key, color_hash)
19
+ end
20
+ end
21
+
22
+ def detail_with_index(index:)
23
+ "#{formatted_index(index: index)} #{detail}"
24
+ end
25
+
26
+ def footer
27
+ apply_theme('Footer example', theme_color: color_theme.footer)
28
+ end
29
+
30
+ def header
31
+ apply_theme("Viewing color theme: #{color_theme.theme_name}", theme_color: color_theme.subheader)
32
+ end
33
+
34
+ private
35
+
36
+ def puts_detail(index, color_key, color_hash, header: false)
37
+ if header
38
+ puts "#{apply_theme(index.to_s.ljust(4), theme_color: color_theme.index.bold!)} " \
39
+ "#{apply_theme(color_key.to_s.ljust(15), theme_color: color_theme.index.bold!)} " \
40
+ "#{apply_theme(color_hash.to_s.ljust(10), theme_color: color_theme.index.bold!)}"
41
+ else
42
+ puts "#{apply_theme(index.to_s.ljust(4), theme_color: color_theme.index)} " \
43
+ "#{apply_theme(color_key.to_s.ljust(15), theme_color: color_hash)} " \
44
+ "#{apply_theme(color_hash.to_s.ljust(10), theme_color: color_theme.body)}"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../support/color_themable'
4
+ require_relative 'base_presenter'
5
+
6
+ module Dsu
7
+ module Presenters
8
+ class ConfigurationPresenter < BasePresenter
9
+ attr_reader :config
10
+
11
+ def initialize(config, options: {})
12
+ super
13
+
14
+ @config = config
15
+ end
16
+
17
+ def configuration_header
18
+ apply_theme("Configuration file contents (#{config_path})",
19
+ theme_color: color_theme.header)
20
+ end
21
+
22
+ def configuration_details
23
+ to_h.each_with_index.filter_map do |config_entry, index|
24
+ formatted_config_entry_with_index(config_entry, index: index, theme_color: color_theme.body)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def config_path
31
+ @config_path ||= config.file_path
32
+ end
33
+
34
+ def formatted_config_entry_with_index(config_entry, index:, theme_color:)
35
+ "#{formatted_index(index: index)} #{formatted_config_entry(config_entry: config_entry,
36
+ theme_color: theme_color)}"
37
+ end
38
+
39
+ def formatted_config_entry(config_entry:, theme_color:)
40
+ config_entry = "#{config_entry[0]}: '#{config_entry[1]}'"
41
+ apply_theme(config_entry, theme_color: theme_color)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../support/time_formatable'
4
+ require_relative 'base_presenter'
5
+
6
+ module Dsu
7
+ module Presenters
8
+ class EntryGroupPresenter < BasePresenter
9
+ attr_reader :entry_group
10
+
11
+ def initialize(entry_group, options: {})
12
+ super
13
+
14
+ @entry_group = entry_group
15
+ end
16
+
17
+ def formatted_time
18
+ colors = color_theme.date
19
+ apply_theme(Support::TimeFormatable.formatted_time(time: time), theme_color: colors)
20
+ end
21
+
22
+ def formatted_errors
23
+ return if valid?
24
+
25
+ colors = color_theme.error
26
+ apply_theme(errors.full_messages.join(', '), theme_color: colors)
27
+ end
28
+
29
+ def no_entries_available
30
+ colors = color_theme.info
31
+ apply_theme('(no entries available for this day)', theme_color: colors)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_presenter'
4
+
5
+ module Dsu
6
+ module Presenters
7
+ class EntryPresenter < BasePresenter
8
+ attr_reader :entry
9
+
10
+ def initialize(entry, options: {})
11
+ super
12
+
13
+ @entry = entry
14
+ end
15
+
16
+ def formatted_description
17
+ apply_theme(description, theme_color: color_theme.body)
18
+ end
19
+
20
+ def formatted_description_with_index(index:)
21
+ "#{formatted_index(index: index)} #{formatted_description}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/color_theme'
4
+
5
+ module Dsu
6
+ module Services
7
+ module ColorTheme
8
+ class HydratorService
9
+ def initialize(theme_name:, theme_hash:, options: {})
10
+ raise ArgumentError, 'theme_name is nil.' if theme_name.nil?
11
+ raise ArgumentError, "theme_name is the wrong object type: \"#{theme_name}\"." unless theme_hash.is_a?(Hash)
12
+ raise ArgumentError, 'theme_hash is nil' if theme_hash.nil?
13
+ raise ArgumentError, "theme_hash is the wrong object type: \"#{theme_hash}\"" unless theme_hash.is_a?(Hash)
14
+ raise ArgumentError, 'options is nil' if options.nil?
15
+ raise ArgumentError, "options is the wrong object type:\"#{options}\"" unless options.is_a?(Hash)
16
+
17
+ @theme_name = theme_name
18
+ @theme_hash = theme_hash
19
+ @options = options || {}
20
+ end
21
+
22
+ def call
23
+ Models::ColorTheme.new(theme_name: theme_name, theme_hash: hydrate)
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :theme_hash, :theme_name, :options
29
+
30
+ def hydrate
31
+ theme_hash.each_pair do |key, value|
32
+ next if %i[version description].include?(key)
33
+
34
+ value.each_pair do |k, _v|
35
+ value[k] = value[k].to_sym
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/configuration'
4
+ require_relative '../../views/shared/message'
5
+
6
+ module Dsu
7
+ module Services
8
+ module Configuration
9
+ class HydratorService
10
+ def initialize(config_hash:, options: {})
11
+ raise ArgumentError, 'config_hash is nil' if config_hash.nil?
12
+
13
+ unless config_hash.is_a?(Hash)
14
+ raise ArgumentError,
15
+ "config_hash is the wrong object type: \"#{config_hash}\""
16
+ end
17
+ raise ArgumentError, 'options is nil' if options.nil?
18
+ raise ArgumentError, "options is the wrong object type:\"#{options}\"" unless options.is_a?(Hash)
19
+
20
+ @config_hash = config_hash.dup
21
+ @options = options || {}
22
+ end
23
+
24
+ def call
25
+ hydrate
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :config_hash, :options
31
+
32
+ def hydrate
33
+ config_hash[:version] = config_hash[:version].to_i
34
+ config_hash[:entries_display_order] = config_hash[:entries_display_order].to_sym
35
+ config_hash
36
+ rescue JSON::ParserError => _e
37
+ Models::Configuration::DEFAULT_CONFIGURATION
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/entry'
4
+
5
+ module Dsu
6
+ module Services
7
+ module Entry
8
+ class HydratorService
9
+ def initialize(entries_array:, options: {})
10
+ raise ArgumentError, 'entries_array is nil' if entries_array.nil?
11
+ raise ArgumentError, 'options is nil' if options.nil?
12
+
13
+ @entries_array = entries_array
14
+ @options = options || {}
15
+ end
16
+
17
+ def call
18
+ hydrate
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :entries_array, :options
24
+
25
+ def hydrate
26
+ entries_array.map do |entry_hash|
27
+ Dsu::Models::Entry.new(**entry_hash)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/color_theme'
4
+ require_relative '../../models/entry'
5
+ require_relative '../../support/color_themable'
6
+ require_relative '../../support/time_formatable'
7
+ require_relative '../../views/shared/model_errors'
8
+ require_relative '../stdout_redirector_service'
9
+ require_relative '../temp_file/reader_service'
10
+ require_relative '../temp_file/writer_service'
11
+
12
+ module Dsu
13
+ module Services
14
+ module EntryGroup
15
+ class EditorService
16
+ include Support::ColorThemable
17
+ include Support::TimeFormatable
18
+
19
+ def initialize(entry_group:, options: {})
20
+ raise ArgumentError, 'entry_group is nil' if entry_group.nil?
21
+ raise ArgumentError, 'entry_group is the wrong object type' unless entry_group.is_a?(Models::EntryGroup)
22
+ raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash) || options.nil?
23
+
24
+ @entry_group = entry_group
25
+ @options = options || {}
26
+ end
27
+
28
+ def call
29
+ edit_view = render_edit_view
30
+ edit edit_view
31
+ # NOTE: Return the original entry group object as any permanent changes
32
+ # will have been applied to it.
33
+ entry_group
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :entry_group, :options
39
+
40
+ # Renders the edit view to a string so we can write it to a temporary file
41
+ # and edit it. The edits will be used to update the entry group.
42
+ def render_edit_view
43
+ puts apply_theme("Editing entry group #{formatted_time(time: entry_group.time)}...",
44
+ theme_color: color_theme.header)
45
+ # TODO: Call #render_as_string directly below?
46
+ StdoutRedirectorService.call { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
47
+ # Views::EntryGroup::Edit.new(entry_group: entry_group).render_as_string
48
+ end
49
+
50
+ # Writes the temporary file contents to disk and opens it in the editor
51
+ # for editing. It then copies the changes to the entry group and writes
52
+ # the changes to the entry group file.
53
+ def edit(edit_view)
54
+ entry_group_with_edits = Models::EntryGroup.new(time: entry_group.time)
55
+
56
+ TempFile::WriterService.new(tmp_file_content: edit_view).call do |tmp_file_path|
57
+ if Kernel.system("${EDITOR:-#{configuration.editor}} #{tmp_file_path}")
58
+ TempFile::ReaderService.new(tmp_file_path: tmp_file_path).call do |editor_line|
59
+ next unless process_description?(editor_line)
60
+
61
+ entry_group_with_edits.entries << Models::Entry.new(description: editor_line)
62
+ end
63
+
64
+ process_entry_group!(entry_group_with_edits)
65
+ else
66
+ puts apply_theme(
67
+ [
68
+ "Failed to open temporary file in editor '#{configuration.editor}'; " \
69
+ "the system error returned was: '#{$CHILD_STATUS}'.",
70
+ 'Either set the EDITOR environment variable ' \
71
+ 'or set the dsu editor configuration option (`$ dsu config info`).',
72
+ 'Run `$ dsu help config` for more information.'
73
+ ], theme_color: color_theme.error
74
+ )
75
+ end
76
+ end
77
+ end
78
+
79
+ def process_entry_group!(entry_group_with_edits)
80
+ if entry_group_with_edits.entries.empty?
81
+ entry_group.delete
82
+ return
83
+ end
84
+
85
+ Views::Shared::ModelErrors.new(model: entry_group_with_edits).render if entry_group_with_edits.invalid?
86
+
87
+ # Make sure we're saving only valid, unique entries.
88
+ entry_group.entries = entry_group_with_edits.valid_unique_entries
89
+ entry_group.save!
90
+ end
91
+
92
+ def process_description?(description)
93
+ description = Models::Entry.clean_description(description)
94
+ !(description.blank? || description[0] == '#')
95
+ end
96
+
97
+ def color_theme
98
+ @color_theme ||= Models::ColorTheme.current_or_default
99
+ end
100
+
101
+ def configuration
102
+ Models::Configuration.new
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/entry_group'
4
+ require_relative '../entry/hydrator_service'
5
+
6
+ module Dsu
7
+ module Services
8
+ module EntryGroup
9
+ class HydratorService
10
+ def initialize(entry_group_hash:, options: {})
11
+ raise ArgumentError, 'entry_group_hash is nil' if entry_group_hash.nil?
12
+ raise ArgumentError, 'options is nil' if options.nil?
13
+
14
+ @entry_group_hash = entry_group_hash
15
+ @options = options || {}
16
+ end
17
+
18
+ def call
19
+ Models::EntryGroup.new(**hydrate)
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :entry_group_hash, :options
25
+
26
+ # Returns a Hash with :time and :entries values hydrated (i.e. Time and Entry objects respectively).
27
+ def hydrate
28
+ entry_group_hash.tap do |hash|
29
+ hash[:time] = Time.parse(hash[:time])
30
+ hash[:entries] =
31
+ Entry::HydratorService.new(entries_array: hash[:entries], options: options).call
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Services
5
+ module MigrationVersion
6
+ class HydratorService
7
+ def initialize(migration_version_hash:, options: {})
8
+ raise ArgumentError, 'migration_version_hash is nil' if migration_version_hash.nil?
9
+
10
+ unless migration_version_hash.is_a?(Hash)
11
+ raise ArgumentError,
12
+ "migration_version_hash is the wrong object type: \"#{migration_version_hash}\""
13
+ end
14
+ raise ArgumentError, 'options is nil' if options.nil?
15
+ raise ArgumentError, "options is the wrong object type:\"#{options}\"" unless options.is_a?(Hash)
16
+
17
+ @migration_version_hash = migration_version_hash.dup
18
+ @options = options || {}
19
+ end
20
+
21
+ def call
22
+ hydrate
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :migration_version_hash, :options
28
+
29
+ def hydrate
30
+ migration_version_hash[:version] = migration_version_hash[:version].to_i
31
+ migration_version_hash
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Services
5
+ # This service captures $stderr, resirects it to a StringIO object,
6
+ # and returns the string value.
7
+ # https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
8
+ module StderrRedirectorService
9
+ class << self
10
+ def call
11
+ raise ArgumentError, 'no block was provided' unless block_given?
12
+
13
+ # The output stream must be an IO-like object. In this case we capture it in
14
+ # an in-memory IO object so we can return the string value. Any IO object can
15
+ # be used here.
16
+ string_io = StringIO.new
17
+ original_stderr, $stderr = $stderr, string_io # rubocop:disable Style/ParallelAssignment
18
+ yield
19
+ string_io.string
20
+ ensure
21
+ # Restore the original $stderr.
22
+ $stderr = original_stderr
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Services
5
+ module TempFile
6
+ class ReaderService
7
+ def initialize(tmp_file_path:, options: {})
8
+ raise ArgumentError, 'tmp_file_path is nil' if tmp_file_path.nil?
9
+ raise ArgumentError, 'tmp_file_path is the wrong object type' unless tmp_file_path.is_a?(String)
10
+ raise ArgumentError, 'tmp_file_path is empty' if tmp_file_path.empty?
11
+ raise ArgumentError, 'tmp_file_path does not exist' unless File.exist?(tmp_file_path)
12
+ raise ArgumentError, 'options is nil' if options.nil?
13
+ raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
14
+
15
+ @tmp_file_path = tmp_file_path
16
+ @options = options || {}
17
+ end
18
+
19
+ def call
20
+ raise ArgumentError, 'no block given' unless block_given?
21
+
22
+ File.foreach(tmp_file_path) do |line|
23
+ yield line.strip
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :tmp_file_path, :options
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+
5
+ module Dsu
6
+ module Services
7
+ module TempFile
8
+ class WriterService
9
+ def initialize(tmp_file_content:, options: {})
10
+ raise ArgumentError, 'tmp_file_content is nil' if tmp_file_content.nil?
11
+ raise ArgumentError, 'tmp_file_content is the wrong object type' unless tmp_file_content.is_a?(String)
12
+ raise ArgumentError, 'options is nil' if options.nil?
13
+ raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
14
+
15
+ @tmp_file_content = tmp_file_content
16
+ @options = options || {}
17
+ end
18
+
19
+ def call
20
+ raise ArgumentError, 'no block given' unless block_given?
21
+
22
+ Tempfile.new('dsu').tap do |file|
23
+ file.write("#{tmp_file_content}\n")
24
+ file.close
25
+ yield file.path
26
+ end.unlink
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :tmp_file_content, :options
32
+ end
33
+ end
34
+ end
35
+ end