annotate 2.6.3 → 3.2.0

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