annotate 2.7.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
|