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