annotaterb 4.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,480 +0,0 @@
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