annotaterb 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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