rails_lens 0.0.0 → 0.2.2

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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE.txt +2 -2
  4. data/README.md +463 -9
  5. data/exe/rails_lens +25 -0
  6. data/lib/rails_lens/analyzers/association_analyzer.rb +111 -0
  7. data/lib/rails_lens/analyzers/base.rb +35 -0
  8. data/lib/rails_lens/analyzers/best_practices_analyzer.rb +114 -0
  9. data/lib/rails_lens/analyzers/column_analyzer.rb +97 -0
  10. data/lib/rails_lens/analyzers/composite_keys.rb +62 -0
  11. data/lib/rails_lens/analyzers/database_constraints.rb +35 -0
  12. data/lib/rails_lens/analyzers/delegated_types.rb +129 -0
  13. data/lib/rails_lens/analyzers/enums.rb +34 -0
  14. data/lib/rails_lens/analyzers/error_handling.rb +66 -0
  15. data/lib/rails_lens/analyzers/foreign_key_analyzer.rb +47 -0
  16. data/lib/rails_lens/analyzers/generated_columns.rb +56 -0
  17. data/lib/rails_lens/analyzers/index_analyzer.rb +128 -0
  18. data/lib/rails_lens/analyzers/inheritance.rb +212 -0
  19. data/lib/rails_lens/analyzers/notes.rb +325 -0
  20. data/lib/rails_lens/analyzers/performance_analyzer.rb +110 -0
  21. data/lib/rails_lens/annotation_pipeline.rb +87 -0
  22. data/lib/rails_lens/cli.rb +176 -0
  23. data/lib/rails_lens/cli_error_handler.rb +86 -0
  24. data/lib/rails_lens/commands.rb +164 -0
  25. data/lib/rails_lens/connection.rb +133 -0
  26. data/lib/rails_lens/erd/column_type_formatter.rb +32 -0
  27. data/lib/rails_lens/erd/domain_color_mapper.rb +40 -0
  28. data/lib/rails_lens/erd/mysql_column_type_formatter.rb +19 -0
  29. data/lib/rails_lens/erd/postgresql_column_type_formatter.rb +19 -0
  30. data/lib/rails_lens/erd/visualizer.rb +329 -0
  31. data/lib/rails_lens/errors.rb +78 -0
  32. data/lib/rails_lens/extension_loader.rb +261 -0
  33. data/lib/rails_lens/extensions/base.rb +194 -0
  34. data/lib/rails_lens/extensions/closure_tree_ext.rb +157 -0
  35. data/lib/rails_lens/file_insertion_helper.rb +168 -0
  36. data/lib/rails_lens/mailer/annotator.rb +226 -0
  37. data/lib/rails_lens/mailer/extractor.rb +201 -0
  38. data/lib/rails_lens/model_detector.rb +252 -0
  39. data/lib/rails_lens/parsers/class_info.rb +46 -0
  40. data/lib/rails_lens/parsers/module_info.rb +33 -0
  41. data/lib/rails_lens/parsers/parser_result.rb +55 -0
  42. data/lib/rails_lens/parsers/prism_parser.rb +90 -0
  43. data/lib/rails_lens/parsers.rb +10 -0
  44. data/lib/rails_lens/providers/association_notes_provider.rb +11 -0
  45. data/lib/rails_lens/providers/base.rb +37 -0
  46. data/lib/rails_lens/providers/best_practices_notes_provider.rb +11 -0
  47. data/lib/rails_lens/providers/column_notes_provider.rb +11 -0
  48. data/lib/rails_lens/providers/composite_keys_provider.rb +11 -0
  49. data/lib/rails_lens/providers/database_constraints_provider.rb +11 -0
  50. data/lib/rails_lens/providers/delegated_types_provider.rb +11 -0
  51. data/lib/rails_lens/providers/enums_provider.rb +11 -0
  52. data/lib/rails_lens/providers/extension_notes_provider.rb +20 -0
  53. data/lib/rails_lens/providers/extensions_provider.rb +22 -0
  54. data/lib/rails_lens/providers/foreign_key_notes_provider.rb +11 -0
  55. data/lib/rails_lens/providers/generated_columns_provider.rb +11 -0
  56. data/lib/rails_lens/providers/index_notes_provider.rb +20 -0
  57. data/lib/rails_lens/providers/inheritance_provider.rb +23 -0
  58. data/lib/rails_lens/providers/notes_provider_base.rb +25 -0
  59. data/lib/rails_lens/providers/performance_notes_provider.rb +11 -0
  60. data/lib/rails_lens/providers/schema_provider.rb +61 -0
  61. data/lib/rails_lens/providers/section_provider_base.rb +28 -0
  62. data/lib/rails_lens/railtie.rb +17 -0
  63. data/lib/rails_lens/rake_bootstrapper.rb +18 -0
  64. data/lib/rails_lens/route/annotator.rb +268 -0
  65. data/lib/rails_lens/route/extractor.rb +133 -0
  66. data/lib/rails_lens/route/parser.rb +59 -0
  67. data/lib/rails_lens/schema/adapters/base.rb +345 -0
  68. data/lib/rails_lens/schema/adapters/database_info.rb +118 -0
  69. data/lib/rails_lens/schema/adapters/mysql.rb +279 -0
  70. data/lib/rails_lens/schema/adapters/postgresql.rb +197 -0
  71. data/lib/rails_lens/schema/adapters/sqlite3.rb +96 -0
  72. data/lib/rails_lens/schema/annotation.rb +144 -0
  73. data/lib/rails_lens/schema/annotation_manager.rb +202 -0
  74. data/lib/rails_lens/tasks/annotate.rake +35 -0
  75. data/lib/rails_lens/tasks/erd.rake +24 -0
  76. data/lib/rails_lens/tasks/mailers.rake +27 -0
  77. data/lib/rails_lens/tasks/routes.rake +27 -0
  78. data/lib/rails_lens/tasks/schema.rake +108 -0
  79. data/lib/rails_lens/version.rb +5 -0
  80. data/lib/rails_lens.rb +138 -5
  81. metadata +215 -11
@@ -0,0 +1,345 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ module Schema
5
+ module Adapters
6
+ class Base
7
+ attr_reader :connection, :table_name
8
+
9
+ def initialize(connection, table_name)
10
+ @connection = connection
11
+ @table_name = table_name
12
+ end
13
+
14
+ def generate_annotation(_model_class)
15
+ lines = []
16
+ lines << "table = \"#{table_name}\""
17
+ lines << "database_dialect = \"#{database_dialect}\""
18
+ lines << ''
19
+
20
+ add_columns_toml(lines)
21
+ add_indexes_toml(lines) if show_indexes?
22
+ add_foreign_keys_toml(lines) if show_foreign_keys?
23
+ add_check_constraints_toml(lines) if show_check_constraints?
24
+
25
+ lines.join("\n")
26
+ end
27
+
28
+ delegate :adapter_name, to: :connection
29
+
30
+ protected
31
+
32
+ def database_dialect
33
+ RailsLens::Connection.database_dialect(connection)
34
+ end
35
+
36
+ def add_columns(lines)
37
+ lines << 'Columns:'
38
+ columns.each do |column|
39
+ lines << format_column(column)
40
+ end
41
+ end
42
+
43
+ def add_indexes(lines)
44
+ indexes = fetch_indexes
45
+ return if indexes.empty?
46
+
47
+ lines << '' unless lines.last && lines.last.empty?
48
+ lines << 'Indexes:'
49
+ indexes.each do |index|
50
+ lines << format_index(index)
51
+ end
52
+ end
53
+
54
+ def add_foreign_keys(lines)
55
+ foreign_keys = fetch_foreign_keys
56
+ return if foreign_keys.empty?
57
+
58
+ lines << '' unless lines.last && lines.last.empty?
59
+ lines << 'Foreign Keys:'
60
+ foreign_keys.each do |fk|
61
+ lines << format_foreign_key(fk)
62
+ end
63
+ end
64
+
65
+ def add_check_constraints(lines)
66
+ constraints = fetch_check_constraints
67
+ return if constraints.empty?
68
+
69
+ lines << '' unless lines.last && lines.last.empty?
70
+ lines << 'Check Constraints:'
71
+ constraints.each do |constraint|
72
+ lines << format_check_constraint(constraint)
73
+ end
74
+ end
75
+
76
+ def columns
77
+ @columns ||= connection.columns(table_name)
78
+ end
79
+
80
+ def fetch_indexes
81
+ connection.indexes(table_name)
82
+ end
83
+
84
+ def fetch_foreign_keys
85
+ if connection.supports_foreign_keys?
86
+ connection.foreign_keys(table_name)
87
+ else
88
+ []
89
+ end
90
+ end
91
+
92
+ def fetch_check_constraints
93
+ # Override in database-specific adapters
94
+ []
95
+ end
96
+
97
+ def format_column(column)
98
+ parts = []
99
+ parts << column.name.ljust(column_name_width)
100
+ parts << ":#{column.type.to_s.ljust(12)}"
101
+
102
+ attributes = []
103
+ attributes << 'not null' unless column.null
104
+ attributes << 'primary key' if primary_key?(column)
105
+ attributes << "default: #{format_default(column.default)}" if column.default && show_defaults?
106
+
107
+ parts << attributes.join(', ') unless attributes.empty?
108
+
109
+ " #{parts.join(' ')}"
110
+ end
111
+
112
+ def format_index(index)
113
+ unique = index.unique ? ' UNIQUE' : ''
114
+ columns = Array(index.columns).join(', ')
115
+ " #{index.name} (#{columns})#{unique}"
116
+ end
117
+
118
+ def format_foreign_key(fk)
119
+ " #{fk.name} (#{fk.column} => #{fk.to_table}.#{fk.primary_key})"
120
+ end
121
+
122
+ def format_check_constraint(constraint)
123
+ " #{constraint[:name]}: #{constraint[:expression]}"
124
+ end
125
+
126
+ def format_default(default)
127
+ case default
128
+ when String
129
+ %("#{default}")
130
+ when NilClass
131
+ 'nil'
132
+ else
133
+ default.inspect
134
+ end
135
+ end
136
+
137
+ def primary_key?(column)
138
+ column.name == primary_key_name
139
+ end
140
+
141
+ def primary_key_name
142
+ @primary_key_name ||= connection.primary_key(table_name)
143
+ end
144
+
145
+ def column_name_width
146
+ @column_name_width ||= columns.map { |c| c.name.length }.max || 0
147
+ end
148
+
149
+ def show_indexes?
150
+ RailsLens.config.schema[:format_options][:show_indexes]
151
+ end
152
+
153
+ def show_foreign_keys?
154
+ RailsLens.config.schema[:format_options][:show_foreign_keys] &&
155
+ connection.supports_foreign_keys?
156
+ end
157
+
158
+ def show_check_constraints?
159
+ RailsLens.config.schema[:format_options][:show_check_constraints] &&
160
+ connection.supports_check_constraints?
161
+ end
162
+
163
+ def show_defaults?
164
+ RailsLens.config.schema[:format_options][:show_defaults]
165
+ end
166
+
167
+ def show_comments?
168
+ RailsLens.config.schema[:format_options][:show_comments] &&
169
+ connection.supports_comments?
170
+ end
171
+
172
+ # Structured formatting methods
173
+ def add_columns_structured(lines)
174
+ lines << 'COLUMNS:'
175
+ columns.each do |column|
176
+ lines << format_column_structured(column)
177
+ end
178
+ end
179
+
180
+ def add_indexes_structured(lines)
181
+ indexes = fetch_indexes
182
+ return if indexes.empty?
183
+
184
+ lines << 'INDEXES:'
185
+ indexes.each do |index|
186
+ lines << format_index_structured(index)
187
+ end
188
+ end
189
+
190
+ def add_foreign_keys_structured(lines)
191
+ foreign_keys = fetch_foreign_keys
192
+ return if foreign_keys.empty?
193
+
194
+ lines << 'FOREIGN_KEYS:'
195
+ foreign_keys.each do |fk|
196
+ lines << format_foreign_key_structured(fk)
197
+ end
198
+ end
199
+
200
+ def add_check_constraints_structured(lines)
201
+ constraints = fetch_check_constraints
202
+ return if constraints.empty?
203
+
204
+ lines << 'CHECK_CONSTRAINTS:'
205
+ constraints.each do |constraint|
206
+ lines << format_check_constraint_structured(constraint)
207
+ end
208
+ end
209
+
210
+ def format_column_structured(column)
211
+ attributes = []
212
+ attributes << column.type.to_s
213
+ attributes << 'primary_key' if column.name == 'id' || (column.name.end_with?('_id') && column.type == :bigint)
214
+ attributes << (column.null ? 'nullable' : 'not_null')
215
+ attributes << "default: #{column.default}" if column.default && show_defaults?
216
+
217
+ " #{column.name}: #{attributes.join(', ')}"
218
+ end
219
+
220
+ def format_index_structured(index)
221
+ attributes = []
222
+ attributes << "columns: [#{index.columns.join(', ')}]"
223
+ attributes << 'unique' if index.unique
224
+ attributes << "type: #{index.type}" if index.respond_to?(:type) && index.type
225
+
226
+ " #{index.name}: #{attributes.join(', ')}"
227
+ end
228
+
229
+ def format_foreign_key_structured(fk)
230
+ " #{fk.column}: references #{fk.to_table}(#{fk.primary_key})"
231
+ end
232
+
233
+ def format_check_constraint_structured(constraint)
234
+ if constraint.is_a?(Hash)
235
+ " #{constraint[:name]}: #{constraint[:expression]}"
236
+ else
237
+ " #{constraint.name}: #{constraint.expression}"
238
+ end
239
+ end
240
+
241
+ # TOML formatting methods
242
+ def add_columns_toml(lines)
243
+ lines << 'columns = ['
244
+ columns.each_with_index do |column, index|
245
+ line = ' { '
246
+ attrs = []
247
+ attrs << "name = \"#{column.name}\""
248
+ attrs << "type = \"#{column.type}\""
249
+ attrs << 'primary_key = true' if primary_key?(column)
250
+ attrs << "nullable = #{column.null}"
251
+ attrs << "default = #{format_toml_value(column.default)}" if column.default && show_defaults?
252
+ line += attrs.join(', ')
253
+ line += ' }'
254
+ line += ',' if index < columns.length - 1
255
+ lines << line
256
+ end
257
+ lines << ']'
258
+ end
259
+
260
+ def add_indexes_toml(lines)
261
+ indexes = fetch_indexes
262
+ return if indexes.empty?
263
+
264
+ lines << ''
265
+ lines << 'indexes = ['
266
+ indexes.each_with_index do |index, i|
267
+ line = ' { '
268
+ attrs = []
269
+ attrs << "name = \"#{index.name}\""
270
+ attrs << "columns = [#{index.columns.map { |c| "\"#{c}\"" }.join(', ')}]"
271
+ attrs << 'unique = true' if index.unique
272
+ attrs << "type = \"#{index.type}\"" if index.respond_to?(:type) && index.type
273
+ line += attrs.join(', ')
274
+ line += ' }'
275
+ line += ',' if i < indexes.length - 1
276
+ lines << line
277
+ end
278
+ lines << ']'
279
+ end
280
+
281
+ def add_foreign_keys_toml(lines)
282
+ foreign_keys = fetch_foreign_keys
283
+ return if foreign_keys.empty?
284
+
285
+ lines << ''
286
+ lines << 'foreign_keys = ['
287
+ foreign_keys.each_with_index do |fk, i|
288
+ line = ' { '
289
+ attrs = []
290
+ attrs << "column = \"#{fk.column}\""
291
+ attrs << "references_table = \"#{fk.to_table}\""
292
+ attrs << "references_column = \"#{fk.primary_key}\""
293
+ attrs << "name = \"#{fk.name}\"" if fk.respond_to?(:name) && fk.name
294
+ attrs << "on_delete = \"#{fk.on_delete}\"" if fk.respond_to?(:on_delete) && fk.on_delete
295
+ attrs << "on_update = \"#{fk.on_update}\"" if fk.respond_to?(:on_update) && fk.on_update
296
+ line += attrs.join(', ')
297
+ line += ' }'
298
+ line += ',' if i < foreign_keys.length - 1
299
+ lines << line
300
+ end
301
+ lines << ']'
302
+ end
303
+
304
+ def add_check_constraints_toml(lines)
305
+ constraints = fetch_check_constraints
306
+ return if constraints.empty?
307
+
308
+ lines << ''
309
+ lines << 'check_constraints = ['
310
+ constraints.each_with_index do |constraint, i|
311
+ line = ' { '
312
+ attrs = []
313
+ if constraint.is_a?(Hash)
314
+ attrs << "name = \"#{constraint[:name]}\""
315
+ attrs << "expression = \"#{constraint[:expression]}\""
316
+ else
317
+ attrs << "name = \"#{constraint.name}\""
318
+ attrs << "expression = \"#{constraint.expression}\""
319
+ end
320
+ line += attrs.join(', ')
321
+ line += ' }'
322
+ line += ',' if i < constraints.length - 1
323
+ lines << line
324
+ end
325
+ lines << ']'
326
+ end
327
+
328
+ def format_toml_value(value)
329
+ case value
330
+ when String
331
+ "\"#{value.gsub('"', '\"')}\""
332
+ when NilClass
333
+ 'null'
334
+ when TrueClass, FalseClass
335
+ value.to_s
336
+ when Numeric
337
+ value.to_s
338
+ else
339
+ "\"#{value}\""
340
+ end
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ module Schema
5
+ module Adapters
6
+ class DatabaseInfo
7
+ attr_reader :connection, :adapter_name
8
+
9
+ def initialize(connection)
10
+ @connection = connection
11
+ @adapter_name = connection.adapter_name
12
+ end
13
+
14
+ def generate_annotation
15
+ lines = []
16
+ lines << '== Database Information'
17
+ lines << "Adapter: #{adapter_name}"
18
+ lines << "Database: #{database_name}"
19
+ lines << "Version: #{database_version}"
20
+ lines << "Encoding: #{database_encoding}" if respond_to?(:database_encoding)
21
+ lines << "Collation: #{database_collation}" if respond_to?(:database_collation)
22
+ lines << ''
23
+
24
+ # Add extensions for PostgreSQL
25
+ if adapter_name == 'PostgreSQL' && extensions.any?
26
+ lines << 'Enabled Extensions:'
27
+ extensions.each do |ext|
28
+ lines << " - #{ext['name']} (#{ext['version']})"
29
+ end
30
+ lines << ''
31
+ end
32
+
33
+ # Add schemas for PostgreSQL
34
+ if adapter_name == 'PostgreSQL' && schemas.any?
35
+ lines << 'Database Schemas:'
36
+ schemas.each do |schema|
37
+ lines << " - #{schema}"
38
+ end
39
+ lines << ''
40
+ end
41
+
42
+ lines.join("\n")
43
+ end
44
+
45
+ private
46
+
47
+ def database_name
48
+ connection.current_database
49
+ rescue StandardError
50
+ 'N/A'
51
+ end
52
+
53
+ def database_version
54
+ case adapter_name
55
+ when 'PostgreSQL'
56
+ connection.select_value('SELECT version()').split[1]
57
+ when 'Mysql2'
58
+ connection.select_value('SELECT VERSION()')
59
+ when 'SQLite'
60
+ connection.select_value('SELECT sqlite_version()')
61
+ else
62
+ 'Unknown'
63
+ end
64
+ rescue StandardError
65
+ 'Unknown'
66
+ end
67
+
68
+ def database_encoding
69
+ case adapter_name
70
+ when 'PostgreSQL'
71
+ connection.select_value('SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()')
72
+ when 'Mysql2'
73
+ connection.select_value('SELECT DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = DATABASE()')
74
+ end
75
+ rescue StandardError
76
+ nil
77
+ end
78
+
79
+ def database_collation
80
+ case adapter_name
81
+ when 'PostgreSQL'
82
+ connection.select_value('SELECT datcollate FROM pg_database WHERE datname = current_database()')
83
+ when 'Mysql2'
84
+ connection.select_value('SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = DATABASE()')
85
+ end
86
+ rescue StandardError
87
+ nil
88
+ end
89
+
90
+ def extensions
91
+ return [] unless adapter_name == 'PostgreSQL'
92
+
93
+ connection.select_all(<<-SQL.squish).to_a
94
+ SELECT extname as name, extversion as version
95
+ FROM pg_extension
96
+ WHERE extname NOT IN ('plpgsql')
97
+ ORDER BY extname
98
+ SQL
99
+ rescue StandardError
100
+ []
101
+ end
102
+
103
+ def schemas
104
+ return [] unless adapter_name == 'PostgreSQL'
105
+
106
+ connection.select_values(<<-SQL.squish)
107
+ SELECT schema_name#{' '}
108
+ FROM information_schema.schemata#{' '}
109
+ WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
110
+ ORDER BY schema_name
111
+ SQL
112
+ rescue StandardError
113
+ []
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end