dsu 0.1.0.alpha.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +17 -0
- data/Gemfile.lock +1 -1
- data/README.md +29 -39
- data/lib/dsu/base_cli.rb +0 -3
- data/lib/dsu/cli.rb +42 -49
- data/lib/dsu/command_services/add_entry_service.rb +21 -21
- data/lib/dsu/models/entry.rb +37 -20
- data/lib/dsu/models/entry_group.rb +66 -38
- data/lib/dsu/services/configuration_loader_service.rb +4 -3
- data/lib/dsu/services/entry_group_editor_service.rb +99 -0
- data/lib/dsu/services/entry_group_writer_service.rb +1 -0
- data/lib/dsu/services/stdout_redirector_service.rb +27 -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/colorable.rb +1 -0
- data/lib/dsu/support/descriptable.rb +43 -0
- data/lib/dsu/support/entry_group_loadable.rb +10 -12
- data/lib/dsu/validators/description_validator.rb +38 -0
- data/lib/dsu/validators/entries_validator.rb +43 -31
- data/lib/dsu/validators/time_validator.rb +11 -20
- data/lib/dsu/version.rb +1 -1
- data/lib/dsu/views/edited_entries/shared/errors.rb +39 -0
- data/lib/dsu/views/entry_group/edit.rb +30 -33
- data/lib/dsu/views/entry_group/show.rb +12 -10
- data/lib/dsu/views/shared/messages.rb +56 -0
- data/lib/dsu.rb +4 -0
- metadata +10 -8
- data/lib/dsu/support/commander/command.rb +0 -130
- data/lib/dsu/support/commander/command_help.rb +0 -62
- data/lib/dsu/support/commander/subcommand.rb +0 -45
- data/lib/dsu/support/interactive/cli.rb +0 -161
@@ -6,11 +6,10 @@ require_relative '../support/configuration'
|
|
6
6
|
|
7
7
|
module Dsu
|
8
8
|
module Services
|
9
|
+
# This class loads an entry group file.
|
9
10
|
class ConfigurationLoaderService
|
10
11
|
include Dsu::Support::Configuration
|
11
12
|
|
12
|
-
attr_reader :default_options
|
13
|
-
|
14
13
|
def initialize(default_options: nil)
|
15
14
|
unless default_options.nil? ||
|
16
15
|
default_options.is_a?(Hash) ||
|
@@ -18,7 +17,7 @@ module Dsu
|
|
18
17
|
raise ArgumentError, 'default_options must be a Hash or ActiveSupport::HashWithIndifferentAccess'
|
19
18
|
end
|
20
19
|
|
21
|
-
@default_options
|
20
|
+
@default_options = default_options || {}
|
22
21
|
@default_options = @default_options.with_indifferent_access if @default_options.is_a?(Hash)
|
23
22
|
end
|
24
23
|
|
@@ -28,6 +27,8 @@ module Dsu
|
|
28
27
|
|
29
28
|
private
|
30
29
|
|
30
|
+
attr_reader :default_options
|
31
|
+
|
31
32
|
def config_options
|
32
33
|
return Support::Configuration::DEFAULT_DSU_OPTIONS unless config_file?
|
33
34
|
|
@@ -0,0 +1,99 @@
|
|
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 '../views/edited_entries/shared/errors'
|
8
|
+
require_relative '../views/shared/messages'
|
9
|
+
require_relative 'configuration_loader_service'
|
10
|
+
require_relative 'stdout_redirector_service'
|
11
|
+
|
12
|
+
module Dsu
|
13
|
+
module Services
|
14
|
+
class EntryGroupEditorService
|
15
|
+
include Support::Colorable
|
16
|
+
include Support::Say
|
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
|
+
say "Editing entry group #{formatted_time(time: entry_group.time)}...", HIGHLIGHT
|
44
|
+
StdoutRedirectorService.call { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Writes the temporary file contents to disk and opens it in the editor
|
48
|
+
# for editing. It then copies the changes to the entry group and writes
|
49
|
+
# the changes to the entry group file.
|
50
|
+
def edit(edit_view)
|
51
|
+
entry_group_with_edits = Models::EntryGroup.new(time: entry_group.time)
|
52
|
+
|
53
|
+
Services::TempFileWriterService.new(tmp_file_content: edit_view).call do |tmp_file_path|
|
54
|
+
if Kernel.system("${EDITOR:-#{configuration[:editor]}} #{tmp_file_path}")
|
55
|
+
Services::TempFileReaderService.new(tmp_file_path: tmp_file_path).call do |editor_line|
|
56
|
+
next unless process_description?(editor_line)
|
57
|
+
|
58
|
+
entry_group_with_edits.entries << Models::Entry.new(description: editor_line)
|
59
|
+
end
|
60
|
+
|
61
|
+
process_entry_group!(entry_group_with_edits)
|
62
|
+
else
|
63
|
+
say "Failed to open temporary file in editor '#{configuration[:editor]}'; " \
|
64
|
+
"the system error returned was: '#{$CHILD_STATUS}'.", ERROR
|
65
|
+
say 'Either set the EDITOR environment variable ' \
|
66
|
+
'or set the dsu editor configuration option (`$ dsu config init`).', ERROR
|
67
|
+
say 'Run `$ dsu help config` for more information:', ERROR
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def process_entry_group!(entry_group_with_edits)
|
73
|
+
if entry_group_with_edits.entries.empty?
|
74
|
+
entry_group.delete!
|
75
|
+
return
|
76
|
+
end
|
77
|
+
|
78
|
+
if entry_group_with_edits.invalid?
|
79
|
+
header = 'The following ERRORS were encountered; these changes were not saved:'
|
80
|
+
messages = entry_group_with_edits.errors.full_messages
|
81
|
+
Views::Shared::Messages.new(messages: messages, message_type: :error, options: { header: header }).render
|
82
|
+
end
|
83
|
+
|
84
|
+
# Make sure we're saving only valid, unique entries.
|
85
|
+
entry_group.entries = entry_group_with_edits.valid_unique_entries
|
86
|
+
entry_group.save!
|
87
|
+
end
|
88
|
+
|
89
|
+
def process_description?(description)
|
90
|
+
description = Models::Entry.clean_description(description)
|
91
|
+
!(description.blank? || description[0] == '#')
|
92
|
+
end
|
93
|
+
|
94
|
+
def configuration
|
95
|
+
@configuration ||= ConfigurationLoaderService.new.call
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dsu
|
4
|
+
module Services
|
5
|
+
# This service captures $stdout, 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 StdoutRedirectorService
|
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_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
|
18
|
+
yield
|
19
|
+
string_io.string
|
20
|
+
ensure
|
21
|
+
# Restore the original $stdout.
|
22
|
+
$stdout = original_stdout
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
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
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'pathname'
|
4
4
|
require_relative '../services/entry_group_reader_service'
|
5
5
|
require_relative '../models/entry'
|
6
|
+
require_relative '../models/entry_group'
|
6
7
|
|
7
8
|
module Dsu
|
8
9
|
module Support
|
@@ -12,22 +13,19 @@ module Dsu
|
|
12
13
|
# returns a Hash having :time and :entries
|
13
14
|
# where entries == an Array of Entry Hashes
|
14
15
|
# representing the JSON Entry objects for :time.
|
15
|
-
def
|
16
|
+
def load_entry_group_file_for(time:)
|
16
17
|
entry_group_json = Services::EntryGroupReaderService.new(time: time).call
|
17
|
-
if entry_group_json.present?
|
18
|
-
|
18
|
+
hash = if entry_group_json.present?
|
19
|
+
JSON.parse(entry_group_json, symbolize_names: true).tap do |hash|
|
19
20
|
hash[:time] = Time.parse(hash[:time])
|
20
21
|
end
|
22
|
+
else
|
23
|
+
{ time: time, entries: [] }
|
21
24
|
end
|
22
25
|
|
23
|
-
|
24
|
-
time: time,
|
25
|
-
entries: []
|
26
|
-
}
|
26
|
+
Models::EntryGroup.new(**hydrate_entry_group_hash(hash: hash, time: time))
|
27
27
|
end
|
28
28
|
|
29
|
-
private
|
30
|
-
|
31
29
|
# Accepts an entry group hash and returns a
|
32
30
|
# hydrated entry group hash:
|
33
31
|
#
|
@@ -39,10 +37,10 @@ module Dsu
|
|
39
37
|
# ...
|
40
38
|
# ]
|
41
39
|
# }
|
42
|
-
def hydrate_entry_group_hash(
|
43
|
-
time =
|
40
|
+
def hydrate_entry_group_hash(hash:, time:)
|
41
|
+
time = hash.fetch(:time, time)
|
44
42
|
time = Time.parse(time) unless time.is_a? Time
|
45
|
-
entries =
|
43
|
+
entries = hash.fetch(:entries, [])
|
46
44
|
entries = entries.map { |entry_hash| Models::Entry.new(**entry_hash) }
|
47
45
|
|
48
46
|
{ time: time, entries: entries }
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dsu
|
4
|
+
module Validators
|
5
|
+
class DescriptionValidator < ActiveModel::Validator
|
6
|
+
def validate(record)
|
7
|
+
description = record.description
|
8
|
+
|
9
|
+
if description.blank?
|
10
|
+
record.errors.add(:description, :blank)
|
11
|
+
return
|
12
|
+
end
|
13
|
+
|
14
|
+
unless description.is_a?(String)
|
15
|
+
record.errors.add(field, 'is the wrong object type. ' \
|
16
|
+
"\"String\" was expected, but \"#{description.class}\" was received.")
|
17
|
+
return
|
18
|
+
end
|
19
|
+
|
20
|
+
validate_description record
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate_description(record)
|
26
|
+
description = record.description
|
27
|
+
|
28
|
+
return if description.length.between?(2, 256)
|
29
|
+
|
30
|
+
if description.length < 2
|
31
|
+
record.errors.add(:description, "is too short: \"#{record.short_description}\" (minimum is 2 characters).")
|
32
|
+
elsif description.length > 256
|
33
|
+
record.errors.add(:description, "is too long: \"#{record.short_description}\" (maximum is 256 characters).")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -7,55 +7,67 @@ require_relative '../support/field_errors'
|
|
7
7
|
module Dsu
|
8
8
|
module Validators
|
9
9
|
class EntriesValidator < ActiveModel::Validator
|
10
|
-
include
|
10
|
+
include Support::FieldErrors
|
11
11
|
|
12
12
|
def validate(record)
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
unless record.entries.is_a?(Array)
|
14
|
+
record.errors.add(:entries_entry, 'is the wrong object type. ' \
|
15
|
+
"\"Array\" was expected, but \"#{record.entries.class}\" was received.")
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
validate_entry_types record
|
19
|
+
validate_unique_entry record
|
20
|
+
validate_entries record
|
21
|
+
end
|
19
22
|
|
20
|
-
|
21
|
-
record.errors.add(field, 'is the wrong object type. ' \
|
22
|
-
"\"Array\" was expected, but \"#{entries.class}\" was received.")
|
23
|
-
next
|
24
|
-
end
|
23
|
+
private
|
25
24
|
|
26
|
-
|
27
|
-
|
25
|
+
def validate_entry_types(record)
|
26
|
+
record.entries.each do |entry|
|
27
|
+
next if entry.is_a? Dsu::Models::Entry
|
28
|
+
|
29
|
+
record.errors.add(:entries_entry, 'entry Array element is the wrong object type. ' \
|
30
|
+
"\"Entry\" was expected, but \"#{entry.class}\" was received.",
|
31
|
+
type: Support::FieldErrors::FIELD_TYPE_ERROR)
|
28
32
|
end
|
29
33
|
end
|
30
34
|
|
31
|
-
|
35
|
+
def validate_unique_entry(record)
|
36
|
+
return unless record.entries.is_a? Array
|
32
37
|
|
33
|
-
|
34
|
-
entries.each do |entry|
|
35
|
-
next if entry.is_a? Dsu::Models::Entry
|
38
|
+
entry_objects = record.entries.select { |entry| entry.is_a?(Dsu::Models::Entry) }
|
36
39
|
|
37
|
-
|
38
|
-
|
39
|
-
type: Dsu::Support::FieldErrors::FIELD_TYPE_ERROR)
|
40
|
+
descriptions = entry_objects.map(&:description)
|
41
|
+
return if descriptions.uniq.length == descriptions.length
|
40
42
|
|
41
|
-
|
43
|
+
non_unique_descriptions = descriptions.select { |description| descriptions.count(description) > 1 }.uniq
|
44
|
+
if non_unique_descriptions.any?
|
45
|
+
record.errors.add(:entries_entry, 'contains a duplicate entry: ' \
|
46
|
+
"#{format_non_unique_descriptions(non_unique_descriptions)}.",
|
47
|
+
type: Support::FieldErrors::FIELD_DUPLICATE_ERROR)
|
42
48
|
end
|
43
49
|
end
|
44
50
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
entry_objects = entries.select { |entry| entry.is_a?(Dsu::Models::Entry) }
|
51
|
+
def validate_entries(record)
|
52
|
+
entries = record.entries
|
53
|
+
return if entries.none?
|
49
54
|
|
50
|
-
|
51
|
-
|
55
|
+
entries.each do |entry|
|
56
|
+
next if entry.valid?
|
52
57
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
type: Dsu::Support::FieldErrors::FIELD_DUPLICATE_ERROR)
|
58
|
+
entry.errors.each do |error|
|
59
|
+
record.errors.add(:entries_entry, error.full_message)
|
60
|
+
end
|
57
61
|
end
|
58
62
|
end
|
63
|
+
|
64
|
+
def format_non_unique_descriptions(non_unique_descriptions)
|
65
|
+
non_unique_descriptions.map { |description| "\"#{short_description(description)}\"" }.join(', ')
|
66
|
+
end
|
67
|
+
|
68
|
+
def short_description(description)
|
69
|
+
Models::Entry.short_description(string: description)
|
70
|
+
end
|
59
71
|
end
|
60
72
|
end
|
61
73
|
end
|
@@ -5,29 +5,20 @@ module Dsu
|
|
5
5
|
module Validators
|
6
6
|
class TimeValidator < ActiveModel::Validator
|
7
7
|
def validate(record)
|
8
|
-
|
9
|
-
raise 'options[:fields] is not an Array.' unless options[:fields].is_a? Array
|
10
|
-
raise 'options[:fields] elements are not Symbols.' unless options[:fields].all?(Symbol)
|
8
|
+
time = record.time
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
record.errors.add(field, :blank)
|
17
|
-
next
|
18
|
-
end
|
19
|
-
|
20
|
-
unless time.is_a?(Time)
|
21
|
-
record.errors.add(field, 'is the wrong object type. ' \
|
22
|
-
"\"Time\" was expected, but \"#{time.class}\" was received.")
|
23
|
-
next
|
24
|
-
end
|
10
|
+
if time.nil?
|
11
|
+
record.errors.add(:time, :blank)
|
12
|
+
return
|
13
|
+
end
|
25
14
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
15
|
+
unless time.is_a?(Time)
|
16
|
+
record.errors.add(:time, 'is the wrong object type. ' \
|
17
|
+
"\"Time\" was expected, but \"#{time.class}\" was received.")
|
18
|
+
return
|
30
19
|
end
|
20
|
+
|
21
|
+
record.errors.add(:time, 'is not in localtime format.') if time.utc?
|
31
22
|
end
|
32
23
|
end
|
33
24
|
end
|
data/lib/dsu/version.rb
CHANGED