dsu 0.1.0.alpha.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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