annotate 2.6.3 → 3.2.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 +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
|