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