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 +4 -4
- data/CHANGELOG.md +6 -0
- data/Gemfile.lock +1 -1
- data/README.md +6 -3
- data/lib/dsu/models/entry.rb +6 -1
- data/lib/dsu/models/entry_group.rb +90 -7
- data/lib/dsu/services/configuration_loader_service.rb +3 -3
- data/lib/dsu/services/entry_group_editor_service.rb +148 -0
- data/lib/dsu/services/entry_group_writer_service.rb +1 -0
- data/lib/dsu/services/temp_file_reader_service.rb +8 -8
- data/lib/dsu/services/temp_file_writer_service.rb +6 -6
- data/lib/dsu/subcommands/edit.rb +8 -79
- data/lib/dsu/support/descriptable.rb +43 -0
- data/lib/dsu/validators/entries_validator.rb +8 -7
- data/lib/dsu/version.rb +1 -1
- data/lib/dsu/views/entry_group/edit.rb +5 -3
- data/lib/dsu/views/entry_group/show.rb +3 -7
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8563782a0b4d22e374313aa3373b38e62dec995a6f6d996eaa5fa9c58b0d1cb
|
4
|
+
data.tar.gz: 65ebebe762dac44300a6d163c03275eaffd66018a83f9836b3f12cb6333569ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
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
|
data/lib/dsu/models/entry.rb
CHANGED
@@ -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:
|
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 '../
|
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
|
63
|
-
|
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
|
84
|
-
|
85
|
-
|
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
|
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
|
@@ -3,29 +3,29 @@
|
|
3
3
|
module Dsu
|
4
4
|
module Services
|
5
5
|
class TempFileReaderService
|
6
|
-
def initialize(
|
7
|
-
raise ArgumentError, '
|
8
|
-
raise ArgumentError, '
|
9
|
-
raise ArgumentError, '
|
10
|
-
raise ArgumentError, '
|
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
|
-
@
|
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(
|
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 :
|
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(
|
9
|
-
raise ArgumentError, '
|
10
|
-
raise ArgumentError, '
|
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
|
-
@
|
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("#{
|
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 :
|
30
|
+
attr_reader :tmp_file_content, :options
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
data/lib/dsu/subcommands/edit.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
51
|
-
return if
|
51
|
+
attrs = entry_objects.map(&attr)
|
52
|
+
return if attrs.uniq.length == attrs.length
|
52
53
|
|
53
|
-
|
54
|
-
if
|
55
|
-
record.errors.add(field, "contains duplicate
|
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
@@ -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
|
-
|
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
|
-
|
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
|
+
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-
|
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
|