dsu 1.2.1 → 2.0.1

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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -21
  3. data/Gemfile.lock +7 -7
  4. data/README.md +28 -35
  5. data/bin/console +23 -1
  6. data/bin/dsu +3 -0
  7. data/bin/setup +14 -3
  8. data/exe/dsu +23 -1
  9. data/exe/dsu_migrate.rb +43 -0
  10. data/lib/core/ruby/color_theme_colors.rb +16 -0
  11. data/lib/core/ruby/color_theme_mode.rb +42 -0
  12. data/lib/core/ruby/not_today.rb +7 -0
  13. data/lib/core/ruby/wrap_and_join.rb +31 -0
  14. data/lib/dsu/base_cli.rb +19 -23
  15. data/lib/dsu/cli.rb +47 -37
  16. data/lib/dsu/command_services/add_entry_service.rb +10 -21
  17. data/lib/dsu/crud/json_file.rb +139 -0
  18. data/lib/dsu/crud/raw_json_file.rb +51 -0
  19. data/lib/dsu/env.rb +21 -0
  20. data/lib/dsu/migration/service.rb +196 -0
  21. data/lib/dsu/migration/version.rb +7 -0
  22. data/lib/dsu/models/color_theme.rb +270 -0
  23. data/lib/dsu/models/configuration.rb +160 -0
  24. data/lib/dsu/models/entry.rb +6 -2
  25. data/lib/dsu/models/entry_group.rb +143 -42
  26. data/lib/dsu/models/migration_version.rb +48 -0
  27. data/lib/dsu/presenters/base_presenter.rb +32 -0
  28. data/lib/dsu/presenters/color_theme_presenter.rb +50 -0
  29. data/lib/dsu/presenters/color_theme_show_presenter.rb +49 -0
  30. data/lib/dsu/presenters/configuration_presenter.rb +45 -0
  31. data/lib/dsu/presenters/entry_group_presenter.rb +35 -0
  32. data/lib/dsu/presenters/entry_presenter.rb +25 -0
  33. data/lib/dsu/services/color_theme/hydrator_service.rb +42 -0
  34. data/lib/dsu/services/configuration/hydrator_service.rb +42 -0
  35. data/lib/dsu/services/entry/hydrator_service.rb +33 -0
  36. data/lib/dsu/services/entry_group/editor_service.rb +107 -0
  37. data/lib/dsu/services/entry_group/hydrator_service.rb +37 -0
  38. data/lib/dsu/services/migration_version/hydrator_service.rb +36 -0
  39. data/lib/dsu/services/stderr_redirector_service.rb +27 -0
  40. data/lib/dsu/services/temp_file/reader_service.rb +33 -0
  41. data/lib/dsu/services/temp_file/writer_service.rb +35 -0
  42. data/lib/dsu/subcommands/base_subcommand.rb +14 -0
  43. data/lib/dsu/subcommands/config.rb +92 -32
  44. data/lib/dsu/subcommands/edit.rb +3 -3
  45. data/lib/dsu/subcommands/list.rb +70 -93
  46. data/lib/dsu/subcommands/theme.rb +159 -0
  47. data/lib/dsu/support/ask.rb +14 -19
  48. data/lib/dsu/support/color_themable.rb +34 -0
  49. data/lib/dsu/support/command_help_colorizeable.rb +27 -0
  50. data/lib/dsu/support/command_hookable.rb +60 -0
  51. data/lib/dsu/support/command_options/dsu_times.rb +32 -21
  52. data/lib/dsu/support/command_options/time.rb +7 -1
  53. data/lib/dsu/support/command_options/time_mneumonic.rb +7 -1
  54. data/lib/dsu/support/descriptable.rb +6 -4
  55. data/lib/dsu/support/entry_group_viewable.rb +28 -4
  56. data/lib/dsu/support/fileable.rb +94 -0
  57. data/lib/dsu/support/presentable.rb +11 -0
  58. data/lib/dsu/support/subcommand_help_colorizeable.rb +27 -0
  59. data/lib/dsu/support/time_comparable.rb +19 -0
  60. data/lib/dsu/support/time_formatable.rb +12 -0
  61. data/lib/dsu/support/times_sortable.rb +48 -14
  62. data/lib/dsu/support/utils.rb +11 -0
  63. data/lib/dsu/validators/color_theme_validator.rb +74 -0
  64. data/lib/dsu/validators/entries_validator.rb +4 -8
  65. data/lib/dsu/validators/version_validator.rb +29 -0
  66. data/lib/dsu/version.rb +2 -1
  67. data/lib/dsu/views/color_theme/index.rb +62 -0
  68. data/lib/dsu/views/color_theme/show.rb +106 -0
  69. data/lib/dsu/views/configuration/show.rb +41 -0
  70. data/lib/dsu/views/entry_group/edit.rb +3 -5
  71. data/lib/dsu/views/entry_group/shared/no_entries_to_display.rb +41 -0
  72. data/lib/dsu/views/entry_group/show.rb +16 -15
  73. data/lib/dsu/views/shared/error.rb +17 -0
  74. data/lib/dsu/views/shared/info.rb +17 -0
  75. data/lib/dsu/views/shared/message.rb +85 -0
  76. data/lib/dsu/views/shared/model_errors.rb +31 -0
  77. data/lib/dsu/views/shared/success.rb +17 -0
  78. data/lib/dsu/views/shared/warning.rb +17 -0
  79. data/lib/dsu.rb +22 -1
  80. data/lib/seed_data/themes/cherry.json +79 -0
  81. data/lib/seed_data/themes/default.json +79 -0
  82. data/lib/seed_data/themes/lemon.json +79 -0
  83. data/lib/seed_data/themes/matrix.json +79 -0
  84. data/lib/seed_data/themes/whiteout.json +79 -0
  85. metadata +68 -23
  86. data/lib/dsu/core/ruby/not_today.rb +0 -11
  87. data/lib/dsu/services/ai/tense_translator_service.rb +0 -63
  88. data/lib/dsu/services/configuration_loader_service.rb +0 -55
  89. data/lib/dsu/services/entry_group_deleter_service.rb +0 -31
  90. data/lib/dsu/services/entry_group_editor_service.rb +0 -96
  91. data/lib/dsu/services/entry_group_hydrator_service.rb +0 -43
  92. data/lib/dsu/services/entry_group_reader_service.rb +0 -36
  93. data/lib/dsu/services/entry_group_writer_service.rb +0 -46
  94. data/lib/dsu/services/entry_hydrator_service.rb +0 -35
  95. data/lib/dsu/services/temp_file_reader_service.rb +0 -31
  96. data/lib/dsu/services/temp_file_writer_service.rb +0 -33
  97. data/lib/dsu/support/colorable.rb +0 -14
  98. data/lib/dsu/support/configurable.rb +0 -15
  99. data/lib/dsu/support/configuration.rb +0 -112
  100. data/lib/dsu/support/entry_group_fileable.rb +0 -49
  101. data/lib/dsu/support/entry_group_loadable.rb +0 -49
  102. data/lib/dsu/support/folder_locations.rb +0 -21
  103. data/lib/dsu/support/say.rb +0 -40
  104. data/lib/dsu/views/edited_entries/shared/errors.rb +0 -39
  105. data/lib/dsu/views/shared/messages.rb +0 -56
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../crud/json_file'
5
+ require_relative '../migration/version'
6
+ require_relative '../support/color_themable'
7
+ require_relative '../support/descriptable'
8
+ require_relative '../support/fileable'
9
+ require_relative '../support/presentable'
10
+ require_relative '../validators/color_theme_validator'
11
+ require_relative '../validators/description_validator'
12
+ require_relative '../validators/version_validator'
13
+ require_relative 'configuration'
14
+
15
+ module Dsu
16
+ module Models
17
+ # This class represents a dsu color theme.
18
+ class ColorTheme < Crud::JsonFile
19
+ include Support::ColorThemable
20
+ include Support::Descriptable
21
+ include Support::Fileable
22
+ include Support::Presentable
23
+
24
+ VERSION = Migration::VERSION
25
+
26
+ DEFAULT_THEME_NAME = 'default'
27
+ # Theme colors key/value pair format:
28
+ # <key>: { color: <color> [, mode: <mode>] [, background: <background>] }
29
+ # Where <color> (required) == any color represented in the colorize gem `String.colors` array.
30
+ # <mode> (optional, default is :default) == any mode represented in the colorize gem `String.modes` array.
31
+ # <background> (optional, default is :default) == any color represented in the colorize gem
32
+ # `String.colors` array.
33
+ DEFAULT_THEME_COLORS = {
34
+ help: { color: :cyan },
35
+ dsu_header: { color: :white, mode: :bold, background: :cyan },
36
+ dsu_footer: { color: :cyan },
37
+ header: { color: :cyan, mode: :bold },
38
+ subheader: { color: :cyan, mode: :underline },
39
+ body: { color: :cyan },
40
+ footer: { color: :light_cyan },
41
+ date: { color: :cyan, mode: :bold },
42
+ index: { color: :light_cyan },
43
+ # Status colors.
44
+ info: { color: :cyan },
45
+ success: { color: :green },
46
+ warning: { color: :yellow },
47
+ error: { color: :light_yellow, background: :red },
48
+ # Prompts
49
+ prompt: { color: :cyan, mode: :bold },
50
+ prompt_options: { color: :white, mode: :bold }
51
+ }.freeze
52
+ DEFAULT_THEME = {
53
+ version: VERSION,
54
+ description: 'Default theme.'
55
+ }.merge(DEFAULT_THEME_COLORS).freeze
56
+
57
+ # TODO: Validate other attrs.
58
+ validates_with Validators::DescriptionValidator
59
+ validates_with Validators::ColorThemeValidator
60
+ validates_with Validators::VersionValidator
61
+
62
+ attr_reader :theme_name, :options
63
+
64
+ def initialize(theme_name:, theme_hash: nil, options: {})
65
+ raise ArgumentError, 'theme_name is nil.' if theme_name.nil?
66
+ raise ArgumentError, "theme_name is the wrong object type: \"#{theme_name}\"." unless theme_name.is_a?(String)
67
+ unless theme_hash.is_a?(Hash) || theme_hash.nil?
68
+ raise ArgumentError, "theme_hash is the wrong object type: \"#{theme_hash}\"."
69
+ end
70
+
71
+ FileUtils.mkdir_p themes_folder
72
+
73
+ @theme_name = theme_name
74
+ @options = options || {}
75
+
76
+ super(self.class.send(:themes_path_for, theme_name: @theme_name))
77
+
78
+ theme_hash ||= DEFAULT_THEME.merge(description: "#{@theme_name.capitalize} theme")
79
+
80
+ # Color themes I expect will change a lot, so we're using
81
+ # a little meta-programming here to dynamically create
82
+ # public attr_readers and private attr_writers based on the
83
+ # keys in DEFAULT_THEME, then assign those attributes from
84
+ # the values in theme_hash. theme_hash will be guaranteed to
85
+ # have the same keys as DEFAULT_THEME.keys at this point
86
+ # because we called ensure_theme_hash! above.
87
+ DEFAULT_THEME.each_key do |attr|
88
+ self.class.class_eval do
89
+ attr_reader attr
90
+ attr_writer attr
91
+ private "#{attr}="
92
+ end
93
+ attr_value = theme_hash[attr]
94
+ attr_value = attr_value.merge_default_colors if default_theme_color_keys.include?(attr)
95
+ send("#{attr}=", attr_value)
96
+ end
97
+ end
98
+
99
+ def delete
100
+ self.class.delete(theme_name: theme_name)
101
+ end
102
+
103
+ def delete!
104
+ self.class.delete!(theme_name: theme_name)
105
+ end
106
+
107
+ def exist?
108
+ self.class.exist?(theme_name: theme_name)
109
+ end
110
+
111
+ class << self
112
+ def all
113
+ Dir.glob("#{themes_folder}/*").map do |file_path|
114
+ theme_name = File.basename(file_path, '.*')
115
+ find(theme_name: theme_name)
116
+ end
117
+ end
118
+
119
+ def configuration
120
+ Models::Configuration.new
121
+ end
122
+
123
+ def current
124
+ theme_name = configuration.theme_name
125
+ return unless exist?(theme_name: theme_name)
126
+
127
+ find(theme_name: theme_name)
128
+ end
129
+
130
+ # Returns the current color theme if it exists; otherwise,
131
+ # it returns the default color theme.
132
+ def current_or_default
133
+ current || default
134
+ end
135
+
136
+ def default
137
+ new(theme_name: DEFAULT_THEME_NAME, theme_hash: DEFAULT_THEME)
138
+ end
139
+
140
+ def delete(theme_name:)
141
+ superclass.delete(file_path: themes_path_for(theme_name: theme_name))
142
+ end
143
+
144
+ def delete!(theme_name:)
145
+ superclass.delete!(file_path: themes_path_for(theme_name: theme_name))
146
+ end
147
+
148
+ def ensure_color_theme_color_defaults_for(theme_hash: DEFAULT_THEME)
149
+ theme_hash = theme_hash.dup
150
+
151
+ theme_hash.each_pair do |key, value|
152
+ next unless default_theme_color_keys.include?(key)
153
+
154
+ theme_hash[key] = value.merge_default_colors
155
+ end
156
+ theme_hash
157
+ end
158
+
159
+ def exist?(theme_name:)
160
+ superclass.exist?(file_path: themes_path_for(theme_name: theme_name))
161
+ end
162
+
163
+ def find(theme_name:)
164
+ theme_hash = read!(file_path: themes_path_for(theme_name: theme_name))
165
+ Services::ColorTheme::HydratorService.new(theme_name: theme_name, theme_hash: theme_hash).call
166
+ end
167
+
168
+ def find_or_create(theme_name:)
169
+ return find(theme_name: theme_name) if exist?(theme_name: theme_name)
170
+
171
+ new(theme_name: theme_name).tap(&:write!)
172
+ end
173
+
174
+ def find_or_initialize(theme_name:)
175
+ return find(theme_name: theme_name) if exist?(theme_name: theme_name)
176
+
177
+ new(theme_name: theme_name)
178
+ end
179
+
180
+ # TODO: Unused?
181
+ # def build_color_theme(theme_name:, base_color:, description:)
182
+ # theme_hash = Models::ColorTheme.send(:replace, color_theme: default,
183
+ # replace_color: :cyan, with_color: base_color).tap do |hash|
184
+ # hash[:description] = description
185
+ # end
186
+ # new(theme_name: theme_name, theme_hash: theme_hash)
187
+ # end
188
+
189
+ private
190
+
191
+ def default_theme_color_keys
192
+ DEFAULT_THEME_COLORS.keys
193
+ end
194
+
195
+ def replace(color_theme:, replace_color:, with_color:)
196
+ colors_theme_hash = color_theme.to_theme_colors_h.tap do |hash|
197
+ hash.each_key do |key|
198
+ hash[key] = replace_color(theme_color: hash[key],
199
+ replace_color: replace_color, with_color: with_color)
200
+ end
201
+ end
202
+ DEFAULT_THEME.merge(colors_theme_hash)
203
+ end
204
+
205
+ def replace_color(theme_color:, replace_color:, with_color:)
206
+ %i[color background].each do |color_type|
207
+ color = theme_color[color_type].to_s.sub(replace_color.to_s, with_color.to_s)
208
+ theme_color[color_type] = color.sub('light_light_', 'light_').to_sym
209
+ end
210
+ theme_color
211
+ end
212
+
213
+ # If the color theme is deleted (deleted_theme_name) and the current
214
+ # theme_name in the configuration is the same as the deleted theme,
215
+ # we need to reset the configuration theme to the default theme.
216
+ def reset_default_configuration_color_theme_if!(deleted_theme_name:)
217
+ config = configuration
218
+ return if config.theme_name == self::DEFAULT_THEME_NAME
219
+ return unless config.theme_name == deleted_theme_name
220
+ return unless config.exist?
221
+
222
+ config.theme_name = self::DEFAULT_THEME_NAME
223
+ config.write!
224
+ end
225
+
226
+ def themes_path_for(theme_name:)
227
+ Support::Fileable.themes_path(theme_name: theme_name)
228
+ end
229
+ end
230
+
231
+ def to_h
232
+ {}.tap do |hash|
233
+ DEFAULT_THEME.each_key do |key|
234
+ hash[key] = public_send(key)
235
+ end
236
+ end
237
+ end
238
+
239
+ def to_theme_colors_h
240
+ {}.tap do |hash|
241
+ DEFAULT_THEME_COLORS.each_key do |key|
242
+ hash[key] = public_send(key)
243
+ end
244
+ end
245
+ end
246
+
247
+ def ==(other)
248
+ return false unless other.is_a?(self.class)
249
+ return false unless other.theme_name == theme_name
250
+
251
+ DEFAULT_THEME.keys.all? { |key| public_send(key) == other.public_send(key) }
252
+ end
253
+ alias eql? ==
254
+
255
+ def hash
256
+ DEFAULT_THEME.keys.map { |key| public_send(key) }.tap do |hashes|
257
+ hashes << theme_name.hash
258
+ end.hash
259
+ end
260
+
261
+ private
262
+
263
+ attr_writer :theme_name, :description
264
+
265
+ def default_theme_color_keys
266
+ @default_theme_color_keys ||= self.class.send(:default_theme_color_keys)
267
+ end
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require_relative '../crud/json_file'
5
+ require_relative '../migration/version'
6
+ require_relative '../support/fileable'
7
+ require_relative '../support/presentable'
8
+ require_relative '../validators/version_validator'
9
+
10
+ module Dsu
11
+ module Models
12
+ # This class represents the dsu configuration.
13
+ class Configuration < Crud::JsonFile
14
+ include Support::Fileable
15
+ include Support::Presentable
16
+
17
+ VERSION = Migration::VERSION
18
+
19
+ DEFAULT_CONFIGURATION = {
20
+ version: VERSION,
21
+ # The default editor to use when editing entry groups if the EDITOR
22
+ # environment variable on your system is not set. On nix systmes,
23
+ # the default editor is`nano`. You need to change this default on
24
+ # Windows systems.
25
+ editor: 'nano',
26
+ # The order by which entries should be displayed by default:
27
+ # :asc or :desc, ascending or descending, respectively.
28
+ entries_display_order: :desc,
29
+ carry_over_entries_to_today: false,
30
+ # If true, when using dsu commands that list date ranges (e.g.
31
+ # `dsu list dates`), the displayed list will include dates that
32
+ # have no dsu entries. If false, the displayed list will only
33
+ # include dates that have dsu entries.
34
+ # For all other `dsu list` commands, if true, this option will
35
+ # behave in the aforementioned manner. If false, the displayed
36
+ # list will unconditionally display the first and last dates
37
+ # regardless of whether or not the DSU date has entries or not;
38
+ # all other dates will not be displayed if the DSU date has no
39
+ # entries.
40
+ include_all: false,
41
+ # Themes
42
+ # The currently selected color theme. Should be equal to
43
+ # Models::ColorTheme::DEFAULT_THEME_NAME or the name of a custom
44
+ # theme (with the same file name) that resides in the themes_folder.
45
+ theme_name: 'default'
46
+ }.freeze
47
+
48
+ validates_with Validators::VersionValidator
49
+ validates :editor, presence: true
50
+ validates :entries_display_order, presence: true,
51
+ inclusion: { in: %i[asc desc], message: 'must be :asc or :desc' }
52
+ validates :carry_over_entries_to_today, inclusion: { in: [true, false], message: 'must be true or false' }
53
+ validates :include_all, inclusion: { in: [true, false], message: 'must be true or false' }
54
+ validates :theme_name, presence: true
55
+ validate :validate_theme_file
56
+
57
+ attr_accessor :version,
58
+ :editor,
59
+ :entries_display_order,
60
+ :carry_over_entries_to_today,
61
+ :include_all,
62
+ :theme_name
63
+
64
+ attr_reader :options
65
+
66
+ def initialize(options: {})
67
+ super(config_path)
68
+
69
+ FileUtils.mkdir_p config_folder
70
+
71
+ @options = options || {}
72
+ reload
73
+
74
+ write! unless exist?
75
+ end
76
+
77
+ # Temporarily sets the configuration to the given config_hash.
78
+ # To reset the configuration to its original state, call #reload
79
+ def replace!(config_hash: {})
80
+ raise ArgumentError, 'config_hash is nil.' if config_hash.nil?
81
+ raise ArgumentError, "config_hash must be a Hash: \"#{config_hash}\"." unless config_hash.is_a?(Hash)
82
+
83
+ assign_attributes_from config_hash.dup
84
+
85
+ self
86
+ end
87
+
88
+ # Restores the configuration to its original state from disk.
89
+ def reload
90
+ file_hash = if exist?
91
+ read do |config_hash|
92
+ hydrated_hash = Services::Configuration::HydratorService.new(config_hash: config_hash).call
93
+ config_hash.merge!(hydrated_hash)
94
+ end
95
+ else
96
+ DEFAULT_CONFIGURATION.dup
97
+ end
98
+
99
+ assign_attributes_from file_hash
100
+
101
+ self
102
+ end
103
+
104
+ def carry_over_entries_to_today?
105
+ carry_over_entries_to_today
106
+ end
107
+
108
+ def to_h
109
+ {
110
+ version: version,
111
+ editor: editor,
112
+ entries_display_order: entries_display_order,
113
+ carry_over_entries_to_today: carry_over_entries_to_today,
114
+ include_all: include_all,
115
+ theme_name: theme_name
116
+ }
117
+ end
118
+
119
+ # Override == and hash so that we can compare objects based
120
+ # on attributes alone. This is also useful for comparing objects
121
+ # in an array, for example.
122
+ def ==(other)
123
+ return false unless other.is_a?(Configuration)
124
+
125
+ to_h == other.to_h
126
+ end
127
+ alias eql? ==
128
+
129
+ def hash
130
+ DEFAULT_CONFIGURATION.each_key.map do |key|
131
+ public_send(key)
132
+ end.hash
133
+ end
134
+
135
+ def merge(hash)
136
+ replace!(config_hash: to_h.merge(hash))
137
+ end
138
+
139
+ private
140
+
141
+ def assign_attributes_from(config_hash)
142
+ @version = config_hash.fetch(:version, VERSION)
143
+ @editor = config_hash.fetch(:editor, DEFAULT_CONFIGURATION[:editor])
144
+ @entries_display_order = config_hash.fetch(:entries_display_order,
145
+ DEFAULT_CONFIGURATION[:entries_display_order])
146
+ @carry_over_entries_to_today = config_hash.fetch(:carry_over_entries_to_today,
147
+ DEFAULT_CONFIGURATION[:carry_over_entries_to_today])
148
+ @include_all = config_hash.fetch(:include_all, DEFAULT_CONFIGURATION[:include_all])
149
+ @theme_name = config_hash.fetch(:theme_name, DEFAULT_CONFIGURATION[:theme_name])
150
+ end
151
+
152
+ def validate_theme_file
153
+ theme_path = themes_path(theme_name: theme_name)
154
+ return if File.exist?(theme_path)
155
+
156
+ errors.add(:base, "Theme file \"#{theme_path}\" does not exist")
157
+ end
158
+ end
159
+ end
160
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'active_model'
4
4
  require_relative '../support/descriptable'
5
+ require_relative '../support/presentable'
5
6
  require_relative '../validators/description_validator'
6
7
 
7
8
  module Dsu
@@ -11,15 +12,18 @@ module Dsu
11
12
  class Entry
12
13
  include ActiveModel::Model
13
14
  include Support::Descriptable
15
+ include Support::Presentable
14
16
 
15
17
  validates_with Validators::DescriptionValidator
16
18
 
17
- attr_reader :description
19
+ attr_reader :description, :options
18
20
 
19
- def initialize(description:)
21
+ def initialize(description:, options: {})
20
22
  raise ArgumentError, 'description is the wrong object type' unless description.is_a?(String)
21
23
 
24
+ # Make sure to call the setter method so that the description is cleaned up.
22
25
  self.description = description
26
+ @options = options || {}
23
27
  end
24
28
 
25
29
  class << self
@@ -1,64 +1,76 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_model'
4
- require_relative '../services/entry_group_editor_service'
5
- require_relative '../services/entry_group_deleter_service'
6
- require_relative '../services/entry_group_reader_service'
7
- require_relative '../services/entry_group_writer_service'
8
- require_relative '../support/entry_group_loadable'
4
+ require_relative '../crud/json_file'
5
+ require_relative '../migration/version'
6
+ require_relative '../services/entry_group/editor_service'
7
+ require_relative '../support/fileable'
8
+ require_relative '../support/presentable'
9
+ require_relative '../support/time_comparable'
9
10
  require_relative '../support/time_formatable'
10
11
  require_relative '../validators/entries_validator'
11
12
  require_relative '../validators/time_validator'
13
+ require_relative '../validators/version_validator'
12
14
  require_relative 'entry'
13
15
 
14
16
  module Dsu
15
17
  module Models
16
18
  # This class represents a group of entries for a given day. IOW,
17
19
  # things someone might want to share at their daily standup (DSU).
18
- class EntryGroup
19
- extend Support::EntryGroupLoadable
20
- include ActiveModel::Model
20
+ class EntryGroup < Crud::JsonFile
21
+ include Support::Fileable
22
+ include Support::Presentable
23
+ include Support::TimeComparable
21
24
  include Support::TimeFormatable
22
25
 
23
- attr_accessor :time
24
- attr_reader :entries
26
+ ENTRIES_FILE_NAME_REGEX = /\d{4}-\d{2}-\d{2}.json/
27
+ ENTRIES_FILE_NAME_TIME_REGEX = /\d{4}-\d{2}-\d{2}/
28
+ VERSION = Migration::VERSION
29
+
30
+ attr_accessor :time, :version
31
+ attr_reader :entries, :options
25
32
 
26
33
  validates_with Validators::EntriesValidator
27
34
  validates_with Validators::TimeValidator
35
+ validates_with Validators::VersionValidator
28
36
 
29
- def initialize(time: nil, entries: [])
37
+ def initialize(time: nil, entries: nil, version: nil, options: {})
30
38
  raise ArgumentError, 'time is the wrong object type' unless time.is_a?(Time) || time.nil?
39
+ raise ArgumentError, 'version is the wrong object type' unless version.is_a?(Integer) || version.nil?
40
+
41
+ FileUtils.mkdir_p(entries_folder)
31
42
 
32
43
  @time = ensure_local_time(time)
44
+
45
+ super(entries_path(time: @time))
46
+
47
+ @version = version || VERSION
33
48
  self.entries = entries || []
49
+ @options = options || {}
34
50
  end
35
51
 
36
- class << self
37
- def delete!(time:, options: {})
38
- Services::EntryGroupDeleterService.new(time: time, options: options).call
39
- end
40
-
41
- def edit(time:, options: {})
42
- # NOTE: Uncomment this line to prohibit edits on
43
- # Entry Groups that do not exist (i.e. have no entries).
44
- # return new(time: time) unless exists?(time: time)
52
+ # Override == and hash so that we can compare Entry Group objects.
53
+ def ==(other)
54
+ return false unless other.is_a?(EntryGroup) &&
55
+ version == other.version &&
56
+ time_equal?(other_time: other.time)
45
57
 
46
- load(time: time).tap do |entry_group|
47
- Services::EntryGroupEditorService.new(entry_group: entry_group, options: options).call
48
- end
49
- end
58
+ entries == other.entries
59
+ end
60
+ alias eql? ==
50
61
 
51
- def exists?(time:)
52
- Dsu::Services::EntryGroupReaderService.entry_group_file_exists?(time: time)
53
- end
62
+ def clone
63
+ self.class.new(time: time, entries: entries.map(&:clone), version: version)
54
64
  end
55
65
 
56
- def valid_unique_entries
57
- entries&.select(&:valid?)&.uniq(&:description)
66
+ def delete
67
+ self.class.delete(time: time)
68
+ entries.clear
58
69
  end
59
70
 
60
- def clone
61
- self.class.new(time: time, entries: entries.map(&:clone))
71
+ def delete!
72
+ self.class.delete!(time: time)
73
+ entries.clear
62
74
  end
63
75
 
64
76
  def entries=(entries)
@@ -70,32 +82,121 @@ module Dsu
70
82
  @entries = entries.map(&:clone)
71
83
  end
72
84
 
73
- # Deletes the entry group file from the file system.
74
- def delete!
75
- self.class.delete!(time: time)
76
- self.entries = []
77
- self
85
+ def exist?
86
+ self.class.exist?(time: time)
87
+ end
88
+
89
+ def hash
90
+ entries.map(&:hash).tap do |hashes|
91
+ hashes << version.hash
92
+ hashes << time_equal_compare_string_for(time: time)
93
+ end.hash
78
94
  end
79
95
 
80
96
  def time_formatted
81
97
  formatted_time(time: time)
82
98
  end
83
99
 
84
- def save!
85
- delete! and return if entries.empty?
86
-
87
- validate!
88
- Services::EntryGroupWriterService.new(entry_group: self).call
89
- self
100
+ def time_yyyy_mm_dd
101
+ yyyy_mm_dd(time: time)
90
102
  end
91
103
 
92
104
  def to_h
93
105
  {
106
+ version: version,
94
107
  time: time.dup,
95
108
  entries: entries.map(&:to_h)
96
109
  }
97
110
  end
98
111
 
112
+ def valid_unique_entries
113
+ entries&.select(&:valid?)&.uniq(&:description)
114
+ end
115
+
116
+ class << self
117
+ def all
118
+ entry_files.filter_map do |file_path|
119
+ entry_file_name = File.basename(file_path)
120
+ next unless entry_file_name.match?(ENTRIES_FILE_NAME_REGEX)
121
+
122
+ entry_date = File.basename(entry_file_name, '.*')
123
+ file_path = entries_path_for(time: Time.parse(entry_date))
124
+ entry_group_hash = read!(file_path: file_path)
125
+ Services::EntryGroup::HydratorService.new(entry_group_hash: entry_group_hash).call
126
+ end
127
+ end
128
+
129
+ def any?
130
+ entry_files.any? do |file_path|
131
+ entry_date = File.basename(file_path, '.*')
132
+ entry_date.match?(ENTRIES_FILE_NAME_TIME_REGEX)
133
+ end
134
+ end
135
+
136
+ def delete(time:)
137
+ superclass.delete(file_path: entries_path_for(time: time))
138
+ end
139
+
140
+ def delete!(time:)
141
+ superclass.delete!(file_path: entries_path_for(time: time))
142
+ end
143
+
144
+ def edit(time:, options: {})
145
+ # NOTE: Uncomment this line to prohibit edits on
146
+ # Entry Groups that do not exist (i.e. have no entries).
147
+ # return new(time: time) unless exists?(time: time)
148
+
149
+ find_or_initialize(time: time).tap do |entry_group|
150
+ Services::EntryGroup::EditorService.new(entry_group: entry_group, options: options).call
151
+ end
152
+ end
153
+
154
+ def exist?(time:)
155
+ superclass.exist?(file_path: entries_path_for(time: time))
156
+ end
157
+
158
+ def find(time:)
159
+ file_path = entries_path_for(time: time)
160
+ entry_group_hash = read!(file_path: file_path)
161
+ Services::EntryGroup::HydratorService.new(entry_group_hash: entry_group_hash).call
162
+ end
163
+
164
+ def find_or_create(time:)
165
+ find_or_initialize(time: time).tap do |entry_group|
166
+ entry_group.write! unless entry_group.exist?
167
+ end
168
+ end
169
+
170
+ def find_or_initialize(time:)
171
+ file_path = entries_path_for(time: time)
172
+ read(file_path: file_path) do |entry_group_hash|
173
+ Services::EntryGroup::HydratorService.new(entry_group_hash: entry_group_hash).call
174
+ end || new(time: time)
175
+ end
176
+
177
+ def write(file_data:, file_path:)
178
+ delete(file_path: file_path) and return true if file_data[:entries].empty?
179
+
180
+ super
181
+ end
182
+
183
+ def write!(file_data:, file_path:)
184
+ delete!(file_path: file_path) and return if file_data[:entries].empty?
185
+
186
+ super
187
+ end
188
+
189
+ private
190
+
191
+ def entries_path_for(time:)
192
+ Support::Fileable.entries_path(time: time)
193
+ end
194
+
195
+ def entry_files
196
+ Dir.glob("#{entries_folder}/*")
197
+ end
198
+ end
199
+
99
200
  private
100
201
 
101
202
  def ensure_local_time(time)