dsu 0.1.0.alpha.5 → 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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'deco_lite'
3
+ require 'active_model'
4
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'
@@ -13,30 +13,28 @@ require_relative 'entry'
13
13
 
14
14
  module Dsu
15
15
  module Models
16
- class EntryGroup < DecoLite::Model
16
+ # This class represents a group of entries for a given day. IOW,
17
+ # things someone might want to share at their daily standup (DSU).
18
+ class EntryGroup
19
+ include ActiveModel::Model
17
20
  extend Support::EntryGroupLoadable
18
21
  include Support::TimeFormatable
19
22
 
20
- validates_with Validators::EntriesValidator, fields: [:entries]
21
- validates_with Validators::TimeValidator, fields: [:time]
23
+ attr_accessor :time
24
+ attr_reader :entries
25
+
26
+ validates_with Validators::EntriesValidator
27
+ validates_with Validators::TimeValidator
22
28
 
23
29
  def initialize(time: nil, entries: [])
24
30
  raise ArgumentError, 'time is the wrong object type' unless time.is_a?(Time) || time.nil?
25
- raise ArgumentError, 'entries is the wrong object type' unless entries.is_a?(Array) || entries.nil?
26
-
27
- time ||= Time.now
28
- time = time.localtime if time.utc?
29
31
 
30
- entries ||= []
31
-
32
- super(hash: {
33
- time: time,
34
- entries: entries
35
- })
32
+ @time = ensure_local_time(time)
33
+ self.entries = entries || []
36
34
  end
37
35
 
38
36
  class << self
39
- def delete(time:, options: {})
37
+ def delete!(time:, options: {})
40
38
  Services::EntryGroupDeleterService.new(time: time, options: options).call
41
39
  end
42
40
 
@@ -46,7 +44,7 @@ module Dsu
46
44
  # return new(time: time) unless exists?(time: time)
47
45
 
48
46
  load(time: time).tap do |entry_group|
49
- entry_group.edit(options: options)
47
+ Services::EntryGroupEditorService.new(entry_group: entry_group, options: options).call
50
48
  end
51
49
  end
52
50
 
@@ -57,116 +55,63 @@ module Dsu
57
55
  # Loads the EntryGroup from the file system and returns an
58
56
  # instantiated EntryGroup object.
59
57
  def load(time: nil)
60
- new(**hydrated_entry_group_hash_for(time: time))
58
+ load_entry_group_file_for(time: time)
61
59
  end
62
60
 
63
61
  # This function returns a hash whose :time and :entries
64
62
  # key values are hydrated with instantiated Time and Entry
65
63
  # objects.
66
- def hydrated_entry_group_hash_for(time:)
67
- entry_group_hash = entry_group_hash_for(time: time)
68
- hydrate_entry_group_hash(entry_group_hash: entry_group_hash, time: time)
64
+ def load_entry_group_for(time:)
65
+ load_entry_group_file_for(time: time)
69
66
  end
67
+ end
70
68
 
71
- def unique?(entry:)
72
-
73
- end
69
+ def valid_unique_entries
70
+ entries&.select(&:valid?)&.uniq(&:description)
74
71
  end
75
72
 
76
- def required_fields
77
- %i[time entries]
73
+ def clone
74
+ clone = super
75
+
76
+ clone.entries = clone.entries.map(&:clone)
77
+ clone
78
78
  end
79
79
 
80
- def edit(options: {})
81
- Services::EntryGroupEditorService.new(entry_group: self, options: options).call
82
- self
80
+ def entries=(entries)
81
+ entries ||= []
82
+
83
+ raise ArgumentError, 'entries is the wrong object type' unless entries.is_a?(Array)
84
+ raise ArgumentError, 'entries contains the wrong object type' unless entries.all?(Entry)
85
+
86
+ @entries = entries.map(&:clone)
83
87
  end
84
88
 
85
89
  # Deletes the entry group file from the file system.
86
- def delete
87
- self.class.delete(time: time)
90
+ def delete!
91
+ self.class.delete!(time: time)
92
+ self.entries = []
88
93
  self
89
94
  end
90
95
 
91
- def entries?
92
- entries.any?
93
- end
94
-
95
96
  def save!
97
+ delete! and return if entries.empty?
98
+
96
99
  validate!
97
100
  Services::EntryGroupWriterService.new(entry_group: self).call
101
+ self
98
102
  end
99
103
 
100
104
  def to_h
101
- super.tap do |hash|
102
- hash[:entries] = hash[:entries].dup
103
- hash[:entries].each_with_index do |entry, index|
104
- hash[:entries][index] = entry.to_h
105
- end
106
- end
107
- end
108
-
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)
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
105
  {
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)
106
+ time: time.dup,
107
+ entries: entries.map(&:to_h)
143
108
  }
144
109
  end
145
110
 
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?
111
+ private
162
112
 
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)
113
+ def ensure_local_time(time)
114
+ time.nil? ? Time.now : time.dup.localtime
170
115
  end
171
116
  end
172
117
  end
@@ -6,6 +6,7 @@ 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
 
@@ -4,7 +4,10 @@ require_relative '../models/entry'
4
4
  require_relative '../support/colorable'
5
5
  require_relative '../support/say'
6
6
  require_relative '../support/time_formatable'
7
+ require_relative '../views/edited_entries/shared/errors'
8
+ require_relative '../views/shared/messages'
7
9
  require_relative 'configuration_loader_service'
10
+ require_relative 'stdout_redirector_service'
8
11
 
9
12
  module Dsu
10
13
  module Services
@@ -24,7 +27,7 @@ module Dsu
24
27
 
25
28
  def call
26
29
  edit_view = render_edit_view
27
- edit!(edit_view: edit_view)
30
+ edit edit_view
28
31
  # NOTE: Return the original entry group object as any permanent changes
29
32
  # will have been applied to it.
30
33
  entry_group
@@ -38,106 +41,54 @@ module Dsu
38
41
  # and edit it. The edits will be used to update the entry group.
39
42
  def render_edit_view
40
43
  say "Editing entry group #{formatted_time(time: entry_group.time)}...", HIGHLIGHT
41
- capture_stdxxx { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
44
+ StdoutRedirectorService.call { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
42
45
  end
43
46
 
44
- # Writes the temporary file contents to disk and opens it in the editor.
45
- def edit!(edit_view:)
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
+
46
53
  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]}';" \
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]}'; " \
49
64
  "the system error returned was: '#{$CHILD_STATUS}'.", ERROR
50
65
  say 'Either set the EDITOR environment variable ' \
51
66
  '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.
67
+ say 'Run `$ dsu help config` for more information:', ERROR
57
68
  end
58
-
59
- update_entry_group!(tmp_file_path: tmp_file_path)
60
69
  end
61
70
  end
62
71
 
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
72
+ def process_entry_group!(entry_group_with_edits)
73
+ if entry_group_with_edits.entries.empty?
74
+ entry_group.delete!
75
+ return
85
76
  end
86
77
 
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 }
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
91
82
  end
92
83
 
93
- # Save or delete any entries.
94
- entry_group.entries = entries
95
- entry_group.delete and return unless entry_group.entries?
96
-
84
+ # Make sure we're saving only valid, unique entries.
85
+ entry_group.entries = entry_group_with_edits.valid_unique_entries
97
86
  entry_group.save!
98
87
  end
99
88
 
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
89
+ def process_description?(description)
90
+ description = Models::Entry.clean_description(description)
91
+ !(description.blank? || description[0] == '#')
141
92
  end
142
93
 
143
94
  def configuration
@@ -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
@@ -6,6 +6,7 @@ module Dsu
6
6
  ABORTED = :red
7
7
  ERROR = :red
8
8
  HIGHLIGHT = :cyan
9
+ INFO = HIGHLIGHT
9
10
  SUCCESS = :green
10
11
  WARNING = :yellow
11
12
  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 entry_group_hash_for(time:)
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
- return JSON.parse(entry_group_json, symbolize_names: true).tap do |hash|
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(entry_group_hash:, time:)
43
- time = entry_group_hash.fetch(:time, 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 = entry_group_hash.fetch(: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,56 +7,67 @@ require_relative '../support/field_errors'
7
7
  module Dsu
8
8
  module Validators
9
9
  class EntriesValidator < ActiveModel::Validator
10
- include Dsu::Support::FieldErrors
10
+ include Support::FieldErrors
11
11
 
12
12
  def validate(record)
13
- raise 'options[:fields] is not defined.' unless options.key? :fields
14
- raise 'options[:fields] is not an Array.' unless options[:fields].is_a? Array
15
- raise 'options[:fields] elements are not Symbols.' unless options[:fields].all?(Symbol)
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
- options[:fields].each do |field|
18
- entries = record.send(field)
18
+ validate_entry_types record
19
+ validate_unique_entry record
20
+ validate_entries record
21
+ end
19
22
 
20
- unless entries.is_a?(Array)
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
- validate_entry_types field, entries, record
27
- validate_unique_entry_attr :uuid, field, entries, record
28
- validate_unique_entry_attr :description, field, entries, record
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)
29
32
  end
30
33
  end
31
34
 
32
- private
35
+ def validate_unique_entry(record)
36
+ return unless record.entries.is_a? Array
33
37
 
34
- def validate_entry_types(field, entries, record)
35
- entries.each do |entry|
36
- next if entry.is_a? Dsu::Models::Entry
38
+ entry_objects = record.entries.select { |entry| entry.is_a?(Dsu::Models::Entry) }
37
39
 
38
- record.errors.add(field, 'entry Array element is the wrong object type. ' \
39
- "\"Entry\" was expected, but \"#{entry.class}\" was received.",
40
- type: Dsu::Support::FieldErrors::FIELD_TYPE_ERROR)
40
+ descriptions = entry_objects.map(&:description)
41
+ return if descriptions.uniq.length == descriptions.length
41
42
 
42
- next
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)
43
48
  end
44
49
  end
45
50
 
46
- def validate_unique_entry_attr(attr, field, entries, record)
47
- return unless entries.is_a? Array
48
-
49
- 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?
50
54
 
51
- attrs = entry_objects.map(&attr)
52
- return if attrs.uniq.length == attrs.length
55
+ entries.each do |entry|
56
+ next if entry.valid?
53
57
 
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(', ')}.",
57
- type: Dsu::Support::FieldErrors::FIELD_DUPLICATE_ERROR)
58
+ entry.errors.each do |error|
59
+ record.errors.add(:entries_entry, error.full_message)
60
+ end
58
61
  end
59
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
60
71
  end
61
72
  end
62
73
  end
@@ -5,29 +5,20 @@ module Dsu
5
5
  module Validators
6
6
  class TimeValidator < ActiveModel::Validator
7
7
  def validate(record)
8
- raise 'options[:fields] is not defined.' unless options.key? :fields
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
- options[:fields].each do |field|
13
- time = record.send(field)
14
-
15
- if time.nil?
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
- if time.utc?
27
- record.errors.add(field, 'is not in localtime format.')
28
- next
29
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dsu
4
- VERSION = '0.1.0.alpha.5'
4
+ VERSION = '1.0.0'
5
5
  end