dsu 1.2.1 → 2.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -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 +70 -25
  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)