annotate 2.7.0 → 3.1.1

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