dsu 1.2.1 → 2.0.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -21
- data/Gemfile.lock +7 -7
- data/README.md +28 -35
- data/bin/console +23 -1
- data/bin/dsu +3 -0
- data/bin/setup +14 -3
- data/exe/dsu +23 -1
- data/exe/dsu_migrate.rb +43 -0
- data/lib/core/ruby/color_theme_colors.rb +16 -0
- data/lib/core/ruby/color_theme_mode.rb +42 -0
- data/lib/core/ruby/not_today.rb +7 -0
- data/lib/core/ruby/wrap_and_join.rb +31 -0
- data/lib/dsu/base_cli.rb +19 -23
- data/lib/dsu/cli.rb +47 -37
- data/lib/dsu/command_services/add_entry_service.rb +10 -21
- data/lib/dsu/crud/json_file.rb +139 -0
- data/lib/dsu/crud/raw_json_file.rb +51 -0
- data/lib/dsu/env.rb +21 -0
- data/lib/dsu/migration/service.rb +196 -0
- data/lib/dsu/migration/version.rb +7 -0
- data/lib/dsu/models/color_theme.rb +270 -0
- data/lib/dsu/models/configuration.rb +160 -0
- data/lib/dsu/models/entry.rb +6 -2
- data/lib/dsu/models/entry_group.rb +143 -42
- data/lib/dsu/models/migration_version.rb +48 -0
- data/lib/dsu/presenters/base_presenter.rb +32 -0
- data/lib/dsu/presenters/color_theme_presenter.rb +50 -0
- data/lib/dsu/presenters/color_theme_show_presenter.rb +49 -0
- data/lib/dsu/presenters/configuration_presenter.rb +45 -0
- data/lib/dsu/presenters/entry_group_presenter.rb +35 -0
- data/lib/dsu/presenters/entry_presenter.rb +25 -0
- data/lib/dsu/services/color_theme/hydrator_service.rb +42 -0
- data/lib/dsu/services/configuration/hydrator_service.rb +42 -0
- data/lib/dsu/services/entry/hydrator_service.rb +33 -0
- data/lib/dsu/services/entry_group/editor_service.rb +107 -0
- data/lib/dsu/services/entry_group/hydrator_service.rb +37 -0
- data/lib/dsu/services/migration_version/hydrator_service.rb +36 -0
- data/lib/dsu/services/stderr_redirector_service.rb +27 -0
- data/lib/dsu/services/temp_file/reader_service.rb +33 -0
- data/lib/dsu/services/temp_file/writer_service.rb +35 -0
- data/lib/dsu/subcommands/base_subcommand.rb +14 -0
- data/lib/dsu/subcommands/config.rb +92 -32
- data/lib/dsu/subcommands/edit.rb +3 -3
- data/lib/dsu/subcommands/list.rb +70 -93
- data/lib/dsu/subcommands/theme.rb +159 -0
- data/lib/dsu/support/ask.rb +14 -19
- data/lib/dsu/support/color_themable.rb +34 -0
- data/lib/dsu/support/command_help_colorizeable.rb +27 -0
- data/lib/dsu/support/command_hookable.rb +60 -0
- data/lib/dsu/support/command_options/dsu_times.rb +32 -21
- data/lib/dsu/support/command_options/time.rb +7 -1
- data/lib/dsu/support/command_options/time_mneumonic.rb +7 -1
- data/lib/dsu/support/descriptable.rb +6 -4
- data/lib/dsu/support/entry_group_viewable.rb +28 -4
- data/lib/dsu/support/fileable.rb +94 -0
- data/lib/dsu/support/presentable.rb +11 -0
- data/lib/dsu/support/subcommand_help_colorizeable.rb +27 -0
- data/lib/dsu/support/time_comparable.rb +19 -0
- data/lib/dsu/support/time_formatable.rb +12 -0
- data/lib/dsu/support/times_sortable.rb +48 -14
- data/lib/dsu/support/utils.rb +11 -0
- data/lib/dsu/validators/color_theme_validator.rb +74 -0
- data/lib/dsu/validators/entries_validator.rb +4 -8
- data/lib/dsu/validators/version_validator.rb +29 -0
- data/lib/dsu/version.rb +2 -1
- data/lib/dsu/views/color_theme/index.rb +62 -0
- data/lib/dsu/views/color_theme/show.rb +106 -0
- data/lib/dsu/views/configuration/show.rb +41 -0
- data/lib/dsu/views/entry_group/edit.rb +3 -5
- data/lib/dsu/views/entry_group/shared/no_entries_to_display.rb +41 -0
- data/lib/dsu/views/entry_group/show.rb +16 -15
- data/lib/dsu/views/shared/error.rb +17 -0
- data/lib/dsu/views/shared/info.rb +17 -0
- data/lib/dsu/views/shared/message.rb +85 -0
- data/lib/dsu/views/shared/model_errors.rb +31 -0
- data/lib/dsu/views/shared/success.rb +17 -0
- data/lib/dsu/views/shared/warning.rb +17 -0
- data/lib/dsu.rb +22 -1
- data/lib/seed_data/themes/cherry.json +79 -0
- data/lib/seed_data/themes/default.json +79 -0
- data/lib/seed_data/themes/lemon.json +79 -0
- data/lib/seed_data/themes/matrix.json +79 -0
- data/lib/seed_data/themes/whiteout.json +79 -0
- metadata +70 -25
- data/lib/dsu/core/ruby/not_today.rb +0 -11
- data/lib/dsu/services/ai/tense_translator_service.rb +0 -63
- data/lib/dsu/services/configuration_loader_service.rb +0 -55
- data/lib/dsu/services/entry_group_deleter_service.rb +0 -31
- data/lib/dsu/services/entry_group_editor_service.rb +0 -96
- data/lib/dsu/services/entry_group_hydrator_service.rb +0 -43
- data/lib/dsu/services/entry_group_reader_service.rb +0 -36
- data/lib/dsu/services/entry_group_writer_service.rb +0 -46
- data/lib/dsu/services/entry_hydrator_service.rb +0 -35
- data/lib/dsu/services/temp_file_reader_service.rb +0 -31
- data/lib/dsu/services/temp_file_writer_service.rb +0 -33
- data/lib/dsu/support/colorable.rb +0 -14
- data/lib/dsu/support/configurable.rb +0 -15
- data/lib/dsu/support/configuration.rb +0 -112
- data/lib/dsu/support/entry_group_fileable.rb +0 -49
- data/lib/dsu/support/entry_group_loadable.rb +0 -49
- data/lib/dsu/support/folder_locations.rb +0 -21
- data/lib/dsu/support/say.rb +0 -40
- data/lib/dsu/views/edited_entries/shared/errors.rb +0 -39
- 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
|