annotaterb 4.0.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE.txt +55 -0
- data/README.md +91 -0
- data/VERSION +1 -0
- data/exe/annotaterb +21 -0
- data/lib/annotate_rb/active_record_patch.rb +9 -0
- data/lib/annotate_rb/commands/annotate_models.rb +22 -0
- data/lib/annotate_rb/commands/annotate_routes.rb +19 -0
- data/lib/annotate_rb/commands/print_help.rb +16 -0
- data/lib/annotate_rb/commands/print_version.rb +12 -0
- data/lib/annotate_rb/commands.rb +10 -0
- data/lib/annotate_rb/config_finder.rb +21 -0
- data/lib/annotate_rb/config_loader.rb +63 -0
- data/lib/annotate_rb/core.rb +23 -0
- data/lib/annotate_rb/eager_loader.rb +23 -0
- data/lib/annotate_rb/env.rb +30 -0
- data/lib/annotate_rb/model_annotator/annotation_pattern_generator.rb +19 -0
- data/lib/annotate_rb/model_annotator/annotator.rb +74 -0
- data/lib/annotate_rb/model_annotator/bad_model_file_error.rb +11 -0
- data/lib/annotate_rb/model_annotator/constants.rb +22 -0
- data/lib/annotate_rb/model_annotator/file_annotation_remover.rb +25 -0
- data/lib/annotate_rb/model_annotator/file_annotator.rb +79 -0
- data/lib/annotate_rb/model_annotator/file_name_resolver.rb +16 -0
- data/lib/annotate_rb/model_annotator/file_patterns.rb +129 -0
- data/lib/annotate_rb/model_annotator/helper.rb +54 -0
- data/lib/annotate_rb/model_annotator/model_class_getter.rb +63 -0
- data/lib/annotate_rb/model_annotator/model_file_annotator.rb +118 -0
- data/lib/annotate_rb/model_annotator/model_files_getter.rb +62 -0
- data/lib/annotate_rb/model_annotator/pattern_getter.rb +27 -0
- data/lib/annotate_rb/model_annotator/schema_info.rb +480 -0
- data/lib/annotate_rb/model_annotator.rb +20 -0
- data/lib/annotate_rb/options.rb +204 -0
- data/lib/annotate_rb/parser.rb +385 -0
- data/lib/annotate_rb/rake_bootstrapper.rb +34 -0
- data/lib/annotate_rb/route_annotator/annotation_processor.rb +56 -0
- data/lib/annotate_rb/route_annotator/annotator.rb +40 -0
- data/lib/annotate_rb/route_annotator/base_processor.rb +104 -0
- data/lib/annotate_rb/route_annotator/header_generator.rb +113 -0
- data/lib/annotate_rb/route_annotator/helper.rb +104 -0
- data/lib/annotate_rb/route_annotator/removal_processor.rb +40 -0
- data/lib/annotate_rb/route_annotator.rb +12 -0
- data/lib/annotate_rb/runner.rb +34 -0
- data/lib/annotate_rb/tasks/annotate_models_migrate.rake +30 -0
- data/lib/annotate_rb.rb +30 -0
- data/lib/generators/annotate_rb/USAGE +4 -0
- data/lib/generators/annotate_rb/install_generator.rb +15 -0
- data/lib/generators/annotate_rb/templates/auto_annotate_models.rake +7 -0
- 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
|