annot8 1.0.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.
- checksums.yaml +7 -0
- data/AUTHORS.md +34 -0
- data/CHANGELOG.md +341 -0
- data/LICENSE.txt +55 -0
- data/README.md +331 -0
- data/RELEASE.md +19 -0
- data/annot8.gemspec +32 -0
- data/bin/annotate +31 -0
- data/lib/annotate/active_record_patch.rb +11 -0
- data/lib/annotate/annotate_models/file_patterns.rb +129 -0
- data/lib/annotate/annotate_models.rb +1005 -0
- data/lib/annotate/annotate_routes/header_generator.rb +117 -0
- data/lib/annotate/annotate_routes/helpers.rb +71 -0
- data/lib/annotate/annotate_routes.rb +121 -0
- data/lib/annotate/constants.rb +30 -0
- data/lib/annotate/helpers.rb +33 -0
- data/lib/annotate/parser.rb +317 -0
- data/lib/annotate/tasks.rb +8 -0
- data/lib/annotate/version.rb +7 -0
- data/lib/annotate.rb +152 -0
- data/lib/generators/annotate/USAGE +4 -0
- data/lib/generators/annotate/install_generator.rb +17 -0
- data/lib/generators/annotate/templates/auto_annotate_models.rake +63 -0
- data/lib/tasks/annotate_models.rake +79 -0
- data/lib/tasks/annotate_models_migrate.rake +65 -0
- data/lib/tasks/annotate_routes.rake +34 -0
- data/potato.md +41 -0
- metadata +120 -0
@@ -0,0 +1,1005 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/ModuleLength
|
4
|
+
|
5
|
+
require 'bigdecimal'
|
6
|
+
|
7
|
+
require 'annotate/constants'
|
8
|
+
require_relative 'annotate_models/file_patterns'
|
9
|
+
|
10
|
+
module AnnotateModels
|
11
|
+
# Annotate Models plugin use this header
|
12
|
+
COMPAT_PREFIX = '== Schema Info'
|
13
|
+
COMPAT_PREFIX_MD = '## Schema Info'
|
14
|
+
PREFIX = '== Schema Information'
|
15
|
+
PREFIX_MD = '## Schema Information'
|
16
|
+
END_MARK = '== Schema Information End'
|
17
|
+
|
18
|
+
SKIP_ANNOTATION_PREFIX = '# -\*- SkipSchemaAnnotations'
|
19
|
+
|
20
|
+
MATCHED_TYPES = %w[test fixture factory serializer scaffold controller helper].freeze
|
21
|
+
|
22
|
+
# Don't show limit (#) on these column types
|
23
|
+
# Example: show "integer" instead of "integer(4)"
|
24
|
+
NO_LIMIT_COL_TYPES = %w[integer bigint boolean].freeze
|
25
|
+
|
26
|
+
# Don't show default value for these column types
|
27
|
+
NO_DEFAULT_COL_TYPES = %w[json jsonb hstore].freeze
|
28
|
+
|
29
|
+
INDEX_CLAUSES = {
|
30
|
+
unique: {
|
31
|
+
default: 'UNIQUE',
|
32
|
+
markdown: '_unique_'
|
33
|
+
},
|
34
|
+
where: {
|
35
|
+
default: 'WHERE',
|
36
|
+
markdown: '_where_'
|
37
|
+
},
|
38
|
+
using: {
|
39
|
+
default: 'USING',
|
40
|
+
markdown: '_using_'
|
41
|
+
}
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
MAGIC_COMMENT_MATCHER = /(^#\s*encoding:.*(?:\n|r\n))|(^# coding:.*(?:\n|\r\n))|(^# -\*- coding:.*(?:\n|\r\n))|(^# -\*- encoding\s?:.*(?:\n|\r\n))|(^#\s*frozen_string_literal:.+(?:\n|\r\n))|(^# -\*- frozen_string_literal\s*:.+-\*-(?:\n|\r\n))/
|
45
|
+
|
46
|
+
class << self
|
47
|
+
def annotate_pattern(options = {})
|
48
|
+
if options[:wrapper_open]
|
49
|
+
return /(?:^(\n|\r\n)?# (?:#{options[:wrapper_open]}).*(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*)|^(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*/
|
50
|
+
end
|
51
|
+
|
52
|
+
/^(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*/
|
53
|
+
end
|
54
|
+
|
55
|
+
def model_dir
|
56
|
+
@model_dir.is_a?(Array) ? @model_dir : [@model_dir || 'app/models']
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_writer :model_dir, :root_dir, :skip_subdirectory_model_load
|
60
|
+
|
61
|
+
def root_dir
|
62
|
+
if @root_dir.blank?
|
63
|
+
['']
|
64
|
+
elsif @root_dir.is_a?(String)
|
65
|
+
@root_dir.split(',')
|
66
|
+
else
|
67
|
+
@root_dir
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def skip_subdirectory_model_load
|
72
|
+
# This option is set in options[:skip_subdirectory_model_load]
|
73
|
+
# and stops the get_loaded_model method from loading a model from a subdir
|
74
|
+
|
75
|
+
if @skip_subdirectory_model_load.blank?
|
76
|
+
false
|
77
|
+
else
|
78
|
+
@skip_subdirectory_model_load
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_patterns(options, pattern_types = [])
|
83
|
+
current_patterns = []
|
84
|
+
root_dir.each do |root_directory|
|
85
|
+
Array(pattern_types).each do |pattern_type|
|
86
|
+
patterns = FilePatterns.generate(root_directory, pattern_type, options)
|
87
|
+
|
88
|
+
current_patterns += if pattern_type.to_sym == :additional_file_patterns
|
89
|
+
patterns
|
90
|
+
else
|
91
|
+
patterns.map { |p| p.sub(%r{^[/]*}, '') }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
current_patterns
|
96
|
+
end
|
97
|
+
|
98
|
+
# Simple quoting for the default column value
|
99
|
+
def quote(value)
|
100
|
+
case value
|
101
|
+
when NilClass then 'NULL'
|
102
|
+
when TrueClass then 'TRUE'
|
103
|
+
when FalseClass then 'FALSE'
|
104
|
+
when Float, Integer then value.to_s
|
105
|
+
# BigDecimals need to be output in a non-normalized form and quoted.
|
106
|
+
when BigDecimal then value.to_s('F')
|
107
|
+
when Array then value.map { |v| quote(v) }
|
108
|
+
else
|
109
|
+
value.inspect
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def schema_default(klass, column)
|
114
|
+
quote(klass.column_defaults[column.name])
|
115
|
+
end
|
116
|
+
|
117
|
+
def retrieve_indexes_from_table(klass)
|
118
|
+
table_name = klass.table_name
|
119
|
+
return [] unless table_name
|
120
|
+
|
121
|
+
indexes = klass.connection.indexes(table_name)
|
122
|
+
return indexes if indexes.any? || !klass.table_name_prefix
|
123
|
+
|
124
|
+
# Try to search the table without prefix
|
125
|
+
table_name_without_prefix = table_name.to_s.sub(klass.table_name_prefix, '')
|
126
|
+
if klass.connection.table_exists?(table_name_without_prefix)
|
127
|
+
klass.connection.indexes(table_name_without_prefix)
|
128
|
+
else
|
129
|
+
[]
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Use the column information in an ActiveRecord class
|
134
|
+
# to create a comment block containing a line for
|
135
|
+
# each column. The line contains the column name,
|
136
|
+
# the type (and length), and any optional attributes
|
137
|
+
def get_schema_info(klass, header, options = {}) # rubocop:disable Metrics/MethodLength
|
138
|
+
info = String.new("# #{header}\n")
|
139
|
+
info << get_schema_header_text(klass, options)
|
140
|
+
|
141
|
+
max_size = max_schema_info_width(klass, options)
|
142
|
+
md_names_overhead = 6
|
143
|
+
md_type_allowance = 18
|
144
|
+
bare_type_allowance = 16
|
145
|
+
|
146
|
+
if options[:format_markdown]
|
147
|
+
info << format(
|
148
|
+
"# %-#{max_size + md_names_overhead}.#{max_size + md_names_overhead}s | %-#{md_type_allowance}.#{md_type_allowance}s | %s\n", 'Name', 'Type', 'Attributes'
|
149
|
+
)
|
150
|
+
|
151
|
+
info << "# #{'-' * (max_size + md_names_overhead)} | #{'-' * md_type_allowance} | #{'-' * 27}\n"
|
152
|
+
end
|
153
|
+
|
154
|
+
cols = columns(klass, options)
|
155
|
+
with_comments = with_comments?(klass, options)
|
156
|
+
with_comments_column = with_comments_column?(klass, options)
|
157
|
+
|
158
|
+
# Precalculate Values
|
159
|
+
cols_meta = cols.to_h do |col|
|
160
|
+
col_comment = with_comments || with_comments_column ? col.comment&.gsub("\n", '\\n') : nil
|
161
|
+
col_type = get_col_type(col)
|
162
|
+
attrs = get_attributes(col, col_type, klass, options)
|
163
|
+
col_name = if with_comments && col_comment
|
164
|
+
"#{col.name}(#{col_comment})"
|
165
|
+
else
|
166
|
+
col.name
|
167
|
+
end
|
168
|
+
simple_formatted_attrs = attrs.join(', ')
|
169
|
+
[col.name,
|
170
|
+
{ col_type: col_type,
|
171
|
+
attrs: attrs,
|
172
|
+
col_name: col_name,
|
173
|
+
simple_formatted_attrs: simple_formatted_attrs,
|
174
|
+
col_comment: col_comment }]
|
175
|
+
end
|
176
|
+
|
177
|
+
# Output annotation
|
178
|
+
bare_max_attrs_length = cols_meta.map { |_, m| m[:simple_formatted_attrs].length }.max
|
179
|
+
|
180
|
+
cols.each do |col|
|
181
|
+
col_type = cols_meta[col.name][:col_type]
|
182
|
+
attrs = cols_meta[col.name][:attrs]
|
183
|
+
col_name = cols_meta[col.name][:col_name]
|
184
|
+
simple_formatted_attrs = cols_meta[col.name][:simple_formatted_attrs]
|
185
|
+
col_comment = cols_meta[col.name][:col_comment]
|
186
|
+
|
187
|
+
if options[:format_rdoc]
|
188
|
+
info << (format("# %-#{max_size}.#{max_size}s<tt>%s</tt>", "*#{col_name}*::",
|
189
|
+
attrs.unshift(col_type).join(', ')).rstrip + "\n")
|
190
|
+
elsif options[:format_yard]
|
191
|
+
info << (sprintf("# @!attribute #{col_name}") + "\n")
|
192
|
+
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)
|
193
|
+
info << (sprintf("# @return [#{ruby_class}]") + "\n")
|
194
|
+
elsif options[:format_markdown]
|
195
|
+
name_remainder = max_size - col_name.length - non_ascii_length(col_name)
|
196
|
+
type_remainder = (md_type_allowance - 2) - col_type.length
|
197
|
+
info << (format("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`", col_name, ' ', col_type, ' ', attrs.join(', ').rstrip).gsub(
|
198
|
+
'``', ' '
|
199
|
+
).rstrip + "\n")
|
200
|
+
elsif with_comments_column
|
201
|
+
info << format_default(col_name, max_size, col_type, bare_type_allowance, simple_formatted_attrs,
|
202
|
+
bare_max_attrs_length, col_comment)
|
203
|
+
else
|
204
|
+
info << format_default(col_name, max_size, col_type, bare_type_allowance, simple_formatted_attrs)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
info << get_index_info(klass, options) if options[:show_indexes] && klass.table_exists?
|
209
|
+
|
210
|
+
info << get_foreign_key_info(klass, options) if options[:show_foreign_keys] && klass.table_exists?
|
211
|
+
|
212
|
+
info << get_check_constraint_info(klass, options) if options[:show_check_constraints] && klass.table_exists?
|
213
|
+
|
214
|
+
info << get_schema_footer_text(klass, options)
|
215
|
+
end
|
216
|
+
|
217
|
+
def get_schema_header_text(klass, options = {})
|
218
|
+
info = String.new("#\n") # Create a new mutable string
|
219
|
+
if options[:format_markdown]
|
220
|
+
info << "# Table name: `#{klass.table_name}`\n"
|
221
|
+
info << "#\n"
|
222
|
+
info << "# ### Columns\n"
|
223
|
+
else
|
224
|
+
info << "# Table name: #{klass.table_name}\n"
|
225
|
+
end
|
226
|
+
info << "#\n"
|
227
|
+
info
|
228
|
+
end
|
229
|
+
|
230
|
+
def get_schema_footer_text(_klass, options = {})
|
231
|
+
info = String.new('')
|
232
|
+
if options[:format_rdoc]
|
233
|
+
info << "#--\n"
|
234
|
+
info << "# #{END_MARK}\n"
|
235
|
+
info << "#++\n"
|
236
|
+
else
|
237
|
+
info << "#\n"
|
238
|
+
end
|
239
|
+
info
|
240
|
+
end
|
241
|
+
|
242
|
+
def get_index_info(klass, options = {})
|
243
|
+
index_info = if options[:format_markdown]
|
244
|
+
String.new("#\n# ### Indexes\n#\n")
|
245
|
+
else
|
246
|
+
String.new("#\n# Indexes\n#\n")
|
247
|
+
end
|
248
|
+
|
249
|
+
indexes = retrieve_indexes_from_table(klass)
|
250
|
+
return '' if indexes.empty?
|
251
|
+
|
252
|
+
max_size = indexes.collect { |index| index.name.size }.max + 1
|
253
|
+
indexes.sort_by(&:name).each do |index|
|
254
|
+
index_info << if options[:format_markdown]
|
255
|
+
final_index_string_in_markdown(index)
|
256
|
+
else
|
257
|
+
final_index_string(index, max_size)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
index_info
|
262
|
+
end
|
263
|
+
|
264
|
+
def get_col_type(col)
|
265
|
+
if (col.respond_to?(:bigint?) && col.bigint?) || /\Abigint\b/ =~ col.sql_type
|
266
|
+
'bigint'
|
267
|
+
else
|
268
|
+
(col.type || col.sql_type).to_s
|
269
|
+
end.dup
|
270
|
+
end
|
271
|
+
|
272
|
+
def index_columns_info(index)
|
273
|
+
Array(index.columns).map do |col|
|
274
|
+
if index.try(:orders) && index.orders[col.to_s]
|
275
|
+
"#{col} #{index.orders[col.to_s].upcase}"
|
276
|
+
else
|
277
|
+
col.to_s.gsub("\r", '\r').gsub("\n", '\n')
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def index_unique_info(index, format = :default)
|
283
|
+
index.unique ? " #{INDEX_CLAUSES[:unique][format]}" : ''
|
284
|
+
end
|
285
|
+
|
286
|
+
def index_where_info(index, format = :default)
|
287
|
+
value = index.try(:where).try(:to_s)
|
288
|
+
if value.blank?
|
289
|
+
''
|
290
|
+
else
|
291
|
+
" #{INDEX_CLAUSES[:where][format]} #{value}"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def index_using_info(index, format = :default)
|
296
|
+
value = index.try(:using) && index.using.try(:to_sym)
|
297
|
+
if !value.blank? && value != :btree
|
298
|
+
" #{INDEX_CLAUSES[:using][format]} #{value}"
|
299
|
+
else
|
300
|
+
''
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def final_index_string_in_markdown(index)
|
305
|
+
details = format(
|
306
|
+
'%s%s%s',
|
307
|
+
index_unique_info(index, :markdown),
|
308
|
+
index_where_info(index, :markdown),
|
309
|
+
index_using_info(index, :markdown)
|
310
|
+
).strip
|
311
|
+
details = " (#{details})" unless details.blank?
|
312
|
+
|
313
|
+
format(
|
314
|
+
"# * `%s`%s:\n# * **`%s`**\n",
|
315
|
+
index.name,
|
316
|
+
details,
|
317
|
+
index_columns_info(index).join("`**\n# * **`")
|
318
|
+
)
|
319
|
+
end
|
320
|
+
|
321
|
+
def final_index_string(index, max_size)
|
322
|
+
format(
|
323
|
+
"# %-#{max_size}.#{max_size}s %s%s%s%s",
|
324
|
+
index.name,
|
325
|
+
"(#{index_columns_info(index).join(',')})",
|
326
|
+
index_unique_info(index),
|
327
|
+
index_where_info(index),
|
328
|
+
index_using_info(index)
|
329
|
+
).rstrip + "\n"
|
330
|
+
end
|
331
|
+
|
332
|
+
def hide_limit?(col_type, options)
|
333
|
+
excludes =
|
334
|
+
if options[:hide_limit_column_types].blank?
|
335
|
+
NO_LIMIT_COL_TYPES
|
336
|
+
else
|
337
|
+
options[:hide_limit_column_types].split(',')
|
338
|
+
end
|
339
|
+
|
340
|
+
excludes.include?(col_type)
|
341
|
+
end
|
342
|
+
|
343
|
+
def hide_default?(col_type, options)
|
344
|
+
excludes =
|
345
|
+
if options[:hide_default_column_types].blank?
|
346
|
+
NO_DEFAULT_COL_TYPES
|
347
|
+
else
|
348
|
+
options[:hide_default_column_types].split(',')
|
349
|
+
end
|
350
|
+
|
351
|
+
excludes.include?(col_type)
|
352
|
+
end
|
353
|
+
|
354
|
+
def get_foreign_key_info(klass, options = {})
|
355
|
+
fk_info = if options[:format_markdown]
|
356
|
+
String.new("#\n# ### Foreign Keys\n#\n")
|
357
|
+
else
|
358
|
+
String.new("#\n# Foreign Keys\n#\n")
|
359
|
+
end
|
360
|
+
|
361
|
+
return '' unless klass.connection.respond_to?(:supports_foreign_keys?) &&
|
362
|
+
klass.connection.supports_foreign_keys? && klass.connection.respond_to?(:foreign_keys)
|
363
|
+
|
364
|
+
foreign_keys = klass.connection.foreign_keys(klass.table_name)
|
365
|
+
return '' if foreign_keys.empty?
|
366
|
+
|
367
|
+
format_name = lambda do |fk|
|
368
|
+
return fk.options[:column] if fk.name.blank?
|
369
|
+
|
370
|
+
options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, '...')
|
371
|
+
end
|
372
|
+
|
373
|
+
max_size = foreign_keys.map(&format_name).map(&:size).max + 1
|
374
|
+
foreign_keys.sort_by { |fk| [format_name.call(fk), fk.column] }.each do |fk|
|
375
|
+
ref_info = "#{fk.column} => #{fk.to_table}.#{fk.primary_key}"
|
376
|
+
constraints_info = String.new('')
|
377
|
+
constraints_info += "ON DELETE => #{fk.on_delete} " if fk.on_delete
|
378
|
+
constraints_info += "ON UPDATE => #{fk.on_update} " if fk.on_update
|
379
|
+
constraints_info.strip!
|
380
|
+
|
381
|
+
fk_info << if options[:format_markdown]
|
382
|
+
format("# * `%s`%s:\n# * **`%s`**\n", format_name.call(fk),
|
383
|
+
constraints_info.blank? ? '' : " (_#{constraints_info}_)", ref_info)
|
384
|
+
else
|
385
|
+
format("# %-#{max_size}.#{max_size}s %s %s", format_name.call(fk), "(#{ref_info})",
|
386
|
+
constraints_info).rstrip + "\n"
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
fk_info
|
391
|
+
end
|
392
|
+
|
393
|
+
def get_check_constraint_info(klass, options = {})
|
394
|
+
cc_info = if options[:format_markdown]
|
395
|
+
String.new("#\n# ### Check Constraints\n#\n")
|
396
|
+
else
|
397
|
+
String.new("#\n# Check Constraints\n#\n")
|
398
|
+
end
|
399
|
+
|
400
|
+
return '' unless klass.connection.respond_to?(:supports_check_constraints?) &&
|
401
|
+
klass.connection.supports_check_constraints? && klass.connection.respond_to?(:check_constraints)
|
402
|
+
|
403
|
+
check_constraints = klass.connection.check_constraints(klass.table_name)
|
404
|
+
return '' if check_constraints.empty?
|
405
|
+
|
406
|
+
max_size = check_constraints.map { |check_constraint| check_constraint.name.size }.max + 1
|
407
|
+
check_constraints.sort_by(&:name).each do |check_constraint|
|
408
|
+
expression = check_constraint.expression ? "(#{check_constraint.expression.squish})" : nil
|
409
|
+
|
410
|
+
cc_info << if options[:format_markdown]
|
411
|
+
cc_info_markdown = format('# * `%s`', check_constraint.name)
|
412
|
+
cc_info_markdown << format(': `%s`', expression) if expression
|
413
|
+
cc_info_markdown << "\n"
|
414
|
+
else
|
415
|
+
format("# %-#{max_size}.#{max_size}s %s", check_constraint.name, expression).rstrip + "\n"
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
cc_info
|
420
|
+
end
|
421
|
+
|
422
|
+
# Add a schema block to a file. If the file already contains
|
423
|
+
# a schema info block (a comment starting with "== Schema Information"),
|
424
|
+
# check if it matches the block that is already there. If so, leave it be.
|
425
|
+
# If not, remove the old info block and write a new one.
|
426
|
+
#
|
427
|
+
# == Returns:
|
428
|
+
# true or false depending on whether the file was modified.
|
429
|
+
#
|
430
|
+
# === Options (opts)
|
431
|
+
# :force<Symbol>:: whether to update the file even if it doesn't seem to need it.
|
432
|
+
# :position_in_*<Symbol>:: where to place the annotated section in fixture or model file,
|
433
|
+
# :before, :top, :after or :bottom. Default is :before.
|
434
|
+
#
|
435
|
+
def annotate_one_file(file_name, info_block, position, options = {})
|
436
|
+
return false unless File.exist?(file_name)
|
437
|
+
|
438
|
+
old_content = File.read(file_name)
|
439
|
+
return false if old_content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/
|
440
|
+
|
441
|
+
# Ignore the Schema version line because it changes with each migration
|
442
|
+
header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/
|
443
|
+
old_header = old_content.match(header_pattern).to_s
|
444
|
+
new_header = info_block.match(header_pattern).to_s
|
445
|
+
|
446
|
+
column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/
|
447
|
+
old_columns = old_header && old_header.scan(column_pattern).sort
|
448
|
+
new_columns = new_header && new_header.scan(column_pattern).sort
|
449
|
+
|
450
|
+
return false if old_columns == new_columns && !options[:force]
|
451
|
+
|
452
|
+
if options[:frozen]
|
453
|
+
abort "annotate error. #{file_name} needs to be updated, but annotate was run with `--frozen`."
|
454
|
+
end
|
455
|
+
|
456
|
+
# Replace inline the old schema info with the new schema info
|
457
|
+
wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ''
|
458
|
+
wrapper_close = options[:wrapper_close] ? "# #{options[:wrapper_close]}\n" : ''
|
459
|
+
wrapped_info_block = "#{wrapper_open}#{info_block}#{wrapper_close}"
|
460
|
+
|
461
|
+
old_annotation = old_content.match(annotate_pattern(options)).to_s
|
462
|
+
|
463
|
+
# if there *was* no old schema info or :force was passed, we simply
|
464
|
+
# need to insert it in correct position
|
465
|
+
if old_annotation.empty? || options[:force]
|
466
|
+
magic_comments_block = magic_comments_as_string(old_content)
|
467
|
+
old_content.gsub!(MAGIC_COMMENT_MATCHER, '')
|
468
|
+
old_content.sub!(annotate_pattern(options), '')
|
469
|
+
|
470
|
+
new_content = if %w[after bottom].include?(options[position].to_s)
|
471
|
+
magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block)
|
472
|
+
elsif magic_comments_block.empty?
|
473
|
+
magic_comments_block + wrapped_info_block + old_content.lstrip
|
474
|
+
else
|
475
|
+
magic_comments_block + "\n" + wrapped_info_block + old_content.lstrip
|
476
|
+
end
|
477
|
+
else
|
478
|
+
# replace the old annotation with the new one
|
479
|
+
|
480
|
+
# keep the surrounding whitespace the same
|
481
|
+
space_match = old_annotation.match(/\A(?<start>\s*).*?\n(?<end>\s*)\z/m)
|
482
|
+
new_annotation = space_match[:start] + wrapped_info_block + space_match[:end]
|
483
|
+
|
484
|
+
new_content = old_content.sub(annotate_pattern(options), new_annotation)
|
485
|
+
end
|
486
|
+
|
487
|
+
File.open(file_name, 'wb') { |f| f.puts new_content }
|
488
|
+
true
|
489
|
+
end
|
490
|
+
|
491
|
+
def magic_comments_as_string(content)
|
492
|
+
magic_comments = content.scan(MAGIC_COMMENT_MATCHER).flatten.compact
|
493
|
+
|
494
|
+
if magic_comments.any?
|
495
|
+
magic_comments.join
|
496
|
+
else
|
497
|
+
''
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
def remove_annotation_of_file(file_name, options = {})
|
502
|
+
if File.exist?(file_name)
|
503
|
+
content = File.read(file_name)
|
504
|
+
return false if content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/
|
505
|
+
|
506
|
+
wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ''
|
507
|
+
content.sub!(/(#{wrapper_open})?#{annotate_pattern(options)}/, '')
|
508
|
+
|
509
|
+
File.open(file_name, 'wb') { |f| f.puts content }
|
510
|
+
|
511
|
+
true
|
512
|
+
else
|
513
|
+
false
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
def matched_types(options)
|
518
|
+
types = MATCHED_TYPES.dup
|
519
|
+
types << 'admin' if options[:active_admin] =~ Annotate::Constants::TRUE_RE && !types.include?('admin')
|
520
|
+
types << 'additional_file_patterns' if options[:additional_file_patterns].present?
|
521
|
+
|
522
|
+
types
|
523
|
+
end
|
524
|
+
|
525
|
+
# Given the name of an ActiveRecord class, create a schema
|
526
|
+
# info block (basically a comment containing information
|
527
|
+
# on the columns and their types) and put it at the front
|
528
|
+
# of the model and fixture source files.
|
529
|
+
#
|
530
|
+
# === Options (opts)
|
531
|
+
# :position_in_class<Symbol>:: where to place the annotated section in model file
|
532
|
+
# :position_in_test<Symbol>:: where to place the annotated section in test/spec file(s)
|
533
|
+
# :position_in_fixture<Symbol>:: where to place the annotated section in fixture file
|
534
|
+
# :position_in_factory<Symbol>:: where to place the annotated section in factory file
|
535
|
+
# :position_in_serializer<Symbol>:: where to place the annotated section in serializer file
|
536
|
+
# :exclude_tests<Symbol>:: whether to skip modification of test/spec files
|
537
|
+
# :exclude_fixtures<Symbol>:: whether to skip modification of fixture files
|
538
|
+
# :exclude_factories<Symbol>:: whether to skip modification of factory files
|
539
|
+
# :exclude_serializers<Symbol>:: whether to skip modification of serializer files
|
540
|
+
# :exclude_scaffolds<Symbol>:: whether to skip modification of scaffold files
|
541
|
+
# :exclude_controllers<Symbol>:: whether to skip modification of controller files
|
542
|
+
# :exclude_helpers<Symbol>:: whether to skip modification of helper files
|
543
|
+
# :exclude_sti_subclasses<Symbol>:: whether to skip modification of files for STI subclasses
|
544
|
+
#
|
545
|
+
# == Returns:
|
546
|
+
# an array of file names that were annotated.
|
547
|
+
#
|
548
|
+
def annotate(klass, file, header, options = {})
|
549
|
+
begin
|
550
|
+
klass.reset_column_information
|
551
|
+
info = get_schema_info(klass, header, options)
|
552
|
+
model_name = klass.name.underscore
|
553
|
+
table_name = klass.table_name
|
554
|
+
model_file_name = File.join(file)
|
555
|
+
annotated = []
|
556
|
+
|
557
|
+
if annotate_one_file(model_file_name, info, :position_in_class,
|
558
|
+
options_with_position(options, :position_in_class))
|
559
|
+
annotated << model_file_name
|
560
|
+
end
|
561
|
+
|
562
|
+
matched_types(options).each do |key|
|
563
|
+
exclusion_key = :"exclude_#{key.pluralize}"
|
564
|
+
position_key = :"position_in_#{key}"
|
565
|
+
|
566
|
+
# Same options for active_admin models
|
567
|
+
if key == 'admin'
|
568
|
+
exclusion_key = :exclude_class
|
569
|
+
position_key = :position_in_class
|
570
|
+
end
|
571
|
+
|
572
|
+
next if options[exclusion_key]
|
573
|
+
|
574
|
+
get_patterns(options, key)
|
575
|
+
.map { |f| resolve_filename(f, model_name, table_name) }
|
576
|
+
.map { |f| expand_glob_into_files(f) }
|
577
|
+
.flatten
|
578
|
+
.each do |f|
|
579
|
+
annotated << f if annotate_one_file(f, info, position_key, options_with_position(options, position_key))
|
580
|
+
end
|
581
|
+
end
|
582
|
+
rescue StandardError => e
|
583
|
+
warn "Unable to annotate #{file}: #{e.message}"
|
584
|
+
warn "\t" + e.backtrace.join("\n\t") if options[:trace]
|
585
|
+
end
|
586
|
+
|
587
|
+
annotated
|
588
|
+
end
|
589
|
+
|
590
|
+
# position = :position_in_fixture or :position_in_class
|
591
|
+
def options_with_position(options, position_in)
|
592
|
+
options.merge(position: options[position_in] || options[:position])
|
593
|
+
end
|
594
|
+
|
595
|
+
# Return a list of the model files to annotate.
|
596
|
+
# If we have command line arguments, they're assumed to the path
|
597
|
+
# of model files from root dir. Otherwise we take all the model files
|
598
|
+
# in the model_dir directory.
|
599
|
+
def get_model_files(options)
|
600
|
+
model_files = []
|
601
|
+
|
602
|
+
model_files = list_model_files_from_argument unless options[:is_rake]
|
603
|
+
|
604
|
+
return model_files unless model_files.empty?
|
605
|
+
|
606
|
+
model_dir.each do |dir|
|
607
|
+
Dir.chdir(dir) do
|
608
|
+
list = if options[:ignore_model_sub_dir]
|
609
|
+
Dir['*.rb'].map { |f| [dir, f] }
|
610
|
+
else
|
611
|
+
Dir['**/*.rb'].reject { |f| f['concerns/'] }.map { |f| [dir, f] }
|
612
|
+
end
|
613
|
+
model_files.concat(list)
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
model_files
|
618
|
+
rescue SystemCallError
|
619
|
+
warn "No models found in directory '#{model_dir.join("', '")}'."
|
620
|
+
warn 'Either specify models on the command line, or use the --model-dir option.'
|
621
|
+
warn "Call 'annotate --help' for more info."
|
622
|
+
exit 1
|
623
|
+
end
|
624
|
+
|
625
|
+
def list_model_files_from_argument
|
626
|
+
return [] if ARGV.empty?
|
627
|
+
|
628
|
+
specified_files = ARGV.map { |file| File.expand_path(file) }
|
629
|
+
|
630
|
+
model_files = model_dir.flat_map do |dir|
|
631
|
+
absolute_dir_path = File.expand_path(dir)
|
632
|
+
specified_files
|
633
|
+
.find_all { |file| file.start_with?(absolute_dir_path) }
|
634
|
+
.map { |file| [dir, file.sub("#{absolute_dir_path}/", '')] }
|
635
|
+
end
|
636
|
+
|
637
|
+
if model_files.size != specified_files.size
|
638
|
+
puts "The specified file could not be found in directory '#{model_dir.join("', '")}'."
|
639
|
+
puts "Call 'annotate --help' for more info."
|
640
|
+
exit 1
|
641
|
+
end
|
642
|
+
|
643
|
+
model_files
|
644
|
+
end
|
645
|
+
private :list_model_files_from_argument
|
646
|
+
|
647
|
+
# Retrieve the classes belonging to the model names we're asked to process
|
648
|
+
# Check for namespaced models in subdirectories as well as models
|
649
|
+
# in subdirectories without namespacing.
|
650
|
+
def get_model_class(file)
|
651
|
+
model_path = file.gsub(/\.rb$/, '')
|
652
|
+
model_dir.each { |dir| model_path = model_path.gsub(/^#{dir}/, '').gsub(%r{^/}, '') }
|
653
|
+
begin
|
654
|
+
get_loaded_model(model_path, file) || raise(BadModelFileError)
|
655
|
+
rescue LoadError
|
656
|
+
# this is for non-rails projects, which don't get Rails auto-require magic
|
657
|
+
file_path = File.expand_path(file)
|
658
|
+
if File.file?(file_path) && Kernel.require(file_path)
|
659
|
+
retry
|
660
|
+
elsif model_path =~ %r{/}
|
661
|
+
model_path = model_path.split('/')[1..].join('/').to_s
|
662
|
+
retry
|
663
|
+
else
|
664
|
+
raise
|
665
|
+
end
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
# Retrieve loaded model class
|
670
|
+
def get_loaded_model(model_path, file)
|
671
|
+
unless skip_subdirectory_model_load
|
672
|
+
loaded_model_class = get_loaded_model_by_path(model_path)
|
673
|
+
return loaded_model_class if loaded_model_class
|
674
|
+
end
|
675
|
+
|
676
|
+
# We cannot get loaded model when `model_path` is loaded by Rails
|
677
|
+
# auto_load/eager_load paths. Try all possible model paths one by one.
|
678
|
+
absolute_file = File.expand_path(file)
|
679
|
+
model_paths =
|
680
|
+
$LOAD_PATH.map(&:to_s)
|
681
|
+
.select { |path| absolute_file.include?(path) }
|
682
|
+
.map { |path| absolute_file.sub(path, '').sub(/\.rb$/, '').sub(%r{^/}, '') }
|
683
|
+
model_paths
|
684
|
+
.map { |path| get_loaded_model_by_path(path) }
|
685
|
+
.find { |loaded_model| !loaded_model.nil? }
|
686
|
+
end
|
687
|
+
|
688
|
+
# Retrieve loaded model class by path to the file where it's supposed to be defined.
|
689
|
+
def get_loaded_model_by_path(model_path)
|
690
|
+
ActiveSupport::Inflector.constantize(ActiveSupport::Inflector.camelize(model_path))
|
691
|
+
rescue StandardError, LoadError
|
692
|
+
# Revert to the old way but it is not really robust
|
693
|
+
ObjectSpace.each_object(::Class)
|
694
|
+
.select do |c|
|
695
|
+
c.is_a?(Class) && # NOTE: we use === to avoid a bug in activesupport 2.3.14 OptionMerger vs. is_a?
|
696
|
+
c.ancestors.respond_to?(:include?) && # to fix FactoryGirl bug, see https://github.com/ctran/annotate_models/pull/82
|
697
|
+
c.ancestors.include?(ActiveRecord::Base)
|
698
|
+
end.detect { |c| ActiveSupport::Inflector.underscore(c.to_s) == model_path }
|
699
|
+
end
|
700
|
+
|
701
|
+
def parse_options(options = {})
|
702
|
+
self.model_dir = split_model_dir(options[:model_dir]) if options[:model_dir]
|
703
|
+
self.root_dir = options[:root_dir] if options[:root_dir]
|
704
|
+
self.skip_subdirectory_model_load = options[:skip_subdirectory_model_load].present?
|
705
|
+
end
|
706
|
+
|
707
|
+
def split_model_dir(option_value)
|
708
|
+
option_value = option_value.split(',') unless option_value.is_a?(Array)
|
709
|
+
option_value.map(&:strip).reject(&:empty?)
|
710
|
+
end
|
711
|
+
|
712
|
+
# We're passed a name of things that might be
|
713
|
+
# ActiveRecord models. If we can find the class, and
|
714
|
+
# if its a subclass of ActiveRecord::Base,
|
715
|
+
# then pass it to the associated block
|
716
|
+
def do_annotations(options = {})
|
717
|
+
parse_options(options)
|
718
|
+
|
719
|
+
header = options[:format_markdown] ? PREFIX_MD.dup : PREFIX.dup
|
720
|
+
version = begin
|
721
|
+
ActiveRecord::Migrator.current_version
|
722
|
+
rescue StandardError
|
723
|
+
0
|
724
|
+
end
|
725
|
+
header << "\n# Schema version: #{version}" if options[:include_version] && version > 0
|
726
|
+
|
727
|
+
annotated = []
|
728
|
+
get_model_files(options).each do |path, filename|
|
729
|
+
annotate_model_file(annotated, File.join(path, filename), header, options)
|
730
|
+
end
|
731
|
+
|
732
|
+
if annotated.empty?
|
733
|
+
puts 'Model files unchanged.'
|
734
|
+
else
|
735
|
+
puts "Annotated (#{annotated.length}): #{annotated.join(', ')}"
|
736
|
+
end
|
737
|
+
end
|
738
|
+
|
739
|
+
def expand_glob_into_files(glob)
|
740
|
+
Dir.glob(glob)
|
741
|
+
end
|
742
|
+
|
743
|
+
def annotate_model_file(annotated, file, header, options)
|
744
|
+
return false if /#{SKIP_ANNOTATION_PREFIX}.*/ =~ (File.exist?(file) ? File.read(file) : '')
|
745
|
+
|
746
|
+
klass = get_model_class(file)
|
747
|
+
do_annotate = klass.is_a?(Class) &&
|
748
|
+
klass < ActiveRecord::Base &&
|
749
|
+
(!options[:exclude_sti_subclasses] || !(klass.superclass < ActiveRecord::Base && klass.table_name == klass.superclass.table_name)) &&
|
750
|
+
!klass.abstract_class? &&
|
751
|
+
klass.table_exists?
|
752
|
+
|
753
|
+
annotated.concat(annotate(klass, file, header, options)) if do_annotate
|
754
|
+
rescue BadModelFileError => e
|
755
|
+
unless options[:ignore_unknown_models]
|
756
|
+
warn "Unable to annotate #{file}: #{e.message}"
|
757
|
+
warn "\t" + e.backtrace.join("\n\t") if options[:trace]
|
758
|
+
end
|
759
|
+
rescue StandardError => e
|
760
|
+
warn "Unable to annotate #{file}: #{e.message}"
|
761
|
+
warn "\t" + e.backtrace.join("\n\t") if options[:trace]
|
762
|
+
end
|
763
|
+
|
764
|
+
def remove_annotations(options = {})
|
765
|
+
parse_options(options)
|
766
|
+
|
767
|
+
deannotated = []
|
768
|
+
deannotated_klass = false
|
769
|
+
get_model_files(options).each do |file|
|
770
|
+
file = File.join(file)
|
771
|
+
begin
|
772
|
+
klass = get_model_class(file)
|
773
|
+
if klass < ActiveRecord::Base && !klass.abstract_class?
|
774
|
+
model_name = klass.name.underscore
|
775
|
+
table_name = klass.table_name
|
776
|
+
model_file_name = file
|
777
|
+
deannotated_klass = true if remove_annotation_of_file(model_file_name, options)
|
778
|
+
|
779
|
+
get_patterns(options, matched_types(options))
|
780
|
+
.map { |f| resolve_filename(f, model_name, table_name) }
|
781
|
+
.each do |f|
|
782
|
+
if File.exist?(f)
|
783
|
+
remove_annotation_of_file(f, options)
|
784
|
+
deannotated_klass = true
|
785
|
+
end
|
786
|
+
end
|
787
|
+
end
|
788
|
+
deannotated << klass if deannotated_klass
|
789
|
+
rescue StandardError => e
|
790
|
+
warn "Unable to deannotate #{File.join(file)}: #{e.message}"
|
791
|
+
warn "\t" + e.backtrace.join("\n\t") if options[:trace]
|
792
|
+
end
|
793
|
+
end
|
794
|
+
puts "Removed annotations from: #{deannotated.join(', ')}"
|
795
|
+
end
|
796
|
+
|
797
|
+
def resolve_filename(filename_template, model_name, table_name)
|
798
|
+
filename_template
|
799
|
+
.gsub('%MODEL_NAME%', model_name)
|
800
|
+
.gsub('%PLURALIZED_MODEL_NAME%', model_name.pluralize)
|
801
|
+
.gsub('%TABLE_NAME%', table_name || model_name.pluralize)
|
802
|
+
end
|
803
|
+
|
804
|
+
def classified_sort(cols)
|
805
|
+
rest_cols = []
|
806
|
+
timestamps = []
|
807
|
+
associations = []
|
808
|
+
id = nil
|
809
|
+
|
810
|
+
cols.each do |c|
|
811
|
+
if c.name.eql?('id')
|
812
|
+
id = c
|
813
|
+
elsif c.name.eql?('created_at') || c.name.eql?('updated_at')
|
814
|
+
timestamps << c
|
815
|
+
elsif c.name[-3, 3].eql?('_id')
|
816
|
+
associations << c
|
817
|
+
else
|
818
|
+
rest_cols << c
|
819
|
+
end
|
820
|
+
end
|
821
|
+
[rest_cols, timestamps, associations].each { |a| a.sort_by!(&:name) }
|
822
|
+
|
823
|
+
([id] << rest_cols << timestamps << associations).flatten.compact
|
824
|
+
end
|
825
|
+
|
826
|
+
private
|
827
|
+
|
828
|
+
def with_comments?(klass, options)
|
829
|
+
options[:with_comment] &&
|
830
|
+
klass.columns.first.respond_to?(:comment) &&
|
831
|
+
klass.columns.any? { |col| !col.comment.nil? }
|
832
|
+
end
|
833
|
+
|
834
|
+
def with_comments_column?(klass, options)
|
835
|
+
options[:with_comment_column] &&
|
836
|
+
klass.columns.first.respond_to?(:comment) &&
|
837
|
+
klass.columns.any? { |col| !col.comment.nil? }
|
838
|
+
end
|
839
|
+
|
840
|
+
def max_schema_info_width(klass, options)
|
841
|
+
cols = columns(klass, options)
|
842
|
+
|
843
|
+
if with_comments?(klass, options)
|
844
|
+
max_size = cols.map do |column|
|
845
|
+
column.name.size + (column.comment ? width(column.comment) : 0)
|
846
|
+
end.max || 0
|
847
|
+
max_size += 2
|
848
|
+
else
|
849
|
+
max_size = cols.map(&:name).map(&:size).max
|
850
|
+
end
|
851
|
+
max_size += options[:format_rdoc] ? 5 : 1
|
852
|
+
|
853
|
+
max_size
|
854
|
+
end
|
855
|
+
|
856
|
+
# rubocop:disable Metrics/ParameterLists
|
857
|
+
def format_default(col_name, max_size, col_type, bare_type_allowance, simple_formatted_attrs,
|
858
|
+
bare_max_attrs_length = 0, col_comment = nil)
|
859
|
+
format(
|
860
|
+
'# %s:%s %s %s',
|
861
|
+
mb_chars_ljust(col_name, max_size),
|
862
|
+
mb_chars_ljust(col_type, bare_type_allowance),
|
863
|
+
mb_chars_ljust(simple_formatted_attrs, bare_max_attrs_length),
|
864
|
+
col_comment
|
865
|
+
).rstrip + "\n"
|
866
|
+
end
|
867
|
+
|
868
|
+
def width(string)
|
869
|
+
string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
|
870
|
+
end
|
871
|
+
|
872
|
+
def mb_chars_ljust(string, length)
|
873
|
+
string = string.to_s
|
874
|
+
padding = length - width(string)
|
875
|
+
if padding > 0
|
876
|
+
string + (' ' * padding)
|
877
|
+
else
|
878
|
+
string[0..length - 1]
|
879
|
+
end
|
880
|
+
end
|
881
|
+
|
882
|
+
def non_ascii_length(string)
|
883
|
+
string.to_s.chars.reject(&:ascii_only?).length
|
884
|
+
end
|
885
|
+
|
886
|
+
def map_col_type_to_ruby_classes(col_type)
|
887
|
+
case col_type
|
888
|
+
when 'integer' then Integer.to_s
|
889
|
+
when 'float' then Float.to_s
|
890
|
+
when 'decimal' then BigDecimal.to_s
|
891
|
+
when 'datetime', 'timestamp', 'time' then Time.to_s
|
892
|
+
when 'date' then Date.to_s
|
893
|
+
when 'text', 'string', 'binary', 'inet', 'uuid' then String.to_s
|
894
|
+
when 'json', 'jsonb' then Hash.to_s
|
895
|
+
when 'boolean' then 'Boolean'
|
896
|
+
end
|
897
|
+
end
|
898
|
+
|
899
|
+
def columns(klass, options)
|
900
|
+
cols = klass.columns
|
901
|
+
cols += translated_columns(klass)
|
902
|
+
|
903
|
+
if ignore_columns = options[:ignore_columns]
|
904
|
+
cols = cols.reject do |col|
|
905
|
+
col.name.match(/#{ignore_columns}/)
|
906
|
+
end
|
907
|
+
end
|
908
|
+
|
909
|
+
cols = cols.sort_by(&:name) if options[:sort]
|
910
|
+
cols = classified_sort(cols) if options[:classified_sort]
|
911
|
+
|
912
|
+
cols
|
913
|
+
end
|
914
|
+
|
915
|
+
##
|
916
|
+
# Add columns managed by the globalize gem if this gem is being used.
|
917
|
+
def translated_columns(klass)
|
918
|
+
return [] unless klass.respond_to? :translation_class
|
919
|
+
|
920
|
+
ignored_cols = ignored_translation_table_colums(klass)
|
921
|
+
klass.translation_class.columns.reject do |col|
|
922
|
+
ignored_cols.include? col.name.to_sym
|
923
|
+
end
|
924
|
+
end
|
925
|
+
|
926
|
+
##
|
927
|
+
# These are the columns that the globalize gem needs to work but
|
928
|
+
# are not necessary for the models to be displayed as annotations.
|
929
|
+
def ignored_translation_table_colums(klass)
|
930
|
+
# Construct the foreign column name in the translations table
|
931
|
+
# eg. Model: Car, foreign column name: car_id
|
932
|
+
foreign_column_name = [
|
933
|
+
klass.table_name.to_s.singularize,
|
934
|
+
'_id'
|
935
|
+
].join.to_sym
|
936
|
+
|
937
|
+
[
|
938
|
+
:id,
|
939
|
+
:created_at,
|
940
|
+
:updated_at,
|
941
|
+
:locale,
|
942
|
+
foreign_column_name
|
943
|
+
]
|
944
|
+
end
|
945
|
+
|
946
|
+
##
|
947
|
+
# Get the list of attributes that should be included in the annotation for
|
948
|
+
# a given column.
|
949
|
+
def get_attributes(column, column_type, klass, options)
|
950
|
+
attrs = []
|
951
|
+
attrs << "default(#{schema_default(klass, column)})" unless column.default.nil? || hide_default?(column_type,
|
952
|
+
options)
|
953
|
+
attrs << 'unsigned' if column.respond_to?(:unsigned?) && column.unsigned?
|
954
|
+
attrs << 'not null' unless column.null
|
955
|
+
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)
|
956
|
+
attrs << 'primary key'
|
957
|
+
end
|
958
|
+
|
959
|
+
if column_type == 'decimal'
|
960
|
+
column_type << "(#{column.precision}, #{column.scale})"
|
961
|
+
elsif !%w[spatial geometry geography].include?(column_type)
|
962
|
+
if column.limit && !options[:format_yard]
|
963
|
+
if column.limit.is_a? Array
|
964
|
+
attrs << "(#{column.limit.join(', ')})"
|
965
|
+
else
|
966
|
+
column_type << "(#{column.limit})" unless hide_limit?(column_type, options)
|
967
|
+
end
|
968
|
+
end
|
969
|
+
end
|
970
|
+
|
971
|
+
# Check out if we got an array column
|
972
|
+
attrs << 'is an Array' if column.respond_to?(:array) && column.array
|
973
|
+
|
974
|
+
# Check out if we got a geometric column
|
975
|
+
# and print the type and SRID
|
976
|
+
if column.respond_to?(:geometry_type)
|
977
|
+
attrs << [column.geometry_type, column.try(:srid)].compact.join(', ')
|
978
|
+
elsif column.respond_to?(:geometric_type) && column.geometric_type.present?
|
979
|
+
attrs << [column.geometric_type.to_s.downcase, column.try(:srid)].compact.join(', ')
|
980
|
+
end
|
981
|
+
|
982
|
+
# Check if the column has indices and print "indexed" if true
|
983
|
+
# If the index includes another column, print it too.
|
984
|
+
if options[:simple_indexes] && klass.table_exists? # Check out if this column is indexed
|
985
|
+
indices = retrieve_indexes_from_table(klass)
|
986
|
+
if indices = indices.select { |ind| ind.columns.include? column.name }
|
987
|
+
indices.sort_by(&:name).each do |ind|
|
988
|
+
next if ind.columns.is_a?(String)
|
989
|
+
|
990
|
+
ind = ind.columns.reject! { |i| i == column.name }
|
991
|
+
attrs << (ind.empty? ? 'indexed' : "indexed => [#{ind.join(', ')}]")
|
992
|
+
end
|
993
|
+
end
|
994
|
+
end
|
995
|
+
|
996
|
+
attrs
|
997
|
+
end
|
998
|
+
end
|
999
|
+
|
1000
|
+
class BadModelFileError < LoadError
|
1001
|
+
def to_s
|
1002
|
+
"file doesn't contain a valid model class"
|
1003
|
+
end
|
1004
|
+
end
|
1005
|
+
end
|