annotaterb 4.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +0 -0
  3. data/LICENSE.txt +55 -0
  4. data/README.md +91 -0
  5. data/VERSION +1 -0
  6. data/exe/annotaterb +21 -0
  7. data/lib/annotate_rb/active_record_patch.rb +9 -0
  8. data/lib/annotate_rb/commands/annotate_models.rb +22 -0
  9. data/lib/annotate_rb/commands/annotate_routes.rb +19 -0
  10. data/lib/annotate_rb/commands/print_help.rb +16 -0
  11. data/lib/annotate_rb/commands/print_version.rb +12 -0
  12. data/lib/annotate_rb/commands.rb +10 -0
  13. data/lib/annotate_rb/config_finder.rb +21 -0
  14. data/lib/annotate_rb/config_loader.rb +63 -0
  15. data/lib/annotate_rb/core.rb +23 -0
  16. data/lib/annotate_rb/eager_loader.rb +23 -0
  17. data/lib/annotate_rb/env.rb +30 -0
  18. data/lib/annotate_rb/model_annotator/annotation_pattern_generator.rb +19 -0
  19. data/lib/annotate_rb/model_annotator/annotator.rb +74 -0
  20. data/lib/annotate_rb/model_annotator/bad_model_file_error.rb +11 -0
  21. data/lib/annotate_rb/model_annotator/constants.rb +22 -0
  22. data/lib/annotate_rb/model_annotator/file_annotation_remover.rb +25 -0
  23. data/lib/annotate_rb/model_annotator/file_annotator.rb +79 -0
  24. data/lib/annotate_rb/model_annotator/file_name_resolver.rb +16 -0
  25. data/lib/annotate_rb/model_annotator/file_patterns.rb +129 -0
  26. data/lib/annotate_rb/model_annotator/helper.rb +54 -0
  27. data/lib/annotate_rb/model_annotator/model_class_getter.rb +63 -0
  28. data/lib/annotate_rb/model_annotator/model_file_annotator.rb +118 -0
  29. data/lib/annotate_rb/model_annotator/model_files_getter.rb +62 -0
  30. data/lib/annotate_rb/model_annotator/pattern_getter.rb +27 -0
  31. data/lib/annotate_rb/model_annotator/schema_info.rb +480 -0
  32. data/lib/annotate_rb/model_annotator.rb +20 -0
  33. data/lib/annotate_rb/options.rb +204 -0
  34. data/lib/annotate_rb/parser.rb +385 -0
  35. data/lib/annotate_rb/rake_bootstrapper.rb +34 -0
  36. data/lib/annotate_rb/route_annotator/annotation_processor.rb +56 -0
  37. data/lib/annotate_rb/route_annotator/annotator.rb +40 -0
  38. data/lib/annotate_rb/route_annotator/base_processor.rb +104 -0
  39. data/lib/annotate_rb/route_annotator/header_generator.rb +113 -0
  40. data/lib/annotate_rb/route_annotator/helper.rb +104 -0
  41. data/lib/annotate_rb/route_annotator/removal_processor.rb +40 -0
  42. data/lib/annotate_rb/route_annotator.rb +12 -0
  43. data/lib/annotate_rb/runner.rb +34 -0
  44. data/lib/annotate_rb/tasks/annotate_models_migrate.rake +30 -0
  45. data/lib/annotate_rb.rb +30 -0
  46. data/lib/generators/annotate_rb/USAGE +4 -0
  47. data/lib/generators/annotate_rb/install_generator.rb +15 -0
  48. data/lib/generators/annotate_rb/templates/auto_annotate_models.rake +7 -0
  49. metadata +96 -0
@@ -0,0 +1,480 @@
1
+ module AnnotateRb
2
+ module ModelAnnotator
3
+ module SchemaInfo # rubocop:disable Metrics/ModuleLength
4
+ # Don't show default value for these column types
5
+ NO_DEFAULT_COL_TYPES = %w[json jsonb hstore].freeze
6
+
7
+ # Don't show limit (#) on these column types
8
+ # Example: show "integer" instead of "integer(4)"
9
+ NO_LIMIT_COL_TYPES = %w[integer bigint boolean].freeze
10
+
11
+ INDEX_CLAUSES = {
12
+ unique: {
13
+ default: 'UNIQUE',
14
+ markdown: '_unique_'
15
+ },
16
+ where: {
17
+ default: 'WHERE',
18
+ markdown: '_where_'
19
+ },
20
+ using: {
21
+ default: 'USING',
22
+ markdown: '_using_'
23
+ }
24
+ }.freeze
25
+
26
+ END_MARK = '== Schema Information End'.freeze
27
+
28
+ class << self
29
+ # Use the column information in an ActiveRecord class
30
+ # to create a comment block containing a line for
31
+ # each column. The line contains the column name,
32
+ # the type (and length), and any optional attributes
33
+ def generate(klass, header, options = {}) # rubocop:disable Metrics/MethodLength
34
+ info = "# #{header}\n"
35
+ info << get_schema_header_text(klass, options)
36
+
37
+ max_size = max_schema_info_width(klass, options)
38
+ md_names_overhead = 6
39
+ md_type_allowance = 18
40
+ bare_type_allowance = 16
41
+
42
+ if options[:format_markdown]
43
+ info << format("# %-#{max_size + md_names_overhead}.#{max_size + md_names_overhead}s | %-#{md_type_allowance}.#{md_type_allowance}s | %s\n",
44
+ 'Name',
45
+ 'Type',
46
+ 'Attributes')
47
+ info << "# #{'-' * (max_size + md_names_overhead)} | #{'-' * md_type_allowance} | #{'-' * 27}\n"
48
+ end
49
+
50
+ cols = columns(klass, options)
51
+ cols.each do |col|
52
+ col_type = get_col_type(col)
53
+ attrs = get_attributes(col, col_type, klass, options)
54
+ col_name = if with_comments?(klass, options) && col.comment
55
+ "#{col.name}(#{col.comment.gsub(/\n/, '\\n')})"
56
+ else
57
+ col.name
58
+ end
59
+
60
+ if options[:format_rdoc]
61
+ info << format("# %-#{max_size}.#{max_size}s<tt>%s</tt>",
62
+ "*#{col_name}*::",
63
+ attrs.unshift(col_type).join(', ')).rstrip + "\n"
64
+ elsif options[:format_yard]
65
+ info << sprintf("# @!attribute #{col_name}") + "\n"
66
+ ruby_class = col.respond_to?(:array) && col.array ? "Array<#{map_col_type_to_ruby_classes(col_type)}>" : map_col_type_to_ruby_classes(col_type)
67
+ info << sprintf("# @return [#{ruby_class}]") + "\n"
68
+ elsif options[:format_markdown]
69
+ name_remainder = max_size - col_name.length - non_ascii_length(col_name)
70
+ type_remainder = (md_type_allowance - 2) - col_type.length
71
+ info << format("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`",
72
+ col_name,
73
+ ' ',
74
+ col_type,
75
+ ' ',
76
+ attrs.join(', ').rstrip).gsub('``', ' ').rstrip + "\n"
77
+ else
78
+ info << format_default(col_name, max_size, col_type, bare_type_allowance, attrs)
79
+ end
80
+ end
81
+
82
+ info << get_index_info(klass, options) if options[:show_indexes] && klass.table_exists?
83
+
84
+ info << get_foreign_key_info(klass, options) if options[:show_foreign_keys] && klass.table_exists?
85
+
86
+ info << get_schema_footer_text(klass, options)
87
+ end
88
+
89
+ private
90
+
91
+ def get_schema_header_text(klass, options = {})
92
+ info = "#\n"
93
+ if options[:format_markdown]
94
+ info << "# Table name: `#{klass.table_name}`\n"
95
+ info << "#\n"
96
+ info << "# ### Columns\n"
97
+ else
98
+ info << "# Table name: #{klass.table_name}\n"
99
+ end
100
+ info << "#\n"
101
+ end
102
+
103
+ def max_schema_info_width(klass, options)
104
+ cols = columns(klass, options)
105
+
106
+ if with_comments?(klass, options)
107
+ max_size = cols.map do |column|
108
+ column.name.size + (column.comment ? width(column.comment) : 0)
109
+ end.max || 0
110
+ max_size += 2
111
+ else
112
+ max_size = cols.map(&:name).map(&:size).max
113
+ end
114
+ max_size += options[:format_rdoc] ? 5 : 1
115
+
116
+ max_size
117
+ end
118
+
119
+ def with_comments?(klass, options)
120
+ options[:with_comment] &&
121
+ klass.columns.first.respond_to?(:comment) &&
122
+ klass.columns.map(&:comment).any? { |comment| !comment.nil? }
123
+ end
124
+
125
+ def width(string)
126
+ string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
127
+ end
128
+
129
+ # TODO: Memoize this since it's called multiple times with the same args
130
+ def columns(klass, options)
131
+ cols = klass.columns
132
+ cols += translated_columns(klass)
133
+
134
+ ignore_columns = options[:ignore_columns]
135
+ if ignore_columns
136
+ cols = cols.reject do |col|
137
+ col.name.match(/#{ignore_columns}/)
138
+ end
139
+ end
140
+
141
+ cols = cols.sort_by(&:name) if options[:sort]
142
+ cols = classified_sort(cols) if options[:classified_sort]
143
+
144
+ cols
145
+ end
146
+
147
+ # Add columns managed by the globalize gem if this gem is being used.
148
+ def translated_columns(klass)
149
+ return [] unless klass.respond_to? :translation_class
150
+
151
+ ignored_cols = ignored_translation_table_colums(klass)
152
+ klass.translation_class.columns.reject do |col|
153
+ ignored_cols.include? col.name.to_sym
154
+ end
155
+ end
156
+
157
+ # These are the columns that the globalize gem needs to work but
158
+ # are not necessary for the models to be displayed as annotations.
159
+ def ignored_translation_table_colums(klass)
160
+ # Construct the foreign column name in the translations table
161
+ # eg. Model: Car, foreign column name: car_id
162
+ foreign_column_name = [
163
+ klass.translation_class.to_s
164
+ .gsub('::Translation', '').gsub('::', '_')
165
+ .downcase,
166
+ '_id'
167
+ ].join.to_sym
168
+
169
+ [
170
+ :id,
171
+ :created_at,
172
+ :updated_at,
173
+ :locale,
174
+ foreign_column_name
175
+ ]
176
+ end
177
+
178
+ def classified_sort(cols)
179
+ rest_cols = []
180
+ timestamps = []
181
+ associations = []
182
+ id = nil
183
+
184
+ cols.each do |c|
185
+ if c.name.eql?('id')
186
+ id = c
187
+ elsif c.name.eql?('created_at') || c.name.eql?('updated_at')
188
+ timestamps << c
189
+ elsif c.name[-3, 3].eql?('_id')
190
+ associations << c
191
+ else
192
+ rest_cols << c
193
+ end
194
+ end
195
+ [rest_cols, timestamps, associations].each { |a| a.sort_by!(&:name) }
196
+
197
+ ([id] << rest_cols << timestamps << associations).flatten.compact
198
+ end
199
+
200
+ def get_col_type(col)
201
+ if (col.respond_to?(:bigint?) && col.bigint?) || /\Abigint\b/ =~ col.sql_type
202
+ 'bigint'
203
+ else
204
+ (col.type || col.sql_type).to_s
205
+ end
206
+ end
207
+
208
+ # Get the list of attributes that should be included in the annotation for
209
+ # a given column.
210
+ def get_attributes(column, column_type, klass, options)
211
+ attrs = []
212
+ attrs << "default(#{schema_default(klass, column)})" unless column.default.nil? || hide_default?(column_type, options)
213
+ attrs << 'unsigned' if column.respond_to?(:unsigned?) && column.unsigned?
214
+ attrs << 'not null' unless column.null
215
+ attrs << 'primary key' if klass.primary_key && (klass.primary_key.is_a?(Array) ? klass.primary_key.collect(&:to_sym).include?(column.name.to_sym) : column.name.to_sym == klass.primary_key.to_sym)
216
+
217
+ if column_type == 'decimal'
218
+ column_type << "(#{column.precision}, #{column.scale})"
219
+ elsif !%w[spatial geometry geography].include?(column_type)
220
+ if column.limit && !options[:format_yard]
221
+ if column.limit.is_a? Array
222
+ attrs << "(#{column.limit.join(', ')})"
223
+ else
224
+ column_type << "(#{column.limit})" unless hide_limit?(column_type, options)
225
+ end
226
+ end
227
+ end
228
+
229
+ # Check out if we got an array column
230
+ attrs << 'is an Array' if column.respond_to?(:array) && column.array
231
+
232
+ # Check out if we got a geometric column
233
+ # and print the type and SRID
234
+ if column.respond_to?(:geometry_type)
235
+ attrs << "#{column.geometry_type}, #{column.srid}"
236
+ elsif column.respond_to?(:geometric_type) && column.geometric_type.present?
237
+ attrs << "#{column.geometric_type.to_s.downcase}, #{column.srid}"
238
+ end
239
+
240
+ # Check if the column has indices and print "indexed" if true
241
+ # If the index includes another column, print it too.
242
+ if options[:simple_indexes] && klass.table_exists? # Check out if this column is indexed
243
+ indices = retrieve_indexes_from_table(klass).select { |ind| ind.columns.include? column.name }
244
+ indices&.sort_by(&:name)&.each do |ind|
245
+ next if ind.columns.is_a?(String)
246
+
247
+ ind = ind.columns.reject! { |i| i == column.name }
248
+ attrs << (ind.empty? ? 'indexed' : "indexed => [#{ind.join(', ')}]")
249
+ end
250
+ end
251
+
252
+ attrs
253
+ end
254
+
255
+ def schema_default(klass, column)
256
+ quote(klass.column_defaults[column.name])
257
+ end
258
+
259
+ # Simple quoting for the default column value
260
+ def quote(value)
261
+ case value
262
+ when NilClass then 'NULL'
263
+ when TrueClass then 'TRUE'
264
+ when FalseClass then 'FALSE'
265
+ when Float, Integer then value.to_s
266
+ # BigDecimals need to be output in a non-normalized form and quoted.
267
+ when BigDecimal then value.to_s('F')
268
+ when Array then value.map { |v| quote(v) }
269
+ else
270
+ value.inspect
271
+ end
272
+ end
273
+
274
+ def hide_default?(col_type, options)
275
+ excludes =
276
+ if options[:hide_default_column_types].blank?
277
+ NO_DEFAULT_COL_TYPES
278
+ else
279
+ options[:hide_default_column_types].split(',')
280
+ end
281
+
282
+ excludes.include?(col_type)
283
+ end
284
+
285
+ def hide_limit?(col_type, options)
286
+ excludes =
287
+ if options[:hide_limit_column_types].blank?
288
+ NO_LIMIT_COL_TYPES
289
+ else
290
+ options[:hide_limit_column_types].split(',')
291
+ end
292
+
293
+ excludes.include?(col_type)
294
+ end
295
+
296
+ def retrieve_indexes_from_table(klass)
297
+ table_name = klass.table_name
298
+ return [] unless table_name
299
+
300
+ indexes = klass.connection.indexes(table_name)
301
+ return indexes if indexes.any? || !klass.table_name_prefix
302
+
303
+ # Try to search the table without prefix
304
+ table_name_without_prefix = table_name.to_s.sub(klass.table_name_prefix, '')
305
+ klass.connection.indexes(table_name_without_prefix)
306
+ end
307
+
308
+ def map_col_type_to_ruby_classes(col_type)
309
+ case col_type
310
+ when 'integer' then Integer.to_s
311
+ when 'float' then Float.to_s
312
+ when 'decimal' then BigDecimal.to_s
313
+ when 'datetime', 'timestamp', 'time' then Time.to_s
314
+ when 'date' then Date.to_s
315
+ when 'text', 'string', 'binary', 'inet', 'uuid' then String.to_s
316
+ when 'json', 'jsonb' then Hash.to_s
317
+ when 'boolean' then 'Boolean'
318
+ end
319
+ end
320
+
321
+ def non_ascii_length(string)
322
+ string.to_s.chars.reject(&:ascii_only?).length
323
+ end
324
+
325
+ def format_default(col_name, max_size, col_type, bare_type_allowance, attrs)
326
+ format('# %s:%s %s',
327
+ mb_chars_ljust(col_name, max_size),
328
+ mb_chars_ljust(col_type, bare_type_allowance),
329
+ attrs.join(', ')).rstrip + "\n"
330
+ end
331
+
332
+ def mb_chars_ljust(string, length)
333
+ string = string.to_s
334
+ padding = length - width(string)
335
+ if padding.positive?
336
+ string + (' ' * padding)
337
+ else
338
+ string[0..(length - 1)]
339
+ end
340
+ end
341
+
342
+ def get_index_info(klass, options = {})
343
+ index_info = if options[:format_markdown]
344
+ "#\n# ### Indexes\n#\n"
345
+ else
346
+ "#\n# Indexes\n#\n"
347
+ end
348
+
349
+ indexes = retrieve_indexes_from_table(klass)
350
+ return '' if indexes.empty?
351
+
352
+ max_size = indexes.collect { |index| index.name.size }.max + 1
353
+ indexes.sort_by(&:name).each do |index|
354
+ index_info << if options[:format_markdown]
355
+ final_index_string_in_markdown(index)
356
+ else
357
+ final_index_string(index, max_size)
358
+ end
359
+ end
360
+
361
+ index_info
362
+ end
363
+
364
+ def final_index_string_in_markdown(index)
365
+ details = format(
366
+ '%s%s%s',
367
+ index_unique_info(index, :markdown),
368
+ index_where_info(index, :markdown),
369
+ index_using_info(index, :markdown)
370
+ ).strip
371
+ details = " (#{details})" unless details.blank?
372
+
373
+ format(
374
+ "# * `%s`%s:\n# * **`%s`**\n",
375
+ index.name,
376
+ details,
377
+ index_columns_info(index).join("`**\n# * **`")
378
+ )
379
+ end
380
+
381
+ def final_index_string(index, max_size)
382
+ format(
383
+ "# %-#{max_size}.#{max_size}s %s%s%s%s",
384
+ index.name,
385
+ "(#{index_columns_info(index).join(',')})",
386
+ index_unique_info(index),
387
+ index_where_info(index),
388
+ index_using_info(index)
389
+ ).rstrip + "\n"
390
+ end
391
+
392
+ def index_columns_info(index)
393
+ Array(index.columns).map do |col|
394
+ if index.try(:orders) && index.orders[col.to_s]
395
+ "#{col} #{index.orders[col.to_s].upcase}"
396
+ else
397
+ col.to_s.gsub("\r", '\r').gsub("\n", '\n')
398
+ end
399
+ end
400
+ end
401
+
402
+ def index_where_info(index, format = :default)
403
+ value = index.try(:where).try(:to_s)
404
+ if value.blank?
405
+ ''
406
+ else
407
+ " #{INDEX_CLAUSES[:where][format]} #{value}"
408
+ end
409
+ end
410
+
411
+ def index_unique_info(index, format = :default)
412
+ index.unique ? " #{INDEX_CLAUSES[:unique][format]}" : ''
413
+ end
414
+
415
+ def index_using_info(index, format = :default)
416
+ value = index.try(:using) && index.using.try(:to_sym)
417
+ if !value.blank? && value != :btree
418
+ " #{INDEX_CLAUSES[:using][format]} #{value}"
419
+ else
420
+ ''
421
+ end
422
+ end
423
+
424
+ def get_foreign_key_info(klass, options = {})
425
+ fk_info = if options[:format_markdown]
426
+ "#\n# ### Foreign Keys\n#\n"
427
+ else
428
+ "#\n# Foreign Keys\n#\n"
429
+ end
430
+
431
+ return '' unless klass.connection.respond_to?(:supports_foreign_keys?) &&
432
+ klass.connection.supports_foreign_keys? && klass.connection.respond_to?(:foreign_keys)
433
+
434
+ foreign_keys = klass.connection.foreign_keys(klass.table_name)
435
+ return '' if foreign_keys.empty?
436
+
437
+ format_name = lambda do |fk|
438
+ return fk.options[:column] if fk.name.blank?
439
+
440
+ options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, '...')
441
+ end
442
+
443
+ max_size = foreign_keys.map(&format_name).map(&:size).max + 1
444
+ foreign_keys.sort_by { |fk| [format_name.call(fk), fk.column] }.each do |fk|
445
+ ref_info = "#{fk.column} => #{fk.to_table}.#{fk.primary_key}"
446
+ constraints_info = ''
447
+ constraints_info += "ON DELETE => #{fk.on_delete} " if fk.on_delete
448
+ constraints_info += "ON UPDATE => #{fk.on_update} " if fk.on_update
449
+ constraints_info.strip!
450
+
451
+ fk_info << if options[:format_markdown]
452
+ format("# * `%s`%s:\n# * **`%s`**\n",
453
+ format_name.call(fk),
454
+ constraints_info.blank? ? '' : " (_#{constraints_info}_)",
455
+ ref_info)
456
+ else
457
+ format("# %-#{max_size}.#{max_size}s %s %s",
458
+ format_name.call(fk),
459
+ "(#{ref_info})",
460
+ constraints_info).rstrip + "\n"
461
+ end
462
+ end
463
+
464
+ fk_info
465
+ end
466
+
467
+ def get_schema_footer_text(_klass, options = {})
468
+ info = ''
469
+ if options[:format_rdoc]
470
+ info << "#--\n"
471
+ info << "# #{END_MARK}\n"
472
+ info << "#++\n"
473
+ else
474
+ info << "#\n"
475
+ end
476
+ end
477
+ end
478
+ end
479
+ end
480
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ autoload :Annotator, 'annotate_rb/model_annotator/annotator'
6
+ autoload :Helper, 'annotate_rb/model_annotator/helper'
7
+ autoload :FilePatterns, 'annotate_rb/model_annotator/file_patterns'
8
+ autoload :Constants, 'annotate_rb/model_annotator/constants'
9
+ autoload :SchemaInfo, 'annotate_rb/model_annotator/schema_info'
10
+ autoload :PatternGetter, 'annotate_rb/model_annotator/pattern_getter'
11
+ autoload :BadModelFileError, 'annotate_rb/model_annotator/bad_model_file_error'
12
+ autoload :FileNameResolver, 'annotate_rb/model_annotator/file_name_resolver'
13
+ autoload :FileAnnotationRemover, 'annotate_rb/model_annotator/file_annotation_remover'
14
+ autoload :AnnotationPatternGenerator, 'annotate_rb/model_annotator/annotation_pattern_generator'
15
+ autoload :ModelClassGetter, 'annotate_rb/model_annotator/model_class_getter'
16
+ autoload :ModelFilesGetter, 'annotate_rb/model_annotator/model_files_getter'
17
+ autoload :FileAnnotator, 'annotate_rb/model_annotator/file_annotator'
18
+ autoload :ModelFileAnnotator, 'annotate_rb/model_annotator/model_file_annotator'
19
+ end
20
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module AnnotateRb
6
+ # Used to hold all of the options when annotating models and routes.
7
+ # Should be the source of truth for what are valid options.
8
+ class Options
9
+ extend Forwardable
10
+
11
+ class << self
12
+ def from(options = {}, state = {})
13
+ new(options, state).load_defaults
14
+ end
15
+ end
16
+
17
+ POSITION_OPTIONS = {
18
+ position: nil, # ModelAnnotator, RouteAnnotator
19
+ position_in_class: nil, # ModelAnnotator
20
+ position_in_factory: nil, # Unused
21
+ position_in_fixture: nil, # Unused
22
+ position_in_routes: nil, # RouteAnnotator
23
+ position_in_serializer: nil, # Unused
24
+ position_in_test: nil, # Unused
25
+ }.freeze
26
+
27
+ FLAG_OPTIONS = {
28
+ classified_sort: true, # ModelAnnotator
29
+ exclude_controllers: true, # Unused
30
+ exclude_factories: false, # Unused
31
+ exclude_fixtures: false, # Unused
32
+ exclude_helpers: true, # Unused
33
+ exclude_scaffolds: true, # Unused
34
+ exclude_serializers: false, # Unused
35
+ exclude_sti_subclasses: false, # ModelAnnotator
36
+ exclude_tests: false, # Unused
37
+ force: false, # ModelAnnotator, but should be used by both
38
+ format_bare: true, # Unused
39
+ format_markdown: false, # ModelAnnotator, RouteAnnotator
40
+ format_rdoc: false, # ModelAnnotator
41
+ format_yard: false, # ModelAnnotator
42
+ frozen: false, # ModelAnnotator, but should be used by both
43
+ ignore_model_sub_dir: false, # ModelAnnotator
44
+ ignore_unknown_models: false, # ModelAnnotator
45
+ include_version: false, # ModelAnnotator
46
+ show_complete_foreign_keys: false, # ModelAnnotator
47
+ show_foreign_keys: true, # ModelAnnotator
48
+ show_indexes: true, # ModelAnnotator
49
+ simple_indexes: false, # ModelAnnotator
50
+ sort: false, # ModelAnnotator
51
+ timestamp: false, # RouteAnnotator
52
+ trace: false, # ModelAnnotator, but is part of Core
53
+ with_comment: true, # ModelAnnotator
54
+ }.freeze
55
+
56
+ OTHER_OPTIONS = {
57
+ active_admin: false, # ModelAnnotator
58
+ command: nil, # Core
59
+ debug: false, # Core
60
+
61
+ # ModelAnnotator
62
+ hide_default_column_types: '<%= ::AnnotateRb::ModelAnnotator::SchemaInfo::NO_DEFAULT_COL_TYPES.join(",") %>',
63
+
64
+ # ModelAnnotator
65
+ hide_limit_column_types: '<%= ::AnnotateRb::ModelAnnotator::SchemaInfo::NO_LIMIT_COL_TYPES.join(",") %>',
66
+
67
+ ignore_columns: nil, # ModelAnnotator
68
+ ignore_routes: nil, # RouteAnnotator
69
+ ignore_unknown_models: false, # ModelAnnotator
70
+ models: true, # Core
71
+ routes: false, # Core
72
+ skip_on_db_migrate: false, # Core
73
+ target_action: :do_annotations, # Core; Possible values: :do_annotations, :remove_annotations
74
+ wrapper: nil, # ModelAnnotator, RouteAnnotator
75
+ wrapper_close: nil, # ModelAnnotator, RouteAnnotator
76
+ wrapper_open: nil, # ModelAnnotator, RouteAnnotator
77
+ }.freeze
78
+
79
+ PATH_OPTIONS = {
80
+ additional_file_patterns: [], # ModelAnnotator
81
+ model_dir: ['app/models'], # ModelAnnotator
82
+ require: [], # Core
83
+ root_dir: [''], # Core; Old model Annotate code depends on it being empty when not provided another value
84
+ # `root_dir` can also be a string but should get converted into an array with that string as the sole element when
85
+ # that happens.
86
+ }.freeze
87
+
88
+ DEFAULT_OPTIONS = {}.merge(POSITION_OPTIONS, FLAG_OPTIONS, OTHER_OPTIONS, PATH_OPTIONS).freeze
89
+
90
+ POSITION_OPTION_KEYS = [
91
+ :position,
92
+ :position_in_class,
93
+ :position_in_routes,
94
+ ].freeze
95
+
96
+ FLAG_OPTION_KEYS = [
97
+ :classified_sort,
98
+ :exclude_sti_subclasses,
99
+ :force,
100
+ :format_markdown,
101
+ :format_rdoc,
102
+ :format_yard,
103
+ :frozen,
104
+ :ignore_model_sub_dir,
105
+ :ignore_unknown_models,
106
+ :include_version,
107
+ :show_complete_foreign_keys,
108
+ :show_foreign_keys,
109
+ :show_indexes,
110
+ :simple_indexes,
111
+ :sort,
112
+ :timestamp,
113
+ :trace,
114
+ :with_comment,
115
+ ].freeze
116
+
117
+ OTHER_OPTION_KEYS = [
118
+ :active_admin,
119
+ :command,
120
+ :debug,
121
+ :hide_default_column_types,
122
+ :hide_limit_column_types,
123
+ :ignore_columns,
124
+ :ignore_routes,
125
+ :ignore_unknown_models,
126
+ :models,
127
+ :routes,
128
+ :skip_on_db_migrate,
129
+ :target_action,
130
+ :wrapper,
131
+ :wrapper_close,
132
+ :wrapper_open,
133
+ ].freeze
134
+
135
+ PATH_OPTION_KEYS = [
136
+ :additional_file_patterns,
137
+ :model_dir,
138
+ :require,
139
+ :root_dir,
140
+ ].freeze
141
+
142
+ ALL_OPTION_KEYS = [
143
+ POSITION_OPTION_KEYS, FLAG_OPTION_KEYS, OTHER_OPTION_KEYS, PATH_OPTION_KEYS
144
+ ].flatten.freeze
145
+
146
+ POSITION_DEFAULT = 'before'
147
+
148
+ # Want this to be read only after initializing
149
+ def_delegator :@options, :[]
150
+
151
+ def initialize(options = {}, state = {})
152
+ @options = options
153
+
154
+ # For now, state is a hash to store state that we need but is not a configuration option
155
+ @state = state
156
+ end
157
+
158
+ def to_h
159
+ @options.to_h
160
+ end
161
+
162
+ def load_defaults
163
+ ALL_OPTION_KEYS.each do |key|
164
+ @options[key] = DEFAULT_OPTIONS[key] unless @options.key?(key)
165
+ end
166
+
167
+ # Set all of the position options in the following order:
168
+ # 1) Use the value if it's defined
169
+ # 2) Use value from :position if it's defined
170
+ # 3) Use default
171
+ POSITION_OPTION_KEYS.each do |key|
172
+ @options[key] = ModelAnnotator::Helper.fallback(
173
+ @options[key], @options[:position], POSITION_DEFAULT
174
+ )
175
+ end
176
+
177
+ # Unpack path options if we're passed in a String
178
+ PATH_OPTION_KEYS.each do |key|
179
+ if @options[key].is_a?(String)
180
+ @options[key] = @options[key].split(',').map(&:strip).reject(&:empty?)
181
+ end
182
+ end
183
+
184
+ # Set wrapper to default to :wrapper
185
+ @options[:wrapper_open] ||= @options[:wrapper]
186
+ @options[:wrapper_close] ||= @options[:wrapper]
187
+
188
+ self
189
+ end
190
+
191
+ def set_state(key, value, overwrite = false)
192
+ if @state.key?(key) && !overwrite
193
+ val = @state[key]
194
+ raise ArgumentError, "Attempting to write '#{value}' to state with key '#{key}', but it already exists with '#{val}'."
195
+ end
196
+
197
+ @state[key] = value
198
+ end
199
+
200
+ def get_state(key)
201
+ @state[key]
202
+ end
203
+ end
204
+ end