dsu 0.1.0.alpha.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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