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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+
5
+ # rubocop:disable Layout/LineLength
6
+ module Dsu
7
+ module Validators
8
+ class ColorThemeValidator < ActiveModel::Validator
9
+ def validate(record)
10
+ default_theme_colors = record.class::DEFAULT_THEME_COLORS
11
+
12
+ # return unless validate_color_theme_keys!(record, default_theme_colors)
13
+
14
+ default_theme_colors.each_key do |theme_color_key|
15
+ theme_colors_hash = record.public_send(theme_color_key)
16
+
17
+ next unless validate_theme_color_type!(record, theme_color_key, theme_colors_hash)
18
+
19
+ if theme_colors_hash.empty?
20
+ record.errors.add(:base, ":#{theme_color_key} colors Hash is empty")
21
+ next
22
+ end
23
+
24
+ validate_theme_colors!(record, theme_colors_hash)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def colors
31
+ @colors ||= String.colors
32
+ end
33
+
34
+ def modes
35
+ @modes ||= String.modes
36
+ end
37
+
38
+ def validate_theme_color_type!(record, theme_color_key, theme_colors_hash)
39
+ return true if theme_colors_hash.is_a?(Hash)
40
+
41
+ record.errors.add(:base, ":#{theme_color_key} value is the wrong object type. " \
42
+ "\"Hash\" was expected, but \"#{theme_colors_hash.class}\" was received.")
43
+ false
44
+ end
45
+
46
+ def validate_theme_colors!(record, theme_colors_hash)
47
+ unless colors.include?(theme_colors_hash[:color])
48
+ value = theme_color_value_to_s(theme_colors_hash[:color])
49
+ record.errors.add(:base, ":color key value #{value} in theme color Hash #{theme_colors_hash} is not a valid color. " \
50
+ "One of #{colors.wrap_and_join(wrapper: [':', ''])} was expected, but #{value} was received.")
51
+ end
52
+
53
+ unless theme_colors_hash[:mode].nil? || modes.include?(theme_colors_hash[:mode])
54
+ value = theme_color_value_to_s(theme_colors_hash[:mode])
55
+ record.errors.add(:base, ":mode key value #{value} in theme color Hash #{theme_colors_hash} is not a valid mode value. " \
56
+ "One of #{modes.wrap_and_join(wrapper: [':', ''])} was expected, but #{value} was received.")
57
+ end
58
+
59
+ unless theme_colors_hash[:background].nil? || colors.include?(theme_colors_hash[:background])
60
+ value = theme_color_value_to_s(theme_colors_hash[:background])
61
+ record.errors.add(:base, ":background key value #{value} in theme color Hash #{theme_colors_hash} is not a valid color. " \
62
+ "One of #{colors.wrap_and_join(wrapper: [':', ''])} was expected, but #{value} was received.")
63
+ end
64
+ end
65
+
66
+ def theme_color_value_to_s(theme_color_value)
67
+ return ":#{theme_color_value}" if theme_color_value.is_a?(Symbol)
68
+
69
+ "'#{theme_color_value}'"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ # rubocop:enable Layout/LineLength
@@ -17,8 +17,8 @@ module Dsu
17
17
  end
18
18
 
19
19
  return if validate_entry_types record
20
- return if validate_unique_entry record
21
20
 
21
+ validate_unique_entry record
22
22
  validate_entries record
23
23
  end
24
24
 
@@ -45,9 +45,9 @@ module Dsu
45
45
  return if descriptions.uniq.length == descriptions.length
46
46
 
47
47
  non_unique_descriptions = descriptions.select { |description| descriptions.count(description) > 1 }.uniq
48
- if non_unique_descriptions.any?
49
- record.errors.add(:entries, 'Array contains a duplicate entry: ' \
50
- "#{format_non_unique_descriptions(non_unique_descriptions)}.",
48
+ non_unique_descriptions.each do |non_unique_description|
49
+ record.errors.add(:entries,
50
+ "array contains duplicate entry: \"#{short_description(non_unique_description)}\".",
51
51
  type: Support::FieldErrors::FIELD_DUPLICATE_ERROR)
52
52
  end
53
53
 
@@ -67,10 +67,6 @@ module Dsu
67
67
  end
68
68
  end
69
69
 
70
- def format_non_unique_descriptions(non_unique_descriptions)
71
- non_unique_descriptions.map { |description| "\"#{short_description(description)}\"" }.join(', ')
72
- end
73
-
74
70
  def short_description(description)
75
71
  Models::Entry.short_description(string: description)
76
72
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # https://guides.rubyonrails.org/active_record_validations.html#validates-with
4
+ module Dsu
5
+ module Validators
6
+ class VersionValidator < ActiveModel::Validator
7
+ def validate(record)
8
+ version = record.version
9
+
10
+ if version.nil?
11
+ record.errors.add(:version, 'is nil')
12
+ return
13
+ end
14
+
15
+ unless version.is_a?(Integer)
16
+ record.errors.add(:version, 'is the wrong object type. ' \
17
+ "\"Integer\" was expected, but \"#{version.class}\" was received.")
18
+ nil
19
+ end
20
+
21
+ # TODO: This validation should check the configuration version
22
+ # against the current migration version and they should match.
23
+ # unless version == record.class::VERSION
24
+ # record.errors.add(:version, "\"#{version}\" is not the correct version: \"#{record.class::VERSION}\"")
25
+ # end
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/dsu/version.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dsu
4
- VERSION = '1.2.1'
4
+ VERSION_REGEX = /\A\d+\.\d+\.\d+(\.alpha\.\d+)?\z/
5
+ VERSION = '2.0.1'
5
6
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/configuration'
4
+ require_relative '../../models/color_theme'
5
+
6
+ module Dsu
7
+ module Views
8
+ module ColorTheme
9
+ class Index
10
+ include Support::ColorThemable
11
+
12
+ def render
13
+ render!
14
+ end
15
+
16
+ private
17
+
18
+ def render!
19
+ presenter = color_theme.presenter
20
+ puts presenter.header
21
+ puts
22
+ render_theme_details
23
+ puts
24
+ puts presenter.footer
25
+ end
26
+
27
+ def render_theme_details
28
+ themes_folder = Models::Configuration.new.themes_folder
29
+ theme_file_names = Dir.glob("#{themes_folder}/*").map { |theme_path| File.basename(theme_path, '.*') }
30
+ theme_file_names << default_theme_name unless theme_file_names.include?(default_theme_name)
31
+ theme_file_names.sort.each_with_index do |theme_file, index|
32
+ color_theme = if theme_file == default_theme_name
33
+ default_theme
34
+ else
35
+ Models::ColorTheme.find(theme_name: theme_file)
36
+ end
37
+ presenter = color_theme.presenter
38
+ puts presenter.detail_with_index(index: index)
39
+ end
40
+ end
41
+
42
+ # When getting the default theme, if it exists on disk, use that; otherwise,
43
+ # use the in-memory default theme.
44
+ def default_theme
45
+ if Models::ColorTheme.exist?(theme_name: default_theme_name)
46
+ Models::ColorTheme.find(theme_name: default_theme_name)
47
+ else
48
+ Models::ColorTheme.default
49
+ end
50
+ end
51
+
52
+ def default_theme_name
53
+ Dsu::Models::ColorTheme::DEFAULT_THEME_NAME
54
+ end
55
+
56
+ def color_theme
57
+ @color_theme ||= Models::ColorTheme.current_or_default
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/color_theme'
4
+ require_relative '../../models/configuration'
5
+ require_relative '../entry_group/show'
6
+ require_relative '../shared/error'
7
+ require_relative '../shared/info'
8
+ require_relative '../shared/success'
9
+ require_relative '../shared/warning'
10
+
11
+ module Dsu
12
+ module Views
13
+ module ColorTheme
14
+ class Show
15
+ include Support::ColorThemable
16
+
17
+ def initialize(theme_name:, options: {})
18
+ @theme_name = theme_name
19
+ @options = options || {}
20
+ end
21
+
22
+ def render
23
+ render!
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :theme_name, :options
29
+
30
+ def color_theme
31
+ @color_theme ||= Models::ColorTheme.find_or_initialize(theme_name: theme_name)
32
+ end
33
+
34
+ def presenter
35
+ @presenter ||= Dsu::Presenters::ColorThemeShowPresenter.new(color_theme, options: options)
36
+ end
37
+
38
+ def render!
39
+ puts presenter.header
40
+ puts
41
+
42
+ presenter.detail
43
+ puts
44
+
45
+ display_entry_group_example
46
+ puts
47
+
48
+ display_configuration_example
49
+ puts
50
+
51
+ display_messages_example
52
+ puts
53
+
54
+ puts presenter.footer
55
+ end
56
+
57
+ def display_entry_group_example
58
+ puts apply_theme('`dsu list` example', theme_color: color_theme.subheader)
59
+ puts
60
+
61
+ options = custom_options
62
+ entry_group = mock_entry_group(options)
63
+ EntryGroup::Show.new(entry_group: entry_group, options: options).render
64
+ end
65
+
66
+ def display_configuration_example
67
+ puts apply_theme('`dsu config info` example', theme_color: color_theme.subheader)
68
+ puts
69
+
70
+ Views::Configuration::Show.new(config: Models::Configuration.new(options: custom_options)).render
71
+ end
72
+
73
+ def display_messages_example
74
+ options = custom_options
75
+ messages = ['Example 1', 'Example 2', 'Example 3']
76
+
77
+ puts apply_theme('Message examples', theme_color: color_theme.subheader)
78
+ puts
79
+
80
+ Shared::Error.new(messages: messages, header: 'Errors example', options: options).render
81
+ puts
82
+
83
+ Shared::Info.new(messages: messages, header: 'Info example', options: options).render
84
+ puts
85
+
86
+ Shared::Success.new(messages: messages, header: 'Success example', options: options).render
87
+ puts
88
+
89
+ Shared::Warning.new(messages: messages, header: 'Warning example', options: options).render
90
+ end
91
+
92
+ def mock_entry_group(options)
93
+ @mock_entry_group ||= Dsu::Models::EntryGroup.new(time: Time.now, entries: [
94
+ Dsu::Models::Entry.new(description: 'Dsu entry 1', options: options),
95
+ Dsu::Models::Entry.new(description: 'Dsu entry 2', options: options),
96
+ Dsu::Models::Entry.new(description: 'Dsu entry 3', options: options)
97
+ ], options: options)
98
+ end
99
+
100
+ def custom_options
101
+ options.merge(theme_name: color_theme.theme_name, output_stream: $stdout)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'active_support/core_ext/numeric/time'
5
+ require_relative '../../models/configuration'
6
+ require_relative '../../support/color_themable'
7
+
8
+ module Dsu
9
+ module Views
10
+ module Configuration
11
+ class Show
12
+ include Support::ColorThemable
13
+
14
+ def initialize(config:, options: {})
15
+ raise ArgumentError, 'config is nil' if config.nil?
16
+ raise ArgumentError, 'config is the wrong object type' unless config.is_a?(Models::Configuration)
17
+ raise ArgumentError, 'options is nil' if options.nil?
18
+ raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
19
+
20
+ @config = config
21
+ @options = options || {}
22
+ end
23
+
24
+ def call
25
+ render!
26
+ end
27
+ alias render call
28
+
29
+ private
30
+
31
+ attr_reader :config, :options
32
+
33
+ def render!
34
+ presenter = config.presenter
35
+ puts presenter.configuration_header
36
+ puts presenter.configuration_details
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../../models/configuration'
3
4
  require_relative '../../models/entry_group'
4
- require_relative '../../support/entry_group_fileable'
5
5
 
6
6
  module Dsu
7
7
  module Views
8
8
  module EntryGroup
9
9
  class Edit
10
- include Support::EntryGroupFileable
11
-
12
10
  def initialize(entry_group:, options: {})
13
11
  raise ArgumentError, 'entry_group is nil' if entry_group.nil?
14
12
  raise ArgumentError, 'entry_group is the wrong object type' unless entry_group.is_a?(Models::EntryGroup)
@@ -108,13 +106,13 @@ module Dsu
108
106
  # TODO: Make this configurable or accept an option?
109
107
  @previous_entry_group ||= (1..7).each do |days|
110
108
  t = time.days_ago(days)
111
- return Models::EntryGroup.load(time: t) if entry_group_file_exists_for?(time: t)
109
+ return Models::EntryGroup.find(time: t) if Models::EntryGroup.exist?(time: t)
112
110
  end
113
111
  nil
114
112
  end
115
113
 
116
114
  def carry_over_entries_to_today?
117
- configuration.merge(options)[:carry_over_entries_to_today]
115
+ Models::Configuration.new.merge(options).carry_over_entries_to_today?
118
116
  end
119
117
  end
120
118
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../models/color_theme'
4
+ require_relative '../../../support/color_themable'
5
+ require_relative '../../../support/time_formatable'
6
+
7
+ module Dsu
8
+ module Views
9
+ module EntryGroup
10
+ module Shared
11
+ class NoEntriesToDisplay
12
+ include Support::ColorThemable
13
+ include Support::TimeFormatable
14
+
15
+ def initialize(times:, options: {})
16
+ raise ArgumentError, 'times must be an Array' unless times.is_a?(Array)
17
+ raise ArgumentError, 'times must contain Time objects' unless times.all?(Time)
18
+ raise ArgumentError, 'options must be a Hash' unless options.is_a?(Hash) || options.nil?
19
+
20
+ @times = times
21
+ @options = options || {}
22
+ end
23
+
24
+ def render
25
+ entry_group_times.sort!
26
+ time_range = "#{formatted_time(time: times.first)} " \
27
+ "through #{formatted_time(time: times.last)}"
28
+ message = "(nothing to display for #{time_range})"
29
+ puts apply_theme(message, theme_color: color_theme.info)
30
+ end
31
+
32
+ private
33
+
34
+ def color_theme
35
+ @color_theme ||= Models::ColorTheme.current_or_default
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -3,16 +3,14 @@
3
3
  require 'time'
4
4
  require 'active_support/core_ext/numeric/time'
5
5
  require_relative '../../models/entry_group'
6
- require_relative '../../support/colorable'
7
- require_relative '../../support/say'
6
+ require_relative '../../support/color_themable'
8
7
  require_relative '../../support/time_formatable'
9
8
 
10
9
  module Dsu
11
10
  module Views
12
11
  module EntryGroup
13
12
  class Show
14
- include Support::Colorable
15
- include Support::Say
13
+ include Support::ColorThemable
16
14
  include Support::TimeFormatable
17
15
 
18
16
  def initialize(entry_group:, options: {})
@@ -35,22 +33,25 @@ module Dsu
35
33
  attr_reader :entry_group, :options
36
34
 
37
35
  def render!
38
- say formatted_time(time: entry_group.time), HIGHLIGHT
39
- say('(no entries available for this day)') and return if entry_group.entries.empty?
36
+ puts presenter.formatted_time
37
+
38
+ entry_group.validate!
39
+ puts presenter.no_entries_available and return if entry_group.entries.empty?
40
40
 
41
41
  entry_group.entries.each_with_index do |entry, index|
42
- prefix = "#{format('%03s', index + 1)}. "
43
- description = colorize_string(string: entry.description, mode: :bold)
44
- entry_info = "#{prefix} #{description}"
45
- unless entry.valid?
46
- entry_info = "#{entry_info} (validation failed: #{entry_errors(entry_group_deleter_service)})"
47
- end
48
- say entry_info
42
+ entry_presenter = entry.presenter
43
+ puts entry_presenter.formatted_description_with_index(index: index)
49
44
  end
45
+ rescue ActiveModel::ValidationError
46
+ puts apply_theme(errors(entry_group), theme_color: presenter.color_theme.error)
47
+ end
48
+
49
+ def errors(model)
50
+ model.errors.full_messages.join(', ')
50
51
  end
51
52
 
52
- def entry_errors(entry)
53
- entry.errors.full_messages.join(', ')
53
+ def presenter
54
+ @presenter ||= entry_group.presenter
54
55
  end
55
56
  end
56
57
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'message'
4
+
5
+ module Dsu
6
+ module Views
7
+ module Shared
8
+ class Error < Message
9
+ def initialize(messages:, header: nil, options: {})
10
+ options = { header: header, output_stream: $stderr }.merge(options)
11
+
12
+ super(messages: messages, message_type: :error, options: options)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'message'
4
+
5
+ module Dsu
6
+ module Views
7
+ module Shared
8
+ class Info < Message
9
+ def initialize(messages:, header: nil, options: {})
10
+ options = { header: header, output_stream: $stdout }.merge(options)
11
+
12
+ super(messages: messages, message_type: :info, options: options)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/color_theme'
4
+
5
+ module Dsu
6
+ module Views
7
+ module Shared
8
+ class Message
9
+ include Support::ColorThemable
10
+
11
+ MESSAGE_TYPES = %i[error info success warning].freeze
12
+
13
+ def initialize(messages:, message_type:, options: {})
14
+ messages = [messages] unless messages.is_a?(Array)
15
+ messages = messages.select(&:present?)
16
+
17
+ validate_arguments!(messages, message_type, options)
18
+
19
+ @messages = messages
20
+ @message_type = message_type
21
+ @options = options || {}
22
+ @message_color = color_theme.public_send(message_type)
23
+ @header = options[:header]
24
+ @ordered_list = options.fetch(:ordered_list, true)
25
+ end
26
+
27
+ def render
28
+ output_stream.puts to_s
29
+ end
30
+
31
+ def to_s
32
+ return if messages.empty?
33
+
34
+ strings = []
35
+
36
+ strings << apply_theme(header, theme_color: color_theme.header) if header.present?
37
+
38
+ strings << if messages.one?
39
+ apply_theme(messages[0], theme_color: message_color)
40
+ else
41
+ messages.each_with_index.map do |message, index|
42
+ message = "#{index + 1}. #{message}" if ordered_list?
43
+ apply_theme(message, theme_color: message_color)
44
+ end
45
+ end
46
+
47
+ strings.flatten.join("\n")
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :messages, :message_color, :message_type, :header, :options
53
+
54
+ def color_theme
55
+ @color_theme ||= begin
56
+ theme_name = options.fetch(:theme_name, Models::Configuration.new.theme_name)
57
+ Models::ColorTheme.find(theme_name: theme_name)
58
+ end
59
+ end
60
+
61
+ def ordered_list?
62
+ @ordered_list
63
+ end
64
+
65
+ def output_stream
66
+ @output_stream ||= options.fetch(:output_stream, $stdout)
67
+ end
68
+
69
+ def validate_arguments!(messages, message_type, options)
70
+ raise ArgumentError, 'messages is empty' if messages.empty?
71
+ unless Models::ColorTheme::DEFAULT_THEME_COLORS.key?(message_type)
72
+ raise ArgumentError, 'message_type is not a valid message type'
73
+ end
74
+ raise ArgumentError, 'options is nil' if options.nil?
75
+
76
+ %i[\[\] fetch].each do |method|
77
+ next if options.respond_to?(method)
78
+
79
+ raise ArgumentError, "options does not respond to :#{method}"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error'
4
+
5
+ module Dsu
6
+ module Views
7
+ module Shared
8
+ class ModelErrors < Error
9
+ def initialize(model:, options: {})
10
+ raise ArgumentError, 'model is nil' if model.nil?
11
+ raise ArgumentError, "model is the wrong object type: \"#{model}\"" unless model.is_a?(ActiveModel::Model)
12
+
13
+ header = options[:header] || 'The following ERRORS were encountered; changes could not be saved:'
14
+ super(messages: model.errors.full_messages, header: header, options: options)
15
+
16
+ @model = model
17
+ end
18
+
19
+ def render
20
+ return if model.valid?
21
+
22
+ super
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :model
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'message'
4
+
5
+ module Dsu
6
+ module Views
7
+ module Shared
8
+ class Success < Message
9
+ def initialize(messages:, header: nil, options: {})
10
+ options = { header: header, output_stream: $stdout }.merge(options)
11
+
12
+ super(messages: messages, message_type: :success, options: options)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'message'
4
+
5
+ module Dsu
6
+ module Views
7
+ module Shared
8
+ class Warning < Message
9
+ def initialize(messages:, header: nil, options: {})
10
+ options = { header: header, output_stream: $stdout }.merge(options)
11
+
12
+ super(messages: messages, message_type: :warning, options: options)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end