doto 0.0.1.pre.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (179) hide show
  1. checksums.yaml +7 -0
  2. data/.env.test +1 -0
  3. data/.reek.yml +20 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +206 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +7 -0
  8. data/CODE_OF_CONDUCT.md +84 -0
  9. data/Gemfile +30 -0
  10. data/Gemfile.lock +179 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +38 -0
  13. data/Rakefile +16 -0
  14. data/bin/console +36 -0
  15. data/bin/doto +3 -0
  16. data/bin/setup +18 -0
  17. data/exe/doto +33 -0
  18. data/lib/core/ruby/color_theme_colors.rb +16 -0
  19. data/lib/core/ruby/color_theme_mode.rb +42 -0
  20. data/lib/core/ruby/wrap_and_join.rb +31 -0
  21. data/lib/doto/base_cli.rb +56 -0
  22. data/lib/doto/cli.rb +131 -0
  23. data/lib/doto/command_services/add_entry_service.rb +50 -0
  24. data/lib/doto/crud/json_file.rb +161 -0
  25. data/lib/doto/env.rb +44 -0
  26. data/lib/doto/migration/base_service.rb +118 -0
  27. data/lib/doto/migration/migrator.rb +24 -0
  28. data/lib/doto/migration/raw_helpers/color_theme_hash.rb +13 -0
  29. data/lib/doto/migration/raw_helpers/configuration_hash.rb +15 -0
  30. data/lib/doto/migration/raw_helpers/entry_group_hash.rb +13 -0
  31. data/lib/doto/migration/raw_json_file.rb +15 -0
  32. data/lib/doto/migration/raw_json_files.rb +56 -0
  33. data/lib/doto/migration/v20230613121411/service.rb +94 -0
  34. data/lib/doto/migration/v20240210161248/service.rb +148 -0
  35. data/lib/doto/migration/version.rb +7 -0
  36. data/lib/doto/models/color_theme.rb +224 -0
  37. data/lib/doto/models/configuration.rb +185 -0
  38. data/lib/doto/models/entry.rb +63 -0
  39. data/lib/doto/models/entry_group.rb +223 -0
  40. data/lib/doto/models/migration_version.rb +49 -0
  41. data/lib/doto/models/project.rb +295 -0
  42. data/lib/doto/presenters/base_presenter.rb +32 -0
  43. data/lib/doto/presenters/base_presenter_ex.rb +15 -0
  44. data/lib/doto/presenters/color_theme_presenter.rb +52 -0
  45. data/lib/doto/presenters/color_theme_show_presenter.rb +55 -0
  46. data/lib/doto/presenters/configuration_presenter.rb +50 -0
  47. data/lib/doto/presenters/entry_group/list/date_presenter.rb +77 -0
  48. data/lib/doto/presenters/entry_group/list/dates_presenter.rb +60 -0
  49. data/lib/doto/presenters/entry_group/list/messages.rb +15 -0
  50. data/lib/doto/presenters/entry_group/list/nothing_to_list.rb +15 -0
  51. data/lib/doto/presenters/entry_group_presenter.rb +35 -0
  52. data/lib/doto/presenters/entry_presenter.rb +25 -0
  53. data/lib/doto/presenters/export/all_presenter.rb +44 -0
  54. data/lib/doto/presenters/export/dates_presenter.rb +55 -0
  55. data/lib/doto/presenters/import/all_presenter.rb +57 -0
  56. data/lib/doto/presenters/import/dates_presenter.rb +70 -0
  57. data/lib/doto/presenters/import/import_entry.rb +22 -0
  58. data/lib/doto/presenters/import/import_file.rb +33 -0
  59. data/lib/doto/presenters/project/create_presenter.rb +44 -0
  60. data/lib/doto/presenters/project/defaultable.rb +15 -0
  61. data/lib/doto/presenters/project/delete_by_number_presenter.rb +54 -0
  62. data/lib/doto/presenters/project/delete_presenter.rb +53 -0
  63. data/lib/doto/presenters/project/list_presenter.rb +24 -0
  64. data/lib/doto/presenters/project/rename_by_number_presenter.rb +63 -0
  65. data/lib/doto/presenters/project/rename_presenter.rb +57 -0
  66. data/lib/doto/presenters/project/use_by_number_presenter.rb +57 -0
  67. data/lib/doto/presenters/project/use_presenter.rb +56 -0
  68. data/lib/doto/services/color_theme/hydrator_service.rb +42 -0
  69. data/lib/doto/services/configuration/hydrator_service.rb +42 -0
  70. data/lib/doto/services/entry/hydrator_service.rb +33 -0
  71. data/lib/doto/services/entry_group/browse_service.rb +100 -0
  72. data/lib/doto/services/entry_group/counter_service.rb +32 -0
  73. data/lib/doto/services/entry_group/deleter_service.rb +35 -0
  74. data/lib/doto/services/entry_group/editor_service.rb +103 -0
  75. data/lib/doto/services/entry_group/exporter_service.rb +98 -0
  76. data/lib/doto/services/entry_group/hydrator_service.rb +37 -0
  77. data/lib/doto/services/entry_group/importer_service.rb +117 -0
  78. data/lib/doto/services/migration_version/hydrator_service.rb +36 -0
  79. data/lib/doto/services/project/hydrator_service.rb +40 -0
  80. data/lib/doto/services/project/rename_service.rb +70 -0
  81. data/lib/doto/services/stderr_redirector_service.rb +27 -0
  82. data/lib/doto/services/stdout_redirector_service.rb +27 -0
  83. data/lib/doto/services/temp_file/reader_service.rb +33 -0
  84. data/lib/doto/services/temp_file/writer_service.rb +35 -0
  85. data/lib/doto/subcommands/base_subcommand.rb +12 -0
  86. data/lib/doto/subcommands/browse.rb +49 -0
  87. data/lib/doto/subcommands/config.rb +81 -0
  88. data/lib/doto/subcommands/delete.rb +108 -0
  89. data/lib/doto/subcommands/edit.rb +48 -0
  90. data/lib/doto/subcommands/export.rb +62 -0
  91. data/lib/doto/subcommands/import.rb +72 -0
  92. data/lib/doto/subcommands/list.rb +95 -0
  93. data/lib/doto/subcommands/project.rb +146 -0
  94. data/lib/doto/subcommands/theme.rb +131 -0
  95. data/lib/doto/support/ask.rb +44 -0
  96. data/lib/doto/support/color_themable.rb +36 -0
  97. data/lib/doto/support/command_help_colorizeable.rb +34 -0
  98. data/lib/doto/support/command_hookable.rb +71 -0
  99. data/lib/doto/support/command_options/doto_times.rb +48 -0
  100. data/lib/doto/support/command_options/time.rb +84 -0
  101. data/lib/doto/support/command_options/time_mnemonic.rb +108 -0
  102. data/lib/doto/support/command_options/time_mnemonics.rb +16 -0
  103. data/lib/doto/support/descriptable.rb +29 -0
  104. data/lib/doto/support/entry_group_browsable.rb +104 -0
  105. data/lib/doto/support/field_errors.rb +11 -0
  106. data/lib/doto/support/fileable.rb +136 -0
  107. data/lib/doto/support/presentable.rb +11 -0
  108. data/lib/doto/support/project_file_system.rb +118 -0
  109. data/lib/doto/support/short_string.rb +24 -0
  110. data/lib/doto/support/time_comparable.rb +21 -0
  111. data/lib/doto/support/time_formatable.rb +65 -0
  112. data/lib/doto/support/times_sortable.rb +71 -0
  113. data/lib/doto/support/transform_project_name.rb +24 -0
  114. data/lib/doto/support/utils.rb +11 -0
  115. data/lib/doto/validators/color_theme_validator.rb +74 -0
  116. data/lib/doto/validators/description_validator.rb +51 -0
  117. data/lib/doto/validators/entries_validator.rb +77 -0
  118. data/lib/doto/validators/project_name_validator.rb +58 -0
  119. data/lib/doto/validators/time_validator.rb +25 -0
  120. data/lib/doto/validators/version_validator.rb +29 -0
  121. data/lib/doto/version.rb +6 -0
  122. data/lib/doto/views/base_list_view.rb +41 -0
  123. data/lib/doto/views/color_theme/index.rb +62 -0
  124. data/lib/doto/views/color_theme/show.rb +107 -0
  125. data/lib/doto/views/configuration/show.rb +41 -0
  126. data/lib/doto/views/entry_group/edit.rb +121 -0
  127. data/lib/doto/views/entry_group/list.rb +23 -0
  128. data/lib/doto/views/entry_group/shared/no_entries_to_display.rb +53 -0
  129. data/lib/doto/views/entry_group/shared/no_entries_to_display_for_month_of.rb +32 -0
  130. data/lib/doto/views/entry_group/shared/no_entries_to_display_for_week_of.rb +33 -0
  131. data/lib/doto/views/entry_group/shared/no_entries_to_display_for_year_of.rb +33 -0
  132. data/lib/doto/views/entry_group/show.rb +63 -0
  133. data/lib/doto/views/export.rb +82 -0
  134. data/lib/doto/views/import.rb +105 -0
  135. data/lib/doto/views/import_dates.rb +17 -0
  136. data/lib/doto/views/project/create.rb +87 -0
  137. data/lib/doto/views/project/delete.rb +96 -0
  138. data/lib/doto/views/project/delete_by_number.rb +19 -0
  139. data/lib/doto/views/project/list.rb +115 -0
  140. data/lib/doto/views/project/rename.rb +98 -0
  141. data/lib/doto/views/project/rename_by_number.rb +21 -0
  142. data/lib/doto/views/project/use.rb +97 -0
  143. data/lib/doto/views/project/use_by_number.rb +19 -0
  144. data/lib/doto/views/shared/error.rb +17 -0
  145. data/lib/doto/views/shared/info.rb +17 -0
  146. data/lib/doto/views/shared/message.rb +85 -0
  147. data/lib/doto/views/shared/model_errors.rb +32 -0
  148. data/lib/doto/views/shared/success.rb +17 -0
  149. data/lib/doto/views/shared/warning.rb +17 -0
  150. data/lib/doto.rb +33 -0
  151. data/lib/locales/en/active_record.yml +17 -0
  152. data/lib/locales/en/commands.yml +165 -0
  153. data/lib/locales/en/miscellaneous.yml +29 -0
  154. data/lib/locales/en/presenters.yml +19 -0
  155. data/lib/locales/en/services.yml +14 -0
  156. data/lib/locales/en/subcommands.yml +786 -0
  157. data/lib/seed_data/0/.todo +5 -0
  158. data/lib/seed_data/20230613121411/.doto +8 -0
  159. data/lib/seed_data/20230613121411/doto/migration_version.json +3 -0
  160. data/lib/seed_data/20230613121411/doto/themes/cherry.json +79 -0
  161. data/lib/seed_data/20230613121411/doto/themes/christmas.json +79 -0
  162. data/lib/seed_data/20230613121411/doto/themes/default.json +79 -0
  163. data/lib/seed_data/20230613121411/doto/themes/lemon.json +79 -0
  164. data/lib/seed_data/20230613121411/doto/themes/light.json +79 -0
  165. data/lib/seed_data/20230613121411/doto/themes/matrix.json +79 -0
  166. data/lib/seed_data/20230613121411/doto/themes/whiteout.json +79 -0
  167. data/lib/seed_data/20240210161248/.doto +9 -0
  168. data/lib/seed_data/20240210161248/doto/current_project.json +4 -0
  169. data/lib/seed_data/20240210161248/doto/migration_version.json +3 -0
  170. data/lib/seed_data/20240210161248/doto/projects/default/project.json +5 -0
  171. data/lib/seed_data/20240210161248/doto/themes/cherry.json +79 -0
  172. data/lib/seed_data/20240210161248/doto/themes/christmas.json +79 -0
  173. data/lib/seed_data/20240210161248/doto/themes/default.json +79 -0
  174. data/lib/seed_data/20240210161248/doto/themes/lemon.json +79 -0
  175. data/lib/seed_data/20240210161248/doto/themes/light.json +79 -0
  176. data/lib/seed_data/20240210161248/doto/themes/matrix.json +79 -0
  177. data/lib/seed_data/20240210161248/doto/themes/whiteout.json +79 -0
  178. data/sig/dsu.rbs +4 -0
  179. metadata +406 -0
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+ require_relative '../crud/json_file'
6
+ require_relative '../migration/version'
7
+ require_relative '../models/configuration'
8
+ require_relative 'fileable'
9
+
10
+ module Doto
11
+ module Support
12
+ module ProjectFileSystem
13
+ class << self
14
+ def included(base)
15
+ base.extend(ClassMethods)
16
+ end
17
+ end
18
+
19
+ def exist?
20
+ self.class.project_file_exist?(project_name: project_name)
21
+ end
22
+ alias persisted? exist?
23
+
24
+ def project_initialized?
25
+ self.class.project_initialized?(project_name: project_name)
26
+ end
27
+
28
+ def project_number
29
+ self.class.project_number_for(project_name: project_name)
30
+ end
31
+
32
+ module ClassMethods
33
+ include Fileable
34
+
35
+ # Returns the currently selected (used) project name
36
+ # from doto/current_project.json
37
+ def current_project_name
38
+ Crud::JsonFile.read!(file_path: current_project_file).fetch(:project_name)
39
+ end
40
+
41
+ def default_project_name
42
+ return Models::Configuration::DEFAULT_CONFIGURATION[:default_project] unless Models::Configuration.exist?
43
+
44
+ Models::Configuration.new.default_project
45
+ end
46
+
47
+ def initialize_project(project_name:)
48
+ return if project_initialized?(project_name: project_name)
49
+
50
+ # TODO: Don't know if I like this here.
51
+ unless current_project_file_exist?
52
+ file_data = {
53
+ version: Doto::Migration::VERSION,
54
+ project_name: default_project_name
55
+ }
56
+ Crud::JsonFile.write!(file_data: file_data, file_path: current_project_file)
57
+ end
58
+
59
+ # Creates doto/projects/<project_name>
60
+ FileUtils.mkdir_p(project_folder_for(project_name: project_name))
61
+ end
62
+
63
+ def project_initialized?(project_name:)
64
+ # Checking these files, checks all the containing folders also
65
+ current_project_file_exist? &&
66
+ project_folder_exist?(project_name: project_name)
67
+ end
68
+
69
+ # Does doto/projects/<project_name>/project.json file exist?
70
+ def project_file_exist?(project_name:)
71
+ project_file_path = project_file_for(project_name: project_name)
72
+ File.exist?(project_file_path)
73
+ end
74
+ alias exist? project_file_exist?
75
+ alias persisted? project_file_exist?
76
+
77
+ # Does doto/current_project.json file exist?
78
+ def current_project_file_exist?
79
+ File.exist?(current_project_file)
80
+ end
81
+ alias current_project_file_persisted? current_project_file_exist?
82
+
83
+ # Does doto/projects/<project_name> folder exist?
84
+ def project_folder_exist?(project_name:)
85
+ Dir.exist?(project_folder_for(project_name: project_name))
86
+ end
87
+
88
+ def project_metadata
89
+ project_folder_names.each_with_index.with_object([]) do |(project_name, index), array|
90
+ array << {
91
+ project_number: index + 1,
92
+ project_name: project_name,
93
+ current_project: project_name == current_project_name,
94
+ default_projet: project_name == default_project_name
95
+ }
96
+ end
97
+ end
98
+
99
+ def project_number_for(project_name:)
100
+ project_metadata.find do |metadata|
101
+ metadata[:project_name] == project_name
102
+ end&.[](:project_number) || -1
103
+ end
104
+
105
+ private
106
+
107
+ def project_folder_names
108
+ Pathname.new(projects_folder)
109
+ .children
110
+ .select(&:directory?)
111
+ .map(&:basename)
112
+ .map(&:to_s)
113
+ .sort { |a, b| a.casecmp(b) }
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Support
5
+ module ShortString
6
+ SHORT_STRING_MAX_COUNT = 25
7
+
8
+ module_function
9
+
10
+ def short_string(string:, count: SHORT_STRING_MAX_COUNT, elipsis: '...')
11
+ return '' if string.blank?
12
+ return string if string.length <= count
13
+
14
+ # Trim to max count and cut at the last space within the limit
15
+ trimmed_string = string[0...count].rpartition(' ')[0]
16
+
17
+ # If no space found, trim by characters
18
+ trimmed_string = string[0...(count - elipsis.length)] if trimmed_string.empty? && !string.empty?
19
+
20
+ "#{trimmed_string}#{elipsis}"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Support
5
+ module TimeComparable
6
+ TIME_COMPARABLE_FORMAT_SPECIFIER = '%Y%m%d'
7
+
8
+ module_function
9
+
10
+ def time_equal?(other_time:)
11
+ time_equal_compare_string_for(time: time) == time_equal_compare_string_for(time: other_time)
12
+ end
13
+
14
+ def time_equal_compare_string_for(time:)
15
+ time = time.in_time_zone
16
+
17
+ time.strftime(TIME_COMPARABLE_FORMAT_SPECIFIER)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'active_support/core_ext/numeric/time'
5
+
6
+ module Doto
7
+ module Support
8
+ # This module provides functions for formatting Time objects
9
+ # to display in the console.
10
+ module TimeFormatable
11
+ module_function
12
+
13
+ # TODO: I18n.
14
+ def formatted_time(time:)
15
+ time = time.in_time_zone
16
+
17
+ today_yesterday_or_tomorrow = if time.today?
18
+ 'Today'
19
+ elsif time.yesterday?
20
+ 'Yesterday'
21
+ elsif time.tomorrow?
22
+ 'Tomorrow'
23
+ end
24
+
25
+ time_zone = timezone_for(time: time)
26
+
27
+ return time.strftime("%A, %Y-%m-%d #{time_zone}") unless today_yesterday_or_tomorrow
28
+
29
+ time.strftime("%A, (#{today_yesterday_or_tomorrow}) %Y-%m-%d #{time_zone}")
30
+ end
31
+
32
+ # TODO: I18n.
33
+ def mm_dd(time:, separator: '/')
34
+ time.strftime("%m#{separator}%d")
35
+ end
36
+
37
+ # TODO: I18n.
38
+ def mm_dd_yyyy(time:, separator: '/')
39
+ time.strftime("%m#{separator}%d#{separator}%Y")
40
+ end
41
+
42
+ def dd_mm_yyyy(time:, separator: '/')
43
+ time.strftime("%d#{separator}%m#{separator}%Y")
44
+ end
45
+
46
+ def timezone_for(time:)
47
+ time.zone
48
+ end
49
+
50
+ # TODO: I18n.
51
+ def yyyy_mm_dd_or_through_for(times:)
52
+ return yyyy_mm_dd(time: times[0]) if times.one?
53
+
54
+ times = [yyyy_mm_dd(time: times.min), yyyy_mm_dd(time: times.max)]
55
+
56
+ I18n.t('information.dates.through', from: times[0], to: times[1])
57
+ end
58
+
59
+ # TODO: I18n.
60
+ def yyyy_mm_dd(time:, separator: '-')
61
+ time.strftime("%Y#{separator}%m#{separator}%d")
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Support
5
+ module TimesSortable
6
+ def sorted_doto_times_for(times:)
7
+ configuration = Models::Configuration.new unless defined?(configuration) && configuration
8
+ entries_display_order = configuration.entries_display_order
9
+ times_sort(times: times_for(times: times), entries_display_order: entries_display_order)
10
+ end
11
+
12
+ def times_sort(times:, entries_display_order: nil)
13
+ times = times.dup
14
+ entries_display_order ||= :asc
15
+
16
+ validate_times_argument!(times: times)
17
+ validate_entries_display_order_argument!(entries_display_order: entries_display_order)
18
+
19
+ return times if times.one?
20
+
21
+ # NOTE: The times array needs to be sorted unconditionally because if
22
+ # the sort is ascending, then the times array needs to be returned
23
+ # in ascending order. If the sort is descending, then in order to
24
+ # properly reverse the times array, it needs to first be sorted in
25
+ # ascending order before being reversed.
26
+ return times.sort if entries_display_order == :asc
27
+
28
+ times.sort_by { |time| -time.to_i }
29
+ end
30
+
31
+ def times_for(times:)
32
+ times = times.dup
33
+ validate_times_argument!(times: times)
34
+
35
+ start_date = times.max
36
+ return times unless start_date.monday? || start_date.on_weekend?
37
+
38
+ # If the start date is a weekend or a Monday then we need to look back
39
+ # to include the preceeding Friday upto and including the start date.
40
+ (0..3).filter_map do |num|
41
+ time = start_date - num.days
42
+ next unless time == start_date || time.on_weekend? || time.friday?
43
+
44
+ time
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def validate_times_argument!(times:)
51
+ raise ArgumentError, "times is the wrong object type: \"#{times.class}\"" unless times.is_a?(Array)
52
+ raise ArgumentError, 'times is empty' if times.empty?
53
+ end
54
+
55
+ def validate_entries_display_order_argument!(entries_display_order:)
56
+ unless entries_display_order.nil? || entries_display_order.is_a?(Symbol)
57
+ raise ArgumentError, "entries_display_order is the wrong object type: \"#{entries_display_order.class}\""
58
+ end
59
+
60
+ unless %i[asc desc].include?(entries_display_order)
61
+ raise ArgumentError, "entries_display_order is invalid: \":#{entries_display_order}\""
62
+ end
63
+ end
64
+
65
+ # NOTE: This, as opposed to using module_function, so that we can
66
+ # invoke .validate_times_sort_arguments! from the .times_sort
67
+ # method with module as the receiver AND when included as a mixin.
68
+ extend self
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Support
5
+ module TransformProjectName
6
+ TRANSFORM_PROJECT_NAME_REGEX = %r{[^/\w\s]|_}
7
+ TRANSFORM_PROJECT_NAME_SEPARATOR = '-'
8
+
9
+ module_function
10
+
11
+ def transform_project_name(project_name, options: {})
12
+ normalized_name = project_name
13
+ .gsub(TRANSFORM_PROJECT_NAME_REGEX, ' ') # Replace non-word characters and underscores with space
14
+ .strip # Remove leading and trailing spaces
15
+ .squeeze(' ') # Convert consecutive spaces to a single space
16
+ .tr(' ', TRANSFORM_PROJECT_NAME_SEPARATOR) # Replace spaces with hyphens
17
+ .squeeze(TRANSFORM_PROJECT_NAME_SEPARATOR) # Ensure no consecutive hyphens
18
+
19
+ normalized_name.downcase! if options[:downcase]
20
+ normalized_name
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Utils
5
+ class << self
6
+ def strip_escapes(escaped_string)
7
+ escaped_string.gsub(/\e\[[0-9;]*[a-zA-Z]/, '')
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+
5
+ # rubocop:disable Layout/LineLength
6
+ module Doto
7
+ module Validators
8
+ class ColorThemeValidator < ActiveModel::Validator
9
+ def validate(record)
10
+ default_theme_colors = record.class::DEFAULT_THEME_COLORS
11
+
12
+ # return unless validate_color_theme_keys!(record, default_theme_colors)
13
+
14
+ default_theme_colors.each_key do |theme_color_key|
15
+ theme_colors_hash = record.public_send(theme_color_key)
16
+
17
+ next unless validate_theme_color_type!(record, theme_color_key, theme_colors_hash)
18
+
19
+ if theme_colors_hash.empty?
20
+ record.errors.add(:base, ":#{theme_color_key} colors Hash is empty")
21
+ next
22
+ end
23
+
24
+ validate_theme_colors!(record, theme_colors_hash)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def colors
31
+ @colors ||= String.colors
32
+ end
33
+
34
+ def modes
35
+ @modes ||= String.modes
36
+ end
37
+
38
+ def validate_theme_color_type!(record, theme_color_key, theme_colors_hash)
39
+ return true if theme_colors_hash.is_a?(Hash)
40
+
41
+ record.errors.add(:base, ":#{theme_color_key} value is the wrong object type. " \
42
+ "\"Hash\" was expected, but \"#{theme_colors_hash.class}\" was received.")
43
+ false
44
+ end
45
+
46
+ def validate_theme_colors!(record, theme_colors_hash)
47
+ unless colors.include?(theme_colors_hash[:color])
48
+ value = theme_color_value_to_s(theme_colors_hash[:color])
49
+ record.errors.add(:base, ":color key value #{value} in theme color Hash #{theme_colors_hash} is not a valid color. " \
50
+ "One of #{colors.wrap_and_join(wrapper: [':', ''])} was expected, but #{value} was received.")
51
+ end
52
+
53
+ unless theme_colors_hash[:mode].nil? || modes.include?(theme_colors_hash[:mode])
54
+ value = theme_color_value_to_s(theme_colors_hash[:mode])
55
+ record.errors.add(:base, ":mode key value #{value} in theme color Hash #{theme_colors_hash} is not a valid mode value. " \
56
+ "One of #{modes.wrap_and_join(wrapper: [':', ''])} was expected, but #{value} was received.")
57
+ end
58
+
59
+ unless theme_colors_hash[:background].nil? || colors.include?(theme_colors_hash[:background])
60
+ value = theme_color_value_to_s(theme_colors_hash[:background])
61
+ record.errors.add(:base, ":background key value #{value} in theme color Hash #{theme_colors_hash} is not a valid color. " \
62
+ "One of #{colors.wrap_and_join(wrapper: [':', ''])} was expected, but #{value} was received.")
63
+ end
64
+ end
65
+
66
+ def theme_color_value_to_s(theme_color_value)
67
+ return ":#{theme_color_value}" if theme_color_value.is_a?(Symbol)
68
+
69
+ "'#{theme_color_value}'"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ # rubocop:enable Layout/LineLength
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
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
+ # TODO: I18n.
16
+ record.errors.add(:description, 'is the wrong object type. ' \
17
+ "\"String\" was expected, but \"#{description.class}\" was received.")
18
+ return
19
+ end
20
+
21
+ validate_description record
22
+ end
23
+
24
+ private
25
+
26
+ def validate_description(record)
27
+ description = record.description
28
+
29
+ return if description.length.between?(min_description_length(record), max_description_length(record))
30
+
31
+ if description.length < min_description_length(record)
32
+ # TODO: I18n.
33
+ record.errors.add(:description, "is too short: \"#{record.short_description}\" " \
34
+ "(minimum is #{min_description_length(record)} characters).")
35
+ elsif description.length > max_description_length(record)
36
+ # TODO: I18n.
37
+ record.errors.add(:description, "is too long: \"#{record.short_description}\" " \
38
+ "(maximum is #{max_description_length(record)} characters).")
39
+ end
40
+ end
41
+
42
+ def min_description_length(record)
43
+ record.class::MIN_DESCRIPTION_LENGTH
44
+ end
45
+
46
+ def max_description_length(record)
47
+ record.class::MAX_DESCRIPTION_LENGTH
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/entry'
4
+ require_relative '../support/field_errors'
5
+
6
+ # https://guides.rubyonrails.org/active_record_validations.html#validates-with
7
+ module Doto
8
+ module Validators
9
+ # TODO: I18n.
10
+ class EntriesValidator < ActiveModel::Validator
11
+ include Support::FieldErrors
12
+
13
+ def validate(record)
14
+ unless record.entries.is_a?(Array)
15
+ record.errors.add(:entries, 'is the wrong object type. ' \
16
+ "\"Array\" was expected, but \"#{record.entries.class}\" was received.")
17
+ return
18
+ end
19
+
20
+ return if validate_entry_types record
21
+
22
+ validate_unique_entry record
23
+ validate_entries record
24
+ end
25
+
26
+ private
27
+
28
+ def validate_entry_types(record)
29
+ record.entries.each do |entry|
30
+ next if entry.is_a? Doto::Models::Entry
31
+
32
+ record.errors.add(:entries, 'Array element is the wrong object type. ' \
33
+ "\"Entry\" was expected, but \"#{entry.class}\" was received.",
34
+ type: Support::FieldErrors::FIELD_TYPE_ERROR)
35
+ end
36
+
37
+ record.errors.any?
38
+ end
39
+
40
+ def validate_unique_entry(record)
41
+ return unless record.entries.is_a? Array
42
+
43
+ entry_objects = record.entries.select { |entry| entry.is_a?(Doto::Models::Entry) }
44
+
45
+ descriptions = entry_objects.map(&:description)
46
+ return if descriptions.uniq.length == descriptions.length
47
+
48
+ non_unique_descriptions = descriptions.select { |description| descriptions.count(description) > 1 }.uniq
49
+ non_unique_descriptions.each do |non_unique_description|
50
+ # TODO: I18n.
51
+ record.errors.add(:entries,
52
+ "array contains duplicate entry: \"#{short_description(non_unique_description)}\".",
53
+ type: Support::FieldErrors::FIELD_DUPLICATE_ERROR)
54
+ end
55
+
56
+ record.errors.any?
57
+ end
58
+
59
+ def validate_entries(record)
60
+ entries = record.entries
61
+ return if entries.none?
62
+
63
+ entries.each do |entry|
64
+ next if entry.valid?
65
+
66
+ entry.errors.each do |error|
67
+ record.errors.add(:entries_entry, error.full_message)
68
+ end
69
+ end
70
+ end
71
+
72
+ def short_description(description)
73
+ Models::Entry.short_description(string: description)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../support/field_errors'
4
+ require_relative '../support/short_string'
5
+
6
+ # https://guides.rubyonrails.org/active_record_validations.html#validates-with
7
+ module Doto
8
+ module Validators
9
+ # TODO: I18n.
10
+ class ProjectNameValidator < ActiveModel::Validator
11
+ include Support::FieldErrors
12
+ include Support::ShortString
13
+
14
+ def validate(record)
15
+ unless record.project_name.is_a?(String)
16
+ record.errors.add(:project_name, 'is the wrong object type. ' \
17
+ "\"String\" was expected, but \"#{record.project.class}\" was received.")
18
+ return
19
+ end
20
+
21
+ unless record.project_name.present?
22
+ record.errors.add(:project_name, :blank, '')
23
+
24
+ return
25
+ end
26
+
27
+ validate_project_name record
28
+ end
29
+
30
+ private
31
+
32
+ def validate_project_name(record)
33
+ project_name = record.project_name
34
+
35
+ return if project_name.length.between?(min_project_name_length(record), max_project_name_length(record))
36
+
37
+ if project_name.length < min_project_name_length(record)
38
+ # TODO: I18n.
39
+ record.errors.add(:project_name, "is too short: \"#{record.project_name}\" " \
40
+ "(minimum is #{min_project_name_length(record)} characters).")
41
+ elsif project_name.length > max_project_name_length(record)
42
+ # TODO: I18n.
43
+ short_project_name = short_string(string: project_name, count: max_project_name_length(record))
44
+ record.errors.add(:project_name, "is too long: \"#{short_project_name}\" " \
45
+ "(maximum is #{max_project_name_length(record)} characters).")
46
+ end
47
+ end
48
+
49
+ def min_project_name_length(record)
50
+ record.class::MIN_PROJECT_NAME_LENGTH
51
+ end
52
+
53
+ def max_project_name_length(record)
54
+ record.class::MAX_PROJECT_NAME_LENGTH
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # https://guides.rubyonrails.org/active_record_validations.html#validates-with
4
+ module Doto
5
+ module Validators
6
+ class TimeValidator < ActiveModel::Validator
7
+ def validate(record)
8
+ time = record.time
9
+
10
+ if time.nil?
11
+ record.errors.add(:time, :blank)
12
+ return
13
+ end
14
+
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
19
+ end
20
+
21
+ record.errors.add(:time, 'is not in localtime format.') unless time == time.in_time_zone
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # https://guides.rubyonrails.org/active_record_validations.html#validates-with
4
+ module Doto
5
+ module Validators
6
+ class VersionValidator < ActiveModel::Validator
7
+ def validate(record)
8
+ version = record.version
9
+
10
+ if version.nil?
11
+ record.errors.add(:version, 'is nil')
12
+ return
13
+ end
14
+
15
+ unless version.is_a?(Integer)
16
+ record.errors.add(:version, 'is the wrong object type. ' \
17
+ "\"Integer\" was expected, but \"#{version.class}\" was received.")
18
+ nil
19
+ end
20
+
21
+ # TODO: This validation should check the configuration version
22
+ # against the current migration version and they should match.
23
+ # unless version == record.class::VERSION
24
+ # record.errors.add(:version, "\"#{version}\" is not the correct version: \"#{record.class::VERSION}\"")
25
+ # end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ VERSION_REGEX = /\A\d+\.\d+\.\d+(\.(alpha|beta|rc)\.\d+)?\z/
5
+ VERSION = '0.0.1-alpha.1'
6
+ end