dsu 0.1.0.alpha.4 → 0.1.0.alpha.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c27f3abb1daf923976963a1612f08258fd8a0460d672c1674dc2c149aa69ed03
4
- data.tar.gz: 8611ccb51518e24f9497ffc0c02a7b1dd567321419728f727969718620da5b59
3
+ metadata.gz: d8563782a0b4d22e374313aa3373b38e62dec995a6f6d996eaa5fa9c58b0d1cb
4
+ data.tar.gz: 65ebebe762dac44300a6d163c03275eaffd66018a83f9836b3f12cb6333569ad
5
5
  SHA512:
6
- metadata.gz: d1b1bcd4f9222bf2629ffa0d02434b429280f20e760676f2cde2eaca40860f56952995ce7a69628981cc0f7caf941a48c07cca91a9c1f96b4334e872d5a7374a
7
- data.tar.gz: 0436e614b146dbfd71ca098cc699d7b6de99cafe47cfbad8141ac6268853b6f7d68285d8222053796bb2db729cf86c14e03e3fa3f2dca97554fd78998449fbbe
6
+ metadata.gz: a412bf735ce124b6df3bb419a1ed925b6b9aaf6cc7e8fdb13773fd883886119d92c03cb4bc68633d57bcfd293c274db16d5504200790f9be65d5d3d3f3e5e21e
7
+ data.tar.gz: bf7ac4a28753385155d6e0cd7f1694723deaa3ad0d0e41205f63446c78a2a8b0b09d42f4e5557d3c3aef4befa8a6d46e568d308a1d9fb6c9a0f4110d301c025d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.1.0.alpha.5] - 2023-05-12
2
+ * Changes
3
+ - `dsu edit SUBCOMMAND` will now allow editing of an entry group for a date that does not yet exist. This will allow you to add entries in the editor using `+|a|add DESCRIPTION`. Be sure to follow the instructions in the editor when editing entry group entries.
4
+ - `dsu edit SUBCOMMAND` will gracefully display an error if the entry sha (Entry#uuid) or entry discription (Entry#description) are not unique. In this case, the entry will not be added to the entry group.
5
+ NOTE: Not all edge cases are being handled currently by `dsu edit SUBCOMMAND`.
6
+ - `dsu add OPTION` will raise an error if the entry discription (Entry#description) are not unique. This will be handled gracefully in a future release.
1
7
  ## [0.1.0.alpha.4] - 2023-05-09
2
8
  * Changes
3
9
  - Gemfile gemspec description changes.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dsu (0.1.0.alpha.4)
4
+ dsu (0.1.0.alpha.5)
5
5
  activesupport (~> 7.0, >= 7.0.4)
6
6
  colorize (~> 0.8.1)
7
7
  deco_lite (~> 1.3)
data/README.md CHANGED
@@ -19,7 +19,7 @@ After installation (`gem install dsu`), the first thing you may want to do is ru
19
19
  ```shell
20
20
  #=>
21
21
  Commands:
22
- dsu add, -a [OPTIONS] DESCRIPTION # Adds a DSU entry having DESCRIPTION to the date associated with the given OPTION
22
+ dsu add, -a [OPTIONS] DESCRIPTION # Adds a DSU entry...
23
23
  dsu config, -c SUBCOMMAND # Manage configuration...
24
24
  dsu edit, -e SUBCOMMAND # Edit DSU entries...
25
25
  dsu help [COMMAND] # Describe available...
@@ -35,7 +35,7 @@ The next thing you may want to do is `add` some DSU activities (entries) for a p
35
35
  ## Adding DSU Entries
36
36
  `dsu add [OPTIONS] DESCRIPTION`
37
37
 
38
- Adding DSU entry using this command will _add_ the DSU entry for the given day (or date, `-d`), and also _display_ the given day's (or date's, `-d`) DSU entries, as well as the DSU entries for the previous day relative to the given day or date (`-d`).
38
+ Adding DSU entry using this command will _add_ the DSU entry for the given day (or date, `-d`), and also _display_ the given day's (or date's, `-d`) DSU entries, as well as the DSU entries for the previous day relative to the given day or date (`-d`). *NOTE: You cannot add duplicate entry group entries; that is, the entry DESCRIPTION needs to be unique within an entry group.*
39
39
 
40
40
  ### Today
41
41
  If you need to add a DSU entry to the current day (today), you can use the `-n`|`--today` option. Today (`-n`) is the default; therefore, the `-n` flag is optional when adding DSU entries for the current day:
@@ -97,7 +97,7 @@ Friday, (Yesterday) 2023-05-05
97
97
  ```
98
98
  ## Editing DSU Entries
99
99
 
100
- You can edit DSU entry groups by date. `dsu` will allow you to edit a DSU entry group using the `dsu edit SUBCOMMAND` date (today|tomorrow|yesterday|date DATE) you specify. `dsu edit` will open your DSU entry group entries in your editor, where you'll be able to perform editing functions against one or all of the entries.
100
+ You can edit DSU entry groups by date. `dsu` will allow you to edit a DSU entry group using the `dsu edit SUBCOMMAND` date (today|tomorrow|yesterday|date DATE) you specify. `dsu edit` will open your DSU entry group entries in your editor, where you'll be able to perform editing functions against one or all of the entries. If no entries exist in the entry group, you can add entries using any of the the *add* (`+|a|add`) editor commands, followed by the entry description. *NOTE: you cannot add duplicate entries; that is, the entry SHA and DESCRIPTION need to be unique within an entry group. Non-unique entries will not be added to the entry group.*
101
101
 
102
102
  Note: See the "[Customizing the `dsu` Configuration File](#customizing-the-dsu-configuration-file)"" section to configure `dsu` to use the editor of your choice.
103
103
 
@@ -245,6 +245,9 @@ DATE may be any date string that can be parsed using `Time.parse`. Consequently,
245
245
  ## WIP Notes
246
246
  This gem is in development (alpha release).
247
247
 
248
+ - Not all edge cases are being handled currently by `dsu edit SUBCOMMAND`.
249
+ - `dsu add OPTION` will raise an error if the entry discription (Entry#description) are not unique. This will be handled gracefully in a future release.
250
+
248
251
  ## Installation
249
252
 
250
253
  $ gem install dsu
@@ -2,12 +2,17 @@
2
2
 
3
3
  require 'deco_lite'
4
4
  require 'securerandom'
5
+ require_relative '../support/descriptable'
5
6
 
6
7
  module Dsu
7
8
  module Models
8
9
  class Entry < DecoLite::Model
10
+ include Support::Descriptable
11
+
12
+ ENTRY_UUID_REGEX = /\A[0-9a-f]{8}\z/i
13
+
9
14
  validates :uuid, presence: true, format: {
10
- with: /\A[0-9a-f]{8}\z/i,
15
+ with: ENTRY_UUID_REGEX,
11
16
  message: 'is the wrong format. ' \
12
17
  '0-9, a-f, and 8 characters were expected.' \
13
18
  }
@@ -1,17 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'deco_lite'
4
- require_relative '../support/entry_group_loadable'
4
+ require_relative '../services/entry_group_editor_service'
5
5
  require_relative '../services/entry_group_deleter_service'
6
6
  require_relative '../services/entry_group_reader_service'
7
7
  require_relative '../services/entry_group_writer_service'
8
+ require_relative '../support/entry_group_loadable'
9
+ require_relative '../support/time_formatable'
8
10
  require_relative '../validators/entries_validator'
9
11
  require_relative '../validators/time_validator'
12
+ require_relative 'entry'
10
13
 
11
14
  module Dsu
12
15
  module Models
13
16
  class EntryGroup < DecoLite::Model
14
17
  extend Support::EntryGroupLoadable
18
+ include Support::TimeFormatable
15
19
 
16
20
  validates_with Validators::EntriesValidator, fields: [:entries]
17
21
  validates_with Validators::TimeValidator, fields: [:time]
@@ -36,6 +40,16 @@ module Dsu
36
40
  Services::EntryGroupDeleterService.new(time: time, options: options).call
37
41
  end
38
42
 
43
+ def edit(time:, options: {})
44
+ # NOTE: Uncomment this line to prohibit edits on
45
+ # Entry Groups that do not exist (i.e. have no entries).
46
+ # return new(time: time) unless exists?(time: time)
47
+
48
+ load(time: time).tap do |entry_group|
49
+ entry_group.edit(options: options)
50
+ end
51
+ end
52
+
39
53
  def exists?(time:)
40
54
  Dsu::Services::EntryGroupReaderService.entry_group_file_exists?(time: time)
41
55
  end
@@ -53,15 +67,18 @@ module Dsu
53
67
  entry_group_hash = entry_group_hash_for(time: time)
54
68
  hydrate_entry_group_hash(entry_group_hash: entry_group_hash, time: time)
55
69
  end
70
+
71
+ def unique?(entry:)
72
+
73
+ end
56
74
  end
57
75
 
58
76
  def required_fields
59
77
  %i[time entries]
60
78
  end
61
79
 
62
- def save!
63
- validate!
64
- Services::EntryGroupWriterService.new(entry_group: self).call
80
+ def edit(options: {})
81
+ Services::EntryGroupEditorService.new(entry_group: self, options: options).call
65
82
  self
66
83
  end
67
84
 
@@ -71,6 +88,15 @@ module Dsu
71
88
  self
72
89
  end
73
90
 
91
+ def entries?
92
+ entries.any?
93
+ end
94
+
95
+ def save!
96
+ validate!
97
+ Services::EntryGroupWriterService.new(entry_group: self).call
98
+ end
99
+
74
100
  def to_h
75
101
  super.tap do |hash|
76
102
  hash[:entries] = hash[:entries].dup
@@ -80,10 +106,67 @@ module Dsu
80
106
  end
81
107
  end
82
108
 
83
- def to_h_localized
84
- to_h.tap do |hash|
85
- hash[:time] = hash[:time].localtime
109
+ def check_unique(sha_or_editor_command:, description:)
110
+ raise ArgumentError, 'sha_or_editor_command is nil' if sha_or_editor_command.nil?
111
+ raise ArgumentError, 'description is nil' if description.nil?
112
+ raise ArgumentError, 'sha_or_editor_command is the wrong object type' unless sha_or_editor_command.is_a?(String)
113
+ raise ArgumentError, 'description is the wrong object type' unless description.is_a?(String)
114
+
115
+ if entries.blank?
116
+ entry_unique_hash = entry_unique_hash_for(uuid_unique: true, description_unique: true)
117
+ return entry_unique_struct_from(entry_unique_hash: entry_unique_hash)
86
118
  end
119
+
120
+ entry_hash = entries.each_with_object({}) do |entry_group_entry, hash|
121
+ hash[entry_group_entry.uuid] = entry_group_entry.description
122
+ end
123
+
124
+ # It is possible that sha_or_editor_command may have an editor command (e.g. +|a|add). If this
125
+ # is the case, just treat it as unique because when the entry is added, it will get a unique uuid.
126
+ uuid_unique = !sha_or_editor_command.match?(Entry::ENTRY_UUID_REGEX) || !entry_hash.key?(sha_or_editor_command)
127
+ entry_unique_hash = entry_unique_hash_for(
128
+ uuid: sha_or_editor_command,
129
+ uuid_unique: uuid_unique,
130
+ description: description,
131
+ description_unique: !entry_hash.value?(description)
132
+ )
133
+ entry_unique_struct_from(entry_unique_hash: entry_unique_hash)
134
+ end
135
+
136
+ def entry_unique_hash_for(uuid_unique:, description_unique:, uuid: nil, description: nil)
137
+ {
138
+ uuid: uuid,
139
+ uuid_unique: uuid_unique,
140
+ description: description,
141
+ description_unique: description_unique,
142
+ formatted_time: Support::TimeFormatable.formatted_time(time: time)
143
+ }
144
+ end
145
+
146
+ def entry_unique_struct_from(entry_unique_hash:)
147
+ Struct.new(*entry_unique_hash.keys, keyword_init: true) do
148
+ def unique?
149
+ uuid_unique? && description_unique?
150
+ end
151
+
152
+ def uuid_unique?
153
+ uuid_unique
154
+ end
155
+
156
+ def description_unique?
157
+ description_unique
158
+ end
159
+
160
+ def messages
161
+ return [] if unique?
162
+
163
+ short_description = Models::Entry.short_description(string: description)
164
+
165
+ messages = []
166
+ messages << "#uuid is not unique: \"#{uuid} #{short_description}\"" unless uuid_unique?
167
+ messages << "#description is not unique: \"#{uuid} #{short_description}\""
168
+ end
169
+ end.new(**entry_unique_hash)
87
170
  end
88
171
  end
89
172
  end
@@ -9,8 +9,6 @@ module Dsu
9
9
  class ConfigurationLoaderService
10
10
  include Dsu::Support::Configuration
11
11
 
12
- attr_reader :default_options
13
-
14
12
  def initialize(default_options: nil)
15
13
  unless default_options.nil? ||
16
14
  default_options.is_a?(Hash) ||
@@ -18,7 +16,7 @@ module Dsu
18
16
  raise ArgumentError, 'default_options must be a Hash or ActiveSupport::HashWithIndifferentAccess'
19
17
  end
20
18
 
21
- @default_options ||= default_options || {}
19
+ @default_options = default_options || {}
22
20
  @default_options = @default_options.with_indifferent_access if @default_options.is_a?(Hash)
23
21
  end
24
22
 
@@ -28,6 +26,8 @@ module Dsu
28
26
 
29
27
  private
30
28
 
29
+ attr_reader :default_options
30
+
31
31
  def config_options
32
32
  return Support::Configuration::DEFAULT_DSU_OPTIONS unless config_file?
33
33
 
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/entry'
4
+ require_relative '../support/colorable'
5
+ require_relative '../support/say'
6
+ require_relative '../support/time_formatable'
7
+ require_relative 'configuration_loader_service'
8
+
9
+ module Dsu
10
+ module Services
11
+ class EntryGroupEditorService
12
+ include Support::Colorable
13
+ include Support::Say
14
+ include Support::TimeFormatable
15
+
16
+ def initialize(entry_group:, options: {})
17
+ raise ArgumentError, 'entry_group is nil' if entry_group.nil?
18
+ raise ArgumentError, 'entry_group is the wrong object type' unless entry_group.is_a?(Models::EntryGroup)
19
+ raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash) || options.nil?
20
+
21
+ @entry_group = entry_group
22
+ @options = options || {}
23
+ end
24
+
25
+ def call
26
+ edit_view = render_edit_view
27
+ edit!(edit_view: edit_view)
28
+ # NOTE: Return the original entry group object as any permanent changes
29
+ # will have been applied to it.
30
+ entry_group
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :entry_group, :options
36
+
37
+ # Renders the edit view to a string so we can write it to a temporary file
38
+ # and edit it. The edits will be used to update the entry group.
39
+ def render_edit_view
40
+ say "Editing entry group #{formatted_time(time: entry_group.time)}...", HIGHLIGHT
41
+ capture_stdxxx { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
42
+ end
43
+
44
+ # Writes the temporary file contents to disk and opens it in the editor.
45
+ def edit!(edit_view:)
46
+ Services::TempFileWriterService.new(tmp_file_content: edit_view).call do |tmp_file_path|
47
+ unless Kernel.system("${EDITOR:-#{configuration[:editor]}} #{tmp_file_path}")
48
+ say "Failed to open temporary file in editor '#{configuration[:editor]}';" \
49
+ "the system error returned was: '#{$CHILD_STATUS}'.", ERROR
50
+ say 'Either set the EDITOR environment variable ' \
51
+ 'or set the dsu editor configuration option (`$ dsu config init`).', ERROR
52
+ say 'Run `$ dsu help config` for more information.', ERROR
53
+
54
+ system('dsu help config')
55
+
56
+ return # rubocop:disable Lint/NonLocalExitFromIterator: This is not an iterator.
57
+ end
58
+
59
+ update_entry_group!(tmp_file_path: tmp_file_path)
60
+ end
61
+ end
62
+
63
+ # TODO: Clean this up
64
+ def update_entry_group!(tmp_file_path:)
65
+ errors = []
66
+ entry_group.entries = entries = []
67
+ Services::TempFileReaderService.new(tmp_file_path: tmp_file_path).call do |tmp_file_line|
68
+ next if comment_or_empty?(tmp_file_line: tmp_file_line)
69
+
70
+ entry_info = editor_entry_info_from(tmp_file_line: tmp_file_line)
71
+ next if entry_info.empty?
72
+ next if delete_entry_cmd?(sha: entry_info[:sha])
73
+ next unless add_entry_cmd?(sha: entry_info[:sha]) || sha?(sha: entry_info[:sha])
74
+
75
+ entry_info[:sha_or_editor_command] = entry_info[:sha]
76
+ entry_info[:sha] = nil if add_entry_cmd?(sha: entry_info[:sha])
77
+
78
+ entry = Models::Entry.new(uuid: entry_info[:sha], description: entry_info[:description])
79
+ entry_group.check_unique(sha_or_editor_command: entry_info[:sha_or_editor_command],
80
+ description: entry_info[:description]).tap do |status|
81
+ entries << entry and next if status.unique?
82
+
83
+ errors << status.messages
84
+ end
85
+ end
86
+
87
+ # Display any errors encountered.
88
+ if errors.any?
89
+ say 'Error: one or more entry values were not unique within the entry group entries:', ERROR
90
+ errors.flatten.each { |message| say "Error: #{message}", ERROR }
91
+ end
92
+
93
+ # Save or delete any entries.
94
+ entry_group.entries = entries
95
+ entry_group.delete and return unless entry_group.entries?
96
+
97
+ entry_group.save!
98
+ end
99
+
100
+ def sha?(sha:)
101
+ sha.match?(Models::Entry::ENTRY_UUID_REGEX)
102
+ end
103
+
104
+ def delete_entry_cmd?(sha:)
105
+ %w[- d delete].include?(sha)
106
+ end
107
+
108
+ def add_entry_cmd?(sha:)
109
+ %w[+ a add].include?(sha)
110
+ end
111
+
112
+ def comment_or_empty?(tmp_file_line:)
113
+ ['#', nil].include? tmp_file_line[0]
114
+ end
115
+
116
+ def editor_entry_info_from(tmp_file_line:)
117
+ match_data = tmp_file_line.match(/(\S+)\s(.+)/)
118
+ {
119
+ sha: match_data[1]&.strip,
120
+ description: match_data[2]&.strip
121
+ }
122
+ rescue StandardError
123
+ {}
124
+ end
125
+
126
+ # TODO: Add this to a module.
127
+ # https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
128
+ def capture_stdxxx
129
+ # The output stream must be an IO-like object. In this case we capture it in
130
+ # an in-memory IO object so we can return the string value. You can assign any
131
+ # IO object here.
132
+ string_io = StringIO.new
133
+ prev_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
134
+ prev_stderr, $stderr = $stderr, string_io # rubocop:disable Style/ParallelAssignment
135
+ yield
136
+ string_io.string
137
+ ensure
138
+ # Restore the previous value of stderr and stdout (typically equal to STDERR).
139
+ $stdout = prev_stdout
140
+ $stderr = prev_stderr
141
+ end
142
+
143
+ def configuration
144
+ @configuration ||= ConfigurationLoaderService.new.call
145
+ end
146
+ end
147
+ end
148
+ end
@@ -26,6 +26,7 @@ module Dsu
26
26
  entry_group.validate!
27
27
  create_entry_group_path_if!
28
28
  write_entry_group_to_file!
29
+ entry_group
29
30
  rescue ActiveModel::ValidationError
30
31
  puts "Error(s) encountered: #{entry_group.errors.full_messages}"
31
32
  raise
@@ -3,29 +3,29 @@
3
3
  module Dsu
4
4
  module Services
5
5
  class TempFileReaderService
6
- def initialize(temp_file_path:, options: {})
7
- raise ArgumentError, 'temp_file_path is nil' if temp_file_path.nil?
8
- raise ArgumentError, 'temp_file_path is the wrong object type' unless temp_file_path.is_a?(String)
9
- raise ArgumentError, 'temp_file_path is empty' if temp_file_path.empty?
10
- raise ArgumentError, 'temp_file_path does not exist' unless File.exist?(temp_file_path)
6
+ def initialize(tmp_file_path:, options: {})
7
+ raise ArgumentError, 'tmp_file_path is nil' if tmp_file_path.nil?
8
+ raise ArgumentError, 'tmp_file_path is the wrong object type' unless tmp_file_path.is_a?(String)
9
+ raise ArgumentError, 'tmp_file_path is empty' if tmp_file_path.empty?
10
+ raise ArgumentError, 'tmp_file_path does not exist' unless File.exist?(tmp_file_path)
11
11
  raise ArgumentError, 'options is nil' if options.nil?
12
12
  raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
13
13
 
14
- @temp_file_path = temp_file_path
14
+ @tmp_file_path = tmp_file_path
15
15
  @options = options || {}
16
16
  end
17
17
 
18
18
  def call
19
19
  raise ArgumentError, 'no block given' unless block_given?
20
20
 
21
- File.foreach(temp_file_path) do |line|
21
+ File.foreach(tmp_file_path) do |line|
22
22
  yield line.strip
23
23
  end
24
24
  end
25
25
 
26
26
  private
27
27
 
28
- attr_reader :temp_file_path, :options
28
+ attr_reader :tmp_file_path, :options
29
29
  end
30
30
  end
31
31
  end
@@ -5,13 +5,13 @@ require 'tempfile'
5
5
  module Dsu
6
6
  module Services
7
7
  class TempFileWriterService
8
- def initialize(temp_file_content:, options: {})
9
- raise ArgumentError, 'temp_file_content is nil' if temp_file_content.nil?
10
- raise ArgumentError, 'temp_file_content is the wrong object type' unless temp_file_content.is_a?(String)
8
+ def initialize(tmp_file_content:, options: {})
9
+ raise ArgumentError, 'tmp_file_content is nil' if tmp_file_content.nil?
10
+ raise ArgumentError, 'tmp_file_content is the wrong object type' unless tmp_file_content.is_a?(String)
11
11
  raise ArgumentError, 'options is nil' if options.nil?
12
12
  raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
13
13
 
14
- @temp_file_content = temp_file_content
14
+ @tmp_file_content = tmp_file_content
15
15
  @options = options || {}
16
16
  end
17
17
 
@@ -19,7 +19,7 @@ module Dsu
19
19
  raise ArgumentError, 'no block given' unless block_given?
20
20
 
21
21
  Tempfile.new('dsu').tap do |file|
22
- file.write("#{temp_file_content}\n")
22
+ file.write("#{tmp_file_content}\n")
23
23
  file.close
24
24
  yield file.path
25
25
  end.unlink
@@ -27,7 +27,7 @@ module Dsu
27
27
 
28
28
  private
29
29
 
30
- attr_reader :temp_file_content, :options
30
+ attr_reader :tmp_file_content, :options
31
31
  end
32
32
  end
33
33
  end
@@ -1,19 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'English'
4
3
  require_relative '../base_cli'
5
4
  require_relative '../models/entry_group'
6
- require_relative '../services/temp_file_reader_service'
7
- require_relative '../services/temp_file_writer_service'
8
- require_relative '../support/time_formatable'
9
- require_relative '../views/entry_group/edit'
10
5
  require_relative '../views/entry_group/show'
11
6
 
12
7
  module Dsu
13
8
  module Subcommands
14
9
  class Edit < Dsu::BaseCLI
15
- include Support::TimeFormatable
16
-
17
10
  map %w[d] => :date
18
11
  map %w[n] => :today
19
12
  map %w[t] => :tomorrow
@@ -25,7 +18,8 @@ module Dsu
25
18
  Edits the DSU entries for today.
26
19
  LONG_DESC
27
20
  def today
28
- Views::EntryGroup::Show.new(entry_group: edit_entry_group(time: Time.now)).render
21
+ entry_group = Models::EntryGroup.edit(time: Time.now)
22
+ Views::EntryGroup::Show.new(entry_group: entry_group).render
29
23
  end
30
24
 
31
25
  desc 'tomorrow, t',
@@ -34,7 +28,8 @@ module Dsu
34
28
  Edits the DSU entries for tomorrow.
35
29
  LONG_DESC
36
30
  def tomorrow
37
- Views::EntryGroup::Show.new(entry_group: edit_entry_group(time: Time.now.tomorrow)).render
31
+ entry_group = Models::EntryGroup.edit(time: Time.now.tomorrow)
32
+ Views::EntryGroup::Show.new(entry_group: entry_group).render
38
33
  end
39
34
 
40
35
  desc 'yesterday, y',
@@ -43,7 +38,8 @@ module Dsu
43
38
  Edits the DSU entries for yesterday.
44
39
  LONG_DESC
45
40
  def yesterday
46
- Views::EntryGroup::Show.new(entry_group: edit_entry_group(time: Time.now.yesterday)).render
41
+ entry_group = Models::EntryGroup.edit(time: Time.now.yesterday)
42
+ Views::EntryGroup::Show.new(entry_group: entry_group).render
47
43
  end
48
44
 
49
45
  desc 'date, d DATE',
@@ -54,79 +50,12 @@ module Dsu
54
50
  \x5 #{date_option_description}
55
51
  LONG_DESC
56
52
  def date(date)
57
- Views::EntryGroup::Show.new(entry_group: edit_entry_group(time: Time.parse(date))).render
53
+ entry_group = Models::EntryGroup.edit(time: Time.parse(date))
54
+ Views::EntryGroup::Show.new(entry_group: entry_group).render
58
55
  rescue ArgumentError => e
59
56
  say "Error: #{e.message}", ERROR
60
57
  exit 1
61
58
  end
62
-
63
- private
64
-
65
- def edit_entry_group(time:)
66
- formatted_time = formatted_time(time: time)
67
- unless Models::EntryGroup.exists?(time: time)
68
- say "No DSU entries exist for #{formatted_time}"
69
- exit 1
70
- end
71
-
72
- say "Editing DSU entries for #{formatted_time}..."
73
- entry_group = Models::EntryGroup.load(time: time)
74
-
75
- # This renders the view to a string...
76
- output = capture_stdxxx do
77
- Views::EntryGroup::Edit.new(entry_group: entry_group).render
78
- end
79
- # ...which is then written to a temp file.
80
- Services::TempFileWriterService.new(temp_file_content: output).call do |temp_file_path|
81
- unless system("${EDITOR:-#{configuration[:editor]}} #{temp_file_path}")
82
- say "Failed to open temporary file in editor '#{configuration[:editor]}';" \
83
- "the system error returned was: '#{$CHILD_STATUS}'.", ERROR
84
- say 'Either set the EDITOR environment variable ' \
85
- 'or set the dsu editor configuration option (`$ dsu config init`).', ERROR
86
- say 'Run `$ dsu help config` for more information.', ERROR
87
- system('dsu help config')
88
- exit 1
89
- end
90
- entries = []
91
- Services::TempFileReaderService.new(temp_file_path: temp_file_path).call do |temp_file_line|
92
- # Skip comments and blank lines.
93
- next if ['#', nil].include? temp_file_line[0]
94
-
95
- match_data = temp_file_line.match(/(\S+)\s(.+)/)
96
- # TODO: Error handling if match_data is nil.
97
- entry_sha = match_data[1]
98
- entry_description = match_data[2]
99
-
100
- next if %w[- d delete].include?(entry_sha) # delete the entry
101
-
102
- entry_sha = nil if %w[+ a add].include?(entry_sha) # add the new entry
103
- entries << Models::Entry.new(uuid: entry_sha, description: entry_description)
104
- end
105
-
106
- entry_group.entries = entries
107
-
108
- return entry_group.delete if entries.empty?
109
-
110
- entry_group.save!
111
- end
112
- entry_group
113
- end
114
-
115
- # https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
116
- def capture_stdxxx
117
- # The output stream must be an IO-like object. In this case we capture it in
118
- # an in-memory IO object so we can return the string value. You can assign any
119
- # IO object here.
120
- string_io = StringIO.new
121
- prev_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
122
- prev_stderr, $stderr = $stderr, string_io # rubocop:disable Style/ParallelAssignment
123
- yield
124
- string_io.string
125
- ensure
126
- # Restore the previous value of stderr and stdout (typically equal to STDERR).
127
- $stdout = prev_stdout
128
- $stderr = prev_stderr
129
- end
130
59
  end
131
60
  end
132
61
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Support
5
+ module Descriptable
6
+ class << self
7
+ def included(mod)
8
+ mod.extend(ClassMethods)
9
+ end
10
+ end
11
+
12
+ def short_description
13
+ return '' if description.blank?
14
+
15
+ self.class.short_description(string: description)
16
+ end
17
+
18
+ module ClassMethods
19
+ def short_description(string:, count: 25, elipsis: '...')
20
+ return elipsis unless string.is_a?(String)
21
+
22
+ elipsis_length = elipsis.length
23
+ count = elipsis_length if count.nil? || count < elipsis_length
24
+
25
+ return string if string.length <= count
26
+
27
+ tokens = string.split
28
+ string = ''
29
+
30
+ return "#{tokens.first[0..(count - elipsis_length)]}#{elipsis}" if tokens.count == 1
31
+
32
+ tokens.each do |token|
33
+ break if string.length + token.length + elipsis_length > count
34
+
35
+ string = "#{string} #{token}"
36
+ end
37
+
38
+ "#{string.strip}#{elipsis}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -24,7 +24,8 @@ module Dsu
24
24
  end
25
25
 
26
26
  validate_entry_types field, entries, record
27
- validate_unique_entry_uuids field, entries, record
27
+ validate_unique_entry_attr :uuid, field, entries, record
28
+ validate_unique_entry_attr :description, field, entries, record
28
29
  end
29
30
  end
30
31
 
@@ -42,17 +43,17 @@ module Dsu
42
43
  end
43
44
  end
44
45
 
45
- def validate_unique_entry_uuids(field, entries, record)
46
+ def validate_unique_entry_attr(attr, field, entries, record)
46
47
  return unless entries.is_a? Array
47
48
 
48
49
  entry_objects = entries.select { |entry| entry.is_a?(Dsu::Models::Entry) }
49
50
 
50
- uuids = entry_objects.map(&:uuid)
51
- return if uuids.uniq.length == uuids.length
51
+ attrs = entry_objects.map(&attr)
52
+ return if attrs.uniq.length == attrs.length
52
53
 
53
- non_unique_uuids = uuids.select { |element| uuids.count(element) > 1 }.uniq
54
- if non_unique_uuids.any?
55
- record.errors.add(field, "contains duplicate UUIDs: #{non_unique_uuids.join(', ')}.",
54
+ non_unique_attrs = attrs.select { |attr| attrs.count(attr) > 1 }.uniq # rubocop:disable Lint/ShadowingOuterLocalVariable
55
+ if non_unique_attrs.any?
56
+ record.errors.add(field, "contains duplicate ##{attr}s: #{non_unique_attrs.join(', ')}.",
56
57
  type: Dsu::Support::FieldErrors::FIELD_DUPLICATE_ERROR)
57
58
  end
58
59
  end
data/lib/dsu/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dsu
4
- VERSION = '0.1.0.alpha.4'
4
+ VERSION = '0.1.0.alpha.5'
5
5
  end
@@ -42,13 +42,15 @@ module Dsu
42
42
 
43
43
  def render_entry_group!
44
44
  say "# Editing DSU Entries for #{formatted_time(time: entry_group.time)}"
45
- say('(no entries available for this day)') and return if entry_group.entries.empty?
46
-
45
+ # TODO: Display entry group entries from the previous DSU date so they can be
46
+ # 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?
47
49
  say ''
48
50
  say '# [SHA/COMMAND] [DESCRIPTION]'
49
51
 
50
52
  entry_group.entries.each do |entry|
51
- say "#{entry.uuid} #{entry.description}"
53
+ say "#{entry.uuid} #{entry.description.strip}"
52
54
  end
53
55
 
54
56
  say ''
@@ -26,13 +26,7 @@ module Dsu
26
26
  end
27
27
 
28
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
29
  render_entry_group!
33
- rescue ActiveModel::ValidationError
34
- puts "Error(s) encountered: #{entry_group.errors.full_messages}"
35
- raise
36
30
  end
37
31
  alias render call
38
32
 
@@ -47,7 +41,9 @@ module Dsu
47
41
  entry_group.entries.each_with_index do |entry, index|
48
42
  prefix = "#{format('%03s', index + 1)}. #{entry.uuid}"
49
43
  description = colorize_string(string: entry.description, mode: :bold)
50
- say "#{prefix} #{description}"
44
+ entry_info = "#{prefix} #{description}"
45
+ entry_info = "#{entry_info} (validation failed)" unless entry.valid?
46
+ say entry_info
51
47
  end
52
48
  end
53
49
  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.4
4
+ version: 0.1.0.alpha.5
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-09 00:00:00.000000000 Z
11
+ date: 2023-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -153,6 +153,7 @@ files:
153
153
  - lib/dsu/models/entry_group.rb
154
154
  - lib/dsu/services/configuration_loader_service.rb
155
155
  - lib/dsu/services/entry_group_deleter_service.rb
156
+ - lib/dsu/services/entry_group_editor_service.rb
156
157
  - lib/dsu/services/entry_group_hydrator_service.rb
157
158
  - lib/dsu/services/entry_group_reader_service.rb
158
159
  - lib/dsu/services/entry_group_writer_service.rb
@@ -168,6 +169,7 @@ files:
168
169
  - lib/dsu/support/commander/command_help.rb
169
170
  - lib/dsu/support/commander/subcommand.rb
170
171
  - lib/dsu/support/configuration.rb
172
+ - lib/dsu/support/descriptable.rb
171
173
  - lib/dsu/support/entry_group_fileable.rb
172
174
  - lib/dsu/support/entry_group_loadable.rb
173
175
  - lib/dsu/support/entry_group_viewable.rb