annotate 2.7.0 → 3.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/{AUTHORS.rdoc → AUTHORS.md} +3 -2
- data/CHANGELOG.md +326 -0
- data/README.md +331 -0
- data/RELEASE.md +19 -0
- data/annotate.gemspec +18 -28
- data/bin/annotate +14 -177
- data/lib/annotate.rb +58 -94
- data/lib/annotate/active_record_patch.rb +1 -1
- data/lib/annotate/annotate_models.rb +685 -325
- data/lib/annotate/annotate_routes.rb +150 -113
- data/lib/annotate/annotate_routes/helpers.rb +69 -0
- data/lib/annotate/constants.rb +38 -0
- data/lib/annotate/helpers.rb +30 -0
- data/lib/annotate/parser.rb +303 -0
- data/lib/annotate/version.rb +1 -1
- data/lib/generators/annotate/install_generator.rb +5 -4
- data/lib/generators/annotate/templates/auto_annotate_models.rake +46 -34
- data/lib/tasks/annotate_models.rake +47 -37
- data/lib/tasks/{migrate.rake → annotate_models_migrate.rake} +14 -16
- data/lib/tasks/annotate_routes.rake +5 -2
- data/potato.md +41 -0
- metadata +29 -19
- data/CHANGELOG.rdoc +0 -208
- data/README.rdoc +0 -255
- data/TODO.rdoc +0 -11
@@ -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 =
|
6
|
-
COMPAT_PREFIX_MD =
|
7
|
-
PREFIX =
|
8
|
-
PREFIX_MD =
|
9
|
-
END_MARK =
|
10
|
-
|
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(
|
17
|
-
MODEL_TEST_DIR = File.join(
|
18
|
-
SPEC_MODEL_DIR = File.join(
|
19
|
-
FIXTURE_TEST_DIR = File.join(
|
20
|
-
FIXTURE_SPEC_DIR = File.join(
|
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(
|
24
|
-
CONTROLLER_SPEC_DIR = File.join(
|
25
|
-
REQUEST_SPEC_DIR = File.join(
|
26
|
-
ROUTING_SPEC_DIR = File.join(
|
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(
|
30
|
-
EXEMPLARS_SPEC_DIR = File.join(
|
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(
|
34
|
-
BLUEPRINTS_SPEC_DIR = File.join(
|
38
|
+
BLUEPRINTS_TEST_DIR = File.join('test', "blueprints")
|
39
|
+
BLUEPRINTS_SPEC_DIR = File.join('spec', "blueprints")
|
35
40
|
|
36
|
-
# Factory
|
37
|
-
|
38
|
-
|
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(
|
42
|
-
FABRICATORS_SPEC_DIR = File.join(
|
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(
|
46
|
-
SERIALIZERS_TEST_DIR = File.join(
|
47
|
-
SERIALIZERS_SPEC_DIR = File.join(
|
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(
|
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(
|
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 =
|
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
|
61
|
-
|
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
|
65
|
-
@model_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.
|
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
|
73
|
-
|
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=
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
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
|
138
|
-
when TrueClass then
|
139
|
-
when FalseClass then
|
140
|
-
when Float,
|
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<<
|
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
|
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
|
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 =
|
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
|
192
|
-
|
251
|
+
col_type = get_col_type(col)
|
193
252
|
attrs = []
|
194
|
-
attrs << "default(#{schema_default(klass, col)})" unless col.default.nil? || col_type
|
195
|
-
attrs <<
|
196
|
-
attrs <<
|
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 ==
|
258
|
+
if col_type == 'decimal'
|
199
259
|
col_type << "(#{col.precision}, #{col.scale})"
|
200
|
-
elsif
|
201
|
-
if
|
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)
|
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 =
|
284
|
+
indices = retrieve_indexes_from_table(klass)
|
227
285
|
if indices = indices.select { |ind| ind.columns.include? col.name }
|
228
|
-
indices.sort_by
|
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.
|
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>", "*#{
|
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 -
|
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`",
|
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 <<
|
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
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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 =
|
271
|
-
return
|
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
|
275
|
-
if
|
276
|
-
|
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
|
-
|
382
|
+
col.to_s.gsub("\r", '\r').gsub("\n", '\n')
|
279
383
|
end
|
280
384
|
end
|
281
|
-
|
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
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
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
|
470
|
+
return '' if foreign_keys.empty?
|
306
471
|
|
307
|
-
|
308
|
-
|
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
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
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
|
-
|
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"),
|
321
|
-
# matches the block that is already there. If so, leave it be.
|
322
|
-
# info block and write a new one.
|
323
|
-
#
|
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
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
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
|
-
|
345
|
-
|
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
|
-
|
348
|
-
|
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
|
-
|
354
|
-
|
355
|
-
|
557
|
+
File.open(file_name, 'wb') { |f| f.puts new_content }
|
558
|
+
true
|
559
|
+
end
|
356
560
|
|
357
|
-
|
358
|
-
|
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
|
-
|
372
|
-
|
373
|
-
end
|
564
|
+
if magic_comments.any?
|
565
|
+
magic_comments.join
|
374
566
|
else
|
375
|
-
|
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
|
-
|
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,
|
579
|
+
File.open(file_name, 'wb') { |f| f.puts content }
|
386
580
|
|
387
|
-
|
581
|
+
true
|
388
582
|
else
|
389
|
-
|
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
|
-
|
628
|
+
annotated << model_file_name
|
424
629
|
end
|
425
630
|
|
426
|
-
|
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
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
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
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
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(:
|
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
|
-
|
456
|
-
if(!options[:is_rake])
|
457
|
-
models = ARGV.dup.reject{|m| m.match(/^(.*)=/)}
|
458
|
-
end
|
671
|
+
model_files = []
|
459
672
|
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
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
|
-
|
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)
|
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) &&
|
729
|
+
if File.file?(file_path) && Kernel.require(file_path)
|
496
730
|
retry
|
497
|
-
elsif model_path
|
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
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
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
|
-
|
783
|
+
def do_annotations(options = {})
|
784
|
+
parse_options(options)
|
528
785
|
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
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 |
|
541
|
-
annotate_model_file(annotated, File.join(
|
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
|
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
|
810
|
+
return false if /#{SKIP_ANNOTATION_PREFIX}.*/ =~ (File.exist?(file) ? File.read(file) : '')
|
553
811
|
klass = get_model_class(file)
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
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
|
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
|
-
|
572
|
-
|
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
|
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
|
595
|
-
rescue
|
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
|
-
|
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
|
617
|
-
if c.name.eql?(
|
876
|
+
cols.each do |c|
|
877
|
+
if c.name.eql?('id')
|
618
878
|
id = c
|
619
|
-
elsif
|
879
|
+
elsif c.name.eql?('created_at') || c.name.eql?('updated_at')
|
620
880
|
timestamps << c
|
621
|
-
elsif c.name[-3,3].eql?(
|
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
|
-
|
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
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
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
|
|