annotate 2.7.0 → 3.1.1

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.
@@ -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