dsu 0.1.0.alpha.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared/messages'
4
+
5
+ module Dsu
6
+ module Views
7
+ module EditedEntries
8
+ module Shared
9
+ class Errors
10
+ def initialize(edited_entries:, options: {})
11
+ raise ArgumentError, 'edited_entries is nil' if edited_entries.nil?
12
+ raise ArgumentError, 'edited_entries is the wrong object type' unless edited_entries.is_a?(Array)
13
+ unless edited_entries.all?(Models::EditedEntry)
14
+ raise ArgumentError, 'edited_entries elements are the wrong object type'
15
+ end
16
+ raise ArgumentError, 'options is nil' if options.nil?
17
+ raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
18
+
19
+ @edited_entries = edited_entries
20
+ @options = options || {}
21
+ @header = options[:header] || 'The following ERRORS were encountered; these changes were not saved:'
22
+ end
23
+
24
+ def render
25
+ return if edited_entries.empty?
26
+ return if edited_entries.all?(&:valid?)
27
+
28
+ messages = edited_entries.map { |edited_entry| edited_entry.errors.full_messages }.flatten
29
+ Views::Shared::Messages.new(messages: messages, message_type: :error, options: { header: header }).render
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :edited_entries, :header, :options
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -3,8 +3,6 @@
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'
8
6
  require_relative '../../support/time_formatable'
9
7
 
10
8
  module Dsu
@@ -25,46 +23,43 @@ module Dsu
25
23
  @options = options || {}
26
24
  end
27
25
 
28
- def call
29
- # Just in case the entry group is invalid, we'll
30
- # validate it before displaying it.
31
- entry_group.validate!
32
- render_entry_group!
33
- rescue ActiveModel::ValidationError
34
- puts "Error(s) encountered: #{entry_group.errors.full_messages}"
35
- raise
26
+ def render
27
+ puts render_as_string
36
28
  end
37
- alias render call
38
-
39
- private
40
29
 
41
- attr_reader :entry_group, :options
30
+ def render_as_string
31
+ # Just in case the entry group is invalid, we'll validate it before displaying it.
32
+ entry_group.validate!
42
33
 
43
- def render_entry_group!
44
- say "# Editing DSU Entries for #{formatted_time(time: entry_group.time)}"
45
34
  # TODO: Display entry group entries from the previous DSU date so they can be
46
35
  # easily copied over; or, add them to the current entry group entries below as
47
- # a "# [+|a|add] <entry group from previous DSU entry description>" (e.g. commented
48
- # out) by default?
49
- say ''
50
- say '# [SHA/COMMAND] [DESCRIPTION]'
36
+ # a "# [+|a|add] <entry group from previous DSU entry description>"
37
+ # (e.g. commented out) by default?
38
+
39
+ <<~EDIT_VIEW
40
+ # Editing DSU Entries for #{formatted_time(time: entry_group.time)}
41
+ # [ENTRY DESCRIPTION]
51
42
 
52
- entry_group.entries.each do |entry|
53
- say "#{entry.uuid} #{entry.description.strip}"
54
- end
43
+ #{entry_group_entry_lines.each(&:strip).join("\n")}
44
+
45
+ # INSTRUCTIONS:
46
+ # ADD a DSU entry: type an ENTRY DESCRIPTION on a new line.
47
+ # EDIT a DSU entry: change the existing ENTRY DESCRIPTION.
48
+ # DELETE a DSU entry: delete the ENTRY DESCRIPTION.
49
+ # NOTE: deleting all of the ENTRY DESCRIPTIONs will delete the entry group file;
50
+ # this is preferable if this is what you want to do :)
51
+ # REORDER a DSU entry: reorder the ENTRY DESCRIPTIONs in order preference.
52
+ #
53
+ # *** When you are done, save and close your editor ***
54
+ EDIT_VIEW
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :entry_group, :options
55
60
 
56
- say ''
57
- say '# INSTRUCTIONS:'
58
- say '# ADD a DSU entry: use one of the following commands: [+|a|add] ' \
59
- 'followed by the description.'
60
- say '# EDIT a DSU entry: change the description.'
61
- say '# DELETE a DSU entry: delete the entry or replace the sha with one ' \
62
- 'of the following commands: [-|d|delete].'
63
- say '# NOTE: deleting all the entries will delete the entry group file; '
64
- say '# this is preferable if this is what you want to do :)'
65
- say '# REORDER a DSU entry: reorder the DSU entries in order preference.'
66
- say '#'
67
- say '# *** When you are done, save and close your editor ***'
61
+ def entry_group_entry_lines
62
+ entry_group.entries.map(&:description)
68
63
  end
69
64
  end
70
65
  end
@@ -26,7 +26,7 @@ module Dsu
26
26
  end
27
27
 
28
28
  def call
29
- render_entry_group!
29
+ render!
30
30
  end
31
31
  alias render call
32
32
 
@@ -34,18 +34,24 @@ module Dsu
34
34
 
35
35
  attr_reader :entry_group, :options
36
36
 
37
- def render_entry_group!
37
+ def render!
38
38
  say formatted_time(time: entry_group.time), HIGHLIGHT
39
39
  say('(no entries available for this day)') 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)}. #{entry.uuid}"
42
+ prefix = "#{format('%03s', index + 1)}. "
43
43
  description = colorize_string(string: entry.description, mode: :bold)
44
44
  entry_info = "#{prefix} #{description}"
45
- entry_info = "#{entry_info} (validation failed)" unless entry.valid?
45
+ unless entry.valid?
46
+ entry_info = "#{entry_info} (validation failed: #{entry_errors(entry_group_deleter_service)})"
47
+ end
46
48
  say entry_info
47
49
  end
48
50
  end
51
+
52
+ def entry_errors(entry)
53
+ entry.errors.full_messages.join(', ')
54
+ end
49
55
  end
50
56
  end
51
57
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../support/colorable'
4
+ require_relative '../../support/say'
5
+
6
+ module Dsu
7
+ module Views
8
+ module Shared
9
+ class Messages
10
+ include Support::Colorable
11
+ include Support::Say
12
+
13
+ MESSAGE_TYPES = %i[error info success warning].freeze
14
+
15
+ def initialize(messages:, message_type:, options: {})
16
+ messages = [messages] unless messages.is_a?(Array)
17
+
18
+ validate_arguments!(messages, message_type, options)
19
+
20
+ @messages = messages.select(&:present?)
21
+ @message_type = message_type
22
+ # We've inluded Support::Colorable, so simply upcase the message_type
23
+ # and convert it to a symbol; this will equate to the color we want.
24
+ @message_color = self.class.const_get(message_type.to_s.upcase)
25
+ @options = options || {}
26
+ @header = options[:header]
27
+ end
28
+
29
+ def render
30
+ return if messages.empty?
31
+
32
+ say header, message_color if header.present?
33
+
34
+ messages.each_with_index do |message, index|
35
+ say "#{index + 1}. #{message}", message_color
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :messages, :message_color, :message_type, :header, :options
42
+
43
+ def validate_arguments!(messages, message_type, options)
44
+ raise ArgumentError, 'messages is nil' if messages.nil?
45
+ raise ArgumentError, 'messages is the wrong object type' unless messages.is_a?(Array)
46
+ raise ArgumentError, 'messages elements are the wrong object type' unless messages.all?(String)
47
+ raise ArgumentError, 'message_type is nil' if message_type.nil?
48
+ raise ArgumentError, 'message_type is the wrong object type' unless message_type.is_a?(Symbol)
49
+ raise ArgumentError, 'message_type is not a valid message type' unless MESSAGE_TYPES.include?(message_type)
50
+ raise ArgumentError, 'options is nil' if options.nil?
51
+ raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
data/lib/dsu.rb CHANGED
@@ -5,6 +5,10 @@ require 'active_support/core_ext/object/blank'
5
5
  require 'active_support/core_ext/hash/indifferent_access'
6
6
  require 'active_support/core_ext/numeric/time'
7
7
 
8
+ Dir.glob("#{__dir__}/lib/core/**/*.rb").each do |file|
9
+ require file
10
+ end
11
+
8
12
  Dir.glob("#{__dir__}/dsu/**/*.rb").each do |file|
9
13
  require file
10
14
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dsu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha.5
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gene M. Angelo, Jr.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-12 00:00:00.000000000 Z
11
+ date: 2023-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -158,6 +158,7 @@ files:
158
158
  - lib/dsu/services/entry_group_reader_service.rb
159
159
  - lib/dsu/services/entry_group_writer_service.rb
160
160
  - lib/dsu/services/entry_hydrator_service.rb
161
+ - lib/dsu/services/stdout_redirector_service.rb
161
162
  - lib/dsu/services/temp_file_reader_service.rb
162
163
  - lib/dsu/services/temp_file_writer_service.rb
163
164
  - lib/dsu/subcommands/config.rb
@@ -165,9 +166,6 @@ files:
165
166
  - lib/dsu/subcommands/list.rb
166
167
  - lib/dsu/support/ask.rb
167
168
  - lib/dsu/support/colorable.rb
168
- - lib/dsu/support/commander/command.rb
169
- - lib/dsu/support/commander/command_help.rb
170
- - lib/dsu/support/commander/subcommand.rb
171
169
  - lib/dsu/support/configuration.rb
172
170
  - lib/dsu/support/descriptable.rb
173
171
  - lib/dsu/support/entry_group_fileable.rb
@@ -175,15 +173,17 @@ files:
175
173
  - lib/dsu/support/entry_group_viewable.rb
176
174
  - lib/dsu/support/field_errors.rb
177
175
  - lib/dsu/support/folder_locations.rb
178
- - lib/dsu/support/interactive/cli.rb
179
176
  - lib/dsu/support/say.rb
180
177
  - lib/dsu/support/time_formatable.rb
181
178
  - lib/dsu/support/times_sortable.rb
179
+ - lib/dsu/validators/description_validator.rb
182
180
  - lib/dsu/validators/entries_validator.rb
183
181
  - lib/dsu/validators/time_validator.rb
184
182
  - lib/dsu/version.rb
183
+ - lib/dsu/views/edited_entries/shared/errors.rb
185
184
  - lib/dsu/views/entry_group/edit.rb
186
185
  - lib/dsu/views/entry_group/show.rb
186
+ - lib/dsu/views/shared/messages.rb
187
187
  - sig/dsu.rbs
188
188
  homepage: https://github.com/gangelo/dsu
189
189
  licenses:
@@ -204,9 +204,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
204
204
  version: 3.0.1
205
205
  required_rubygems_version: !ruby/object:Gem::Requirement
206
206
  requirements:
207
- - - ">"
207
+ - - ">="
208
208
  - !ruby/object:Gem::Version
209
- version: 1.3.1
209
+ version: '0'
210
210
  requirements: []
211
211
  rubygems_version: 3.3.22
212
212
  signing_key:
@@ -1,130 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'command_help'
4
- require_relative 'subcommand'
5
-
6
- module Dsu
7
- module Support
8
- module Commander
9
- # https://www.toptal.com/ruby/ruby-dsl-metaprogramming-guide
10
- module Command
11
- class << self
12
- def included(base)
13
- base.extend ClassMethods
14
- base.engine.command_namespace to_command_namespace_symbol base.name
15
- base.engine.command_prompt base.engine.command_namespace
16
- binding.pry
17
- base.singleton_class.delegate :command_namespace, :command_namespaces, :commands,
18
- :command_add, :command_subcommand_add, :command_prompt, :command_parent,
19
- :help, to: :engine
20
- end
21
-
22
- private
23
-
24
- def to_command_namespace_symbol(namespace, join_token: '_')
25
- namespace.delete(':').split(/(?=[A-Z])/).join(join_token).downcase
26
- end
27
- end
28
-
29
- module ClassMethods
30
- def command_subcommand_create(command_parent:)
31
- new.tap do |subcommand|
32
- subcommand.extend Subcommand
33
- subcommand.command_parent command_parent
34
- end
35
- end
36
-
37
- def engine
38
- @engine ||= Engine.new(owning_command: self)
39
- end
40
-
41
- class Engine
42
- include CommandHelp
43
-
44
- attr_reader :owning_command
45
-
46
- def initialize(owning_command:)
47
- @owning_command = owning_command
48
- end
49
-
50
- def command_add(command:, desc:, long_desc: nil, options: {}, commands: [])
51
- self.commands[command_namespace] ||= {}
52
- self.commands[command_namespace][command] = {
53
- desc: desc,
54
- long_desc: long_desc,
55
- options: options,
56
- commands: commands,
57
- help: command_help_for(command: command, desc: desc,
58
- long_desc: long_desc, options: options, commands: commands)
59
- }
60
- end
61
-
62
- def command_subcommand_add(subcommand, command_parent: nil)
63
- command_parent ||= @owning_command
64
- subcommand = subcommand.command_subcommand_create command_parent: command_parent
65
- commands[command_namespace] ||= {}
66
- binding.pry
67
- subcommand.command_namespaces.each_with_index do |namespace, index|
68
- next if index.zero?
69
-
70
- target = commands.dig(*subcommand.command_namespaces[1..index])
71
- target ||= commands.dig(*subcommand.command_namespaces[0..index - 1])
72
- target[namespace] ||= {}
73
- end
74
- commands.dig(*subcommand.command_namespaces[0..])[subcommand.command_namespaces.last] = subcommand
75
-
76
- # subcommand.commands.each do |command_namespace, command|
77
- # command.each do |subcommand_command, data|
78
- # commands[self.command_namespace][command_namespace] ||= {}
79
- # commands[self.command_namespace][command_namespace][subcommand_command] = {
80
- # desc: data[:desc],
81
- # long_desc: data[:long_desc],
82
- # options: data[:options],
83
- # commands: data[:commands],
84
- # help: command_help_for(command: subcommand_command, namespaces: subcommand.command_namespaces, desc: data[:desc],
85
- # long_desc: data[:long_desc], options: data[:options], commands: data[:commands])
86
- # }
87
- # end
88
- # end
89
- end
90
-
91
- def command_namespaces(namespaces = [])
92
- command_parent&.command_namespaces(namespaces)
93
-
94
- namespaces << command_namespace
95
- namespaces
96
- end
97
-
98
- def command_namespace(namespace = nil)
99
- return @command_namespace || name if namespace.nil?
100
-
101
- @command_namespace = namespace
102
- end
103
-
104
- def command_prompt(value = nil)
105
- return @command_prompt || name if value.nil?
106
-
107
- @command_prompt = value
108
- end
109
-
110
- def command_parent(parent = nil)
111
- return @command_parent if parent.nil?
112
-
113
- @command_parent = parent
114
- end
115
-
116
- def commands
117
- @commands ||= {}
118
- end
119
-
120
- def help
121
- commands.each do |_command, command_data|
122
- puts "#{command_namespaces.join(' ')} #{command_data[:help]}"
123
- end
124
- end
125
- end
126
- end
127
- end
128
- end
129
- end
130
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dsu
4
- module Support
5
- module Commander
6
- module CommandHelp
7
- private
8
-
9
- # rubocop:disable Lint/UnusedMethodArgument
10
- def command_help_for(command:, desc:, namespaces: nil, long_desc: nil, options: {}, commands: [])
11
- namespaces ||= command_namespaces
12
- help =
13
- <<~HELP
14
- #{namespaces&.join(' ')} #{command}#{' [OPTIONS]' if options&.any?} - #{desc}
15
- #{'OPTIONS:' if options&.any?}
16
- #{options_help_for options}
17
- #{'OPTION ALIASES:' if any_option_aliases_for?(options)}
18
- #{options_aliases_help_for options}
19
- #{'---' unless long_desc.blank?}
20
- #{long_desc}
21
- HELP
22
- help.gsub(/\n{2,}/, "\n")
23
- end
24
- # rubocop:enable Lint/UnusedMethodArgument
25
-
26
- def options_help_for(options)
27
- return [] if options.blank?
28
-
29
- options.map do |option, data|
30
- type = option_to_a(data[:type])&.join(' | ')
31
- type = :boolean if type.blank?
32
- "#{option} <#{type}>, default: #{data[:default]}"
33
- end.join("\n")
34
- end
35
-
36
- def options_aliases_help_for(options)
37
- return unless any_option_aliases_for?(options)
38
-
39
- options.filter_map do |option, data|
40
- aliases = option_to_a(data[:aliases])&.join(' | ')
41
- <<~HELP
42
- #{option} aliases: [#{aliases}]
43
- HELP
44
- end.join("\n")
45
- end
46
-
47
- def any_option_aliases_for?(options)
48
- return false if options.blank?
49
-
50
- options.keys.any? { |key| options.dig(key, :aliases).any? }
51
- end
52
-
53
- def option_to_a(option)
54
- return [] if option.blank?
55
- return option if option.is_a? Array
56
-
57
- [option]
58
- end
59
- end
60
- end
61
- end
62
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dsu
4
- module Support
5
- module Commander
6
- # Subcommands should extend this module once they are instantiated
7
- # so that the Subcommand instance has access to all necessary
8
- # class methods for this subcommand to work.
9
- module Subcommand
10
- class << self
11
- def extended(mod)
12
- mod.singleton_class.delegate :command_namespace, :commands,
13
- :command_add, :command_subcommand_add, :command_prompt, :help, to: mod.class
14
- end
15
- end
16
-
17
- # Subcommand-specific instance methods.
18
- #
19
- # Define Subcommand-specific method equivalents of the Command class
20
- # methods needed to make this Subcommand instance unique.
21
-
22
- # def command_namespace(namespace = nil)
23
- # return @command_namespace || name if namespace.nil?
24
-
25
- # @command_namespace = namespace
26
- # end
27
-
28
- # Subcommands can be used by any Command, so the :command_parent needs
29
- # to be unique to this Subcommand instance.
30
- def command_parent(parent = nil)
31
- return @command_prompt if parent.nil?
32
-
33
- @command_prompt = parent
34
- end
35
-
36
- def command_namespaces(namespaces = [])
37
- command_parent&.command_namespaces(namespaces)
38
-
39
- namespaces << command_namespace
40
- namespaces
41
- end
42
- end
43
- end
44
- end
45
- end
@@ -1,161 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../ask'
4
- require_relative '../colorable'
5
- require_relative '../say'
6
-
7
- module Dsu
8
- module Interactive
9
- class Cli
10
- include Support::Colorable
11
- include Support::Ask
12
- include Support::Say
13
-
14
- BACK_COMMANDS = %w[b].freeze
15
- EXIT_COMMANDS = %w[x].freeze
16
- HELP_COMMANDS = %w[?].freeze
17
- PROMPT_TOKEN = '>'
18
-
19
- attr_reader :name, :parent, :prompt
20
-
21
- def initialize(name:, parent: nil, **options)
22
- @name = name
23
- @parent = parent
24
- @prompt = options[:prompt]
25
- end
26
-
27
- # Starts our interactive loop.
28
- def start
29
- help
30
- process_commands
31
- end
32
-
33
- def process(command:)
34
- if command.cancelled?
35
- nil
36
- elsif help?(command.command)
37
- help
38
- elsif back_or_exit?(command.command)
39
- parent&.help
40
- else
41
- unrecognized_command command.command
42
- end
43
- end
44
-
45
- # Dispays the full help; header and body.
46
- def help
47
- help_header
48
- help_body
49
- end
50
-
51
- private
52
-
53
- # This is our interaction loop. Commands that are NOT help or
54
- # back or exit commands are yielded to the subclass to execute. Help
55
- # commands simply display help; back (or exit) commands transfer control
56
- # back to the parent cli (if parent? is true) or exits the current
57
- # cli (if parent? is false) respectfully.
58
- def process_commands
59
- loop do
60
- command = wrap_command(ask)
61
- process(command: command)
62
- next if command.cancelled?
63
- break if back_or_exit?(command.command)
64
- end
65
- say 'Done.', ABORTED unless parent?
66
- end
67
-
68
- def wrap_command(command)
69
- Struct.new(:command, :args, :cancelled, keyword_init: true) do
70
- def cancelled?
71
- cancelled
72
- end
73
-
74
- def cancelled!
75
- self[:cancelled] = true
76
- end
77
- end.new(
78
- command: command.split[0],
79
- args: command.split[1..],
80
- cancelled: false
81
- )
82
- end
83
-
84
- # This is the full prompt that needs to be displayed that includes
85
- # all parent prompts, right down to the current prompt.
86
- def full_prompt
87
- prompts = full_prompt_build prompts: []
88
- prompt_token = "#{PROMPT_TOKEN} "
89
- "#{prompts.join prompt_token}#{prompt_token}"
90
- end
91
-
92
- def parent?
93
- !parent.nil?
94
- end
95
-
96
- def back?(command)
97
- back_commands.include? command
98
- end
99
-
100
- def back_commands
101
- @back_commands ||= BACK_COMMANDS
102
- end
103
-
104
- def exit?(command)
105
- exit_commands.include? command
106
- end
107
-
108
- def exit_commands
109
- @exit_commands ||= EXIT_COMMANDS
110
- end
111
-
112
- def back_or_exit?(command)
113
- (back_commands + exit_commands).include? command
114
- end
115
-
116
- def help?(command)
117
- help_commands.include? command
118
- end
119
-
120
- # Returns what are considered to be commands associated with
121
- # displaying help.
122
- def help_commands
123
- @help_commands ||= HELP_COMMANDS
124
- end
125
-
126
- # Displays the help header; override this if you want to customize
127
- # your own help header in your subclass.
128
- def help_header
129
- say "#{name} Help", HIGHLIGHT
130
- say '---', HIGHLIGHT
131
- end
132
-
133
- # Override this in your subclass and call super AFTER you've
134
- # displayed your subclass' help body.
135
- def help_body
136
- say "[#{HELP_COMMANDS.join(' | ')}] Display help", HIGHLIGHT
137
- say "[#{BACK_COMMANDS.join(' | ')}] Go back", HIGHLIGHT if parent?
138
- say "[#{EXIT_COMMANDS.join(' | ')}] Exit", HIGHLIGHT unless parent?
139
- end
140
-
141
- # This simply outputs our prompt and accepts user input.
142
- def ask
143
- super full_prompt
144
- end
145
-
146
- def unrecognized_command(command)
147
- say "Unrecognized command (\"#{command}\"). Try again.", ERROR
148
- end
149
-
150
- # Builds the full prompt to be used which amounts to:
151
- # <parent cli prompt> PROMPT_TOKEN <child cli 1 prompt>
152
- # PROMPT_TOKEN <child 2 cli prompt> ...
153
- def full_prompt_build(prompts:)
154
- parent.send(:full_prompt_build, prompts: prompts) if parent?
155
-
156
- prompts << prompt
157
- prompts.flatten
158
- end
159
- end
160
- end
161
- end