rails_lens 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +70 -0
- data/lib/rails_lens/analyzers/delegated_types.rb +6 -1
- data/lib/rails_lens/cli.rb +6 -0
- data/lib/rails_lens/erd/visualizer.rb +39 -11
- data/lib/rails_lens/model_detector.rb +16 -5
- data/lib/rails_lens/providers/schema_provider.rb +21 -8
- data/lib/rails_lens/schema/adapters/mysql.rb +6 -6
- data/lib/rails_lens/schema/annotation_manager.rb +2 -2
- data/lib/rails_lens/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 41ebdd3c52cca1c77790aa896dcc5962acb92c44eec90a2dceeda98f310aefb7
|
4
|
+
data.tar.gz: 72e49695a796e9d030fd80aedab53d7dea055db82c98e8023a6a2bb4293e139f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d0e890e0ae21326d6176542668277a1c8e5d6336f013dec8cc8854f892c0136c6a40fb4557789dc661c47de70fbf82c0e59dcc75769b4a41e613f681e75f9a9f
|
7
|
+
data.tar.gz: d5c0d6aaecbd3a46986cc86507b3550347bd09e69f4126721fdb9acec05110a2f622fa2555d8c37151144efdd89ef5b87129576d3bc66c02f2b8cdd3404fcb77
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [0.2.2](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.1...rails_lens/v0.2.2) (2025-07-31)
|
4
|
+
|
5
|
+
|
6
|
+
### Bug Fixes
|
7
|
+
|
8
|
+
* remove from format from the erd visualize ([#5](https://github.com/seuros/rails_lens/issues/5)) ([c2efdc7](https://github.com/seuros/rails_lens/commit/c2efdc7011425fcd8b46dce54d811ce166b0c660))
|
9
|
+
|
10
|
+
## [0.2.1](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.0...rails_lens/v0.2.1) (2025-07-30)
|
11
|
+
|
12
|
+
|
13
|
+
### Bug Fixes
|
14
|
+
|
15
|
+
* resolve GitHub issue [#2](https://github.com/seuros/rails_lens/issues/2) CLI and provider errors ([6d92c67](https://github.com/seuros/rails_lens/commit/6d92c679f1da9186ec4f357c243b41bc57eecd94))
|
16
|
+
* resolve GitHub issue [#2](https://github.com/seuros/rails_lens/issues/2) CLI and provider errors ([a583373](https://github.com/seuros/rails_lens/commit/a583373b40ee7fdde32b3e97295448b1ecaa7ca5))
|
17
|
+
|
3
18
|
## [0.2.0](https://github.com/seuros/rails_lens/compare/rails_lens-v0.1.0...rails_lens/v0.2.0) (2025-07-30)
|
4
19
|
|
5
20
|
|
data/README.md
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
# Rails Lens 🔍
|
2
2
|
|
3
|
+

|
4
|
+

|
5
|
+

|
6
|
+

|
7
|
+
|
3
8
|
> **Precision optics for the Rails universe** - Where every model has perfect clarity through spacetime
|
4
9
|
|
5
10
|
Rails Lens provides intelligent model annotations and ERD generation for Rails applications with database-specific adapters, multi-database support, and advanced code analysis.
|
@@ -26,6 +31,71 @@ Rails Lens provides intelligent model annotations and ERD generation for Rails a
|
|
26
31
|
**✨ Advanced Features:**
|
27
32
|
- STI hierarchy mapping • Delegated types • Polymorphic associations • Enum analysis • LRDL-optimized output
|
28
33
|
|
34
|
+
## Showcase: Real-World Example
|
35
|
+
|
36
|
+
Rescued from AWS limbo, Rails Lens delivers cosmic schema clarity. See this `Announcement` model:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
# frozen_string_literal: true
|
40
|
+
|
41
|
+
# <rails-lens:schema:begin>
|
42
|
+
# table = "announcements"
|
43
|
+
# database_dialect = "PostgreSQL"
|
44
|
+
#
|
45
|
+
# columns = [
|
46
|
+
# { name = "id", type = "integer", primary_key = true, nullable = false },
|
47
|
+
# { name = "body", type = "text", nullable = true },
|
48
|
+
# { name = "audience", type = "string", nullable = true },
|
49
|
+
# { name = "scheduled_at", type = "datetime", nullable = true },
|
50
|
+
# { name = "created_at", type = "datetime", nullable = false },
|
51
|
+
# { name = "updated_at", type = "datetime", nullable = false }
|
52
|
+
# ]
|
53
|
+
#
|
54
|
+
# == Polymorphic Associations
|
55
|
+
# Polymorphic Targets:
|
56
|
+
# - entry (as: :entryable)
|
57
|
+
#
|
58
|
+
# == Enums
|
59
|
+
# - audience: { all_users: "all_users", crew_only: "crew_only", officers_only: "officers_only", command_staff: "command_staff" } (string)
|
60
|
+
#
|
61
|
+
# == Notes
|
62
|
+
# - Column 'body' should probably have NOT NULL constraint
|
63
|
+
# - Column 'audience' should probably have NOT NULL constraint
|
64
|
+
# - String column 'audience' has no length limit - consider adding one
|
65
|
+
# - Large text column 'body' is frequently queried - consider separate storage
|
66
|
+
# <rails-lens:schema:end>
|
67
|
+
class Announcement < ApplicationRecord
|
68
|
+
enum :audience, { all_users: 'all_users', crew_only: 'crew_only', officers_only: 'officers_only', command_staff: 'command_staff' }, suffix: true
|
69
|
+
has_one :entry, as: :entryable, dependent: :destroy
|
70
|
+
validates :audience, presence: true
|
71
|
+
validates :body, presence: true
|
72
|
+
scope :recent, -> { order(created_at: :desc) }
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
**ERD Visualization:**
|
77
|
+
```mermaid
|
78
|
+
erDiagram
|
79
|
+
Announcement ||--o{ Entry : entryable
|
80
|
+
Announcement {
|
81
|
+
integer id PK
|
82
|
+
text body
|
83
|
+
string audience
|
84
|
+
datetime scheduled_at
|
85
|
+
datetime created_at
|
86
|
+
datetime updated_at
|
87
|
+
}
|
88
|
+
Entry {
|
89
|
+
integer id PK
|
90
|
+
string entryable_type
|
91
|
+
integer entryable_id
|
92
|
+
datetime created_at
|
93
|
+
datetime updated_at
|
94
|
+
}
|
95
|
+
```
|
96
|
+
|
97
|
+
**No grepping, no LLM hallucinations. Try it:** `gem install rails_lens`
|
98
|
+
|
29
99
|
## Requirements
|
30
100
|
|
31
101
|
- Ruby >= 3.4.0
|
@@ -17,7 +17,12 @@ module RailsLens
|
|
17
17
|
|
18
18
|
lines << "Type Column: #{delegated_type_info[:type_column]}"
|
19
19
|
lines << "ID Column: #{delegated_type_info[:id_column]}"
|
20
|
-
|
20
|
+
types_list = if delegated_type_info[:types].respond_to?(:keys)
|
21
|
+
delegated_type_info[:types].keys
|
22
|
+
else
|
23
|
+
Array(delegated_type_info[:types])
|
24
|
+
end
|
25
|
+
lines << "Types: #{types_list.join(', ')}"
|
21
26
|
|
22
27
|
lines.join("\n")
|
23
28
|
end
|
data/lib/rails_lens/cli.rb
CHANGED
@@ -8,6 +8,11 @@ module RailsLens
|
|
8
8
|
class CLI < Thor
|
9
9
|
include CLIErrorHandler
|
10
10
|
|
11
|
+
# Thor configuration: exit with proper status codes on failure (modern behavior)
|
12
|
+
def self.exit_on_failure?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
11
16
|
class_option :config, type: :string, default: '.rails-lens.yml', desc: 'Configuration file path'
|
12
17
|
class_option :dry_run, type: :boolean, desc: 'Show what would be done without making changes'
|
13
18
|
class_option :verbose, type: :boolean, desc: 'Verbose output'
|
@@ -15,6 +20,7 @@ module RailsLens
|
|
15
20
|
|
16
21
|
desc 'annotate', 'Annotate Rails models with schema information'
|
17
22
|
option :models, type: :array, desc: 'Specific models to annotate'
|
23
|
+
option :include_abstract, type: :boolean, desc: 'Include abstract classes'
|
18
24
|
option :position, type: :string, enum: %w[before after top bottom], desc: 'Annotation position'
|
19
25
|
option :routes, type: :boolean, desc: 'Annotate controller routes'
|
20
26
|
option :mailers, type: :boolean, desc: 'Annotate mailer methods'
|
@@ -63,9 +63,19 @@ module RailsLens
|
|
63
63
|
end
|
64
64
|
|
65
65
|
group_models.each do |model|
|
66
|
+
# Additional safety check: Skip abstract models that might have slipped through
|
67
|
+
next if model.abstract_class?
|
68
|
+
|
69
|
+
# Skip models without valid tables or columns
|
70
|
+
next unless model.table_exists? && model.columns.present?
|
71
|
+
|
66
72
|
model_display_name = format_model_name(model)
|
73
|
+
|
67
74
|
output << " #{model_display_name} {"
|
75
|
+
# Track opening brace position for error recovery
|
76
|
+
brace_position = output.size
|
68
77
|
|
78
|
+
columns_added = false
|
69
79
|
model.columns.each do |column|
|
70
80
|
type_str = format_column_type(column)
|
71
81
|
name_str = column.name
|
@@ -73,23 +83,35 @@ module RailsLens
|
|
73
83
|
key_str = keys.map(&:to_s).join(' ')
|
74
84
|
|
75
85
|
output << " #{type_str} #{name_str}#{" #{key_str}" unless key_str.empty?}"
|
86
|
+
columns_added = true
|
76
87
|
end
|
77
88
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
89
|
+
# Only close the entity if we successfully added columns
|
90
|
+
if columns_added
|
91
|
+
output << ' }'
|
92
|
+
output << ''
|
93
|
+
Rails.logger.debug { "Added entity: #{model_display_name}" } if options[:verbose]
|
94
|
+
else
|
95
|
+
# Remove the opening brace if no columns were added
|
96
|
+
output.slice!(brace_position..-1)
|
97
|
+
Rails.logger.debug { "Skipped entity #{model_display_name}: no columns found" } if options[:verbose]
|
98
|
+
end
|
82
99
|
rescue StandardError => e
|
83
100
|
Rails.logger.debug { "Warning: Could not add entity #{model.name}: #{e.message}" }
|
84
|
-
#
|
85
|
-
|
86
|
-
|
101
|
+
# Remove any partial entity content added since the opening brace
|
102
|
+
if output.size > brace_position
|
103
|
+
output.slice!(brace_position..-1)
|
104
|
+
end
|
87
105
|
end
|
88
106
|
end
|
89
107
|
|
90
108
|
# Add relationships
|
91
109
|
output << ' %% Relationships'
|
92
110
|
models.each do |model|
|
111
|
+
# Skip abstract models in relationship generation too
|
112
|
+
next if model.abstract_class?
|
113
|
+
next unless model.table_exists? && model.columns.present?
|
114
|
+
|
93
115
|
add_model_relationships(output, model, models)
|
94
116
|
end
|
95
117
|
|
@@ -152,6 +174,10 @@ module RailsLens
|
|
152
174
|
|
153
175
|
next unless target_model && models.include?(target_model)
|
154
176
|
|
177
|
+
# Skip relationships to abstract models
|
178
|
+
next if target_model.abstract_class?
|
179
|
+
next unless target_model.table_exists? && target_model.columns.present?
|
180
|
+
|
155
181
|
case association.macro
|
156
182
|
when :belongs_to
|
157
183
|
add_belongs_to_relationship(output, model, association, target_model)
|
@@ -164,10 +190,12 @@ module RailsLens
|
|
164
190
|
end
|
165
191
|
end
|
166
192
|
|
167
|
-
# Check for closure_tree self-reference
|
168
|
-
|
169
|
-
|
170
|
-
|
193
|
+
# Check for closure_tree self-reference - but only if model is not abstract
|
194
|
+
# rubocop:disable Style/GuardClause
|
195
|
+
if model.respond_to?(:_ct) && !model.abstract_class?
|
196
|
+
output << " #{format_model_name(model)} }o--o{ #{format_model_name(model)} : \"closure_tree\""
|
197
|
+
end
|
198
|
+
# rubocop:enable Style/GuardClause
|
171
199
|
end
|
172
200
|
|
173
201
|
def add_belongs_to_relationship(output, model, association, target_model)
|
@@ -125,7 +125,7 @@ module RailsLens
|
|
125
125
|
|
126
126
|
# Exclude abstract models and models without valid tables
|
127
127
|
before_count = models.size
|
128
|
-
models = filter_models_concurrently(models, trace_filtering)
|
128
|
+
models = filter_models_concurrently(models, trace_filtering, options)
|
129
129
|
log_filter_step('Abstract/invalid table removal', before_count, models.size, trace_filtering)
|
130
130
|
|
131
131
|
# Exclude tables from configuration
|
@@ -183,7 +183,7 @@ module RailsLens
|
|
183
183
|
end
|
184
184
|
end
|
185
185
|
|
186
|
-
def filter_models_concurrently(models, trace_filtering)
|
186
|
+
def filter_models_concurrently(models, trace_filtering, options = {})
|
187
187
|
# Use concurrent futures to check table existence in parallel
|
188
188
|
futures = models.map do |model|
|
189
189
|
Concurrent::Future.execute do
|
@@ -191,10 +191,13 @@ module RailsLens
|
|
191
191
|
reason = nil
|
192
192
|
|
193
193
|
begin
|
194
|
-
# Skip abstract models
|
195
|
-
if model.abstract_class?
|
194
|
+
# Skip abstract models unless explicitly included
|
195
|
+
if model.abstract_class? && !options[:include_abstract]
|
196
196
|
should_exclude = true
|
197
197
|
reason = 'abstract class'
|
198
|
+
# For abstract models that are included, skip table checks
|
199
|
+
elsif model.abstract_class? && options[:include_abstract]
|
200
|
+
reason = 'abstract class (included)'
|
198
201
|
# Skip models without configured tables
|
199
202
|
elsif !model.table_name
|
200
203
|
should_exclude = true
|
@@ -203,8 +206,12 @@ module RailsLens
|
|
203
206
|
elsif !model.table_exists?
|
204
207
|
should_exclude = true
|
205
208
|
reason = "table '#{model.table_name}' does not exist"
|
209
|
+
# Additional check: Skip models that don't have any columns
|
210
|
+
elsif model.columns.empty?
|
211
|
+
should_exclude = true
|
212
|
+
reason = "table '#{model.table_name}' has no columns"
|
206
213
|
else
|
207
|
-
reason = "table '#{model.table_name}' exists"
|
214
|
+
reason = "table '#{model.table_name}' exists with #{model.columns.size} columns"
|
208
215
|
end
|
209
216
|
rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotDefined => e
|
210
217
|
should_exclude = true
|
@@ -212,6 +219,10 @@ module RailsLens
|
|
212
219
|
rescue NameError, NoMethodError => e
|
213
220
|
should_exclude = true
|
214
221
|
reason = "method error checking model - #{e.message}"
|
222
|
+
rescue StandardError => e
|
223
|
+
# Catch any other errors and exclude the model to prevent ERD corruption
|
224
|
+
should_exclude = true
|
225
|
+
reason = "unexpected error checking model - #{e.message}"
|
215
226
|
end
|
216
227
|
|
217
228
|
if trace_filtering
|
@@ -18,18 +18,31 @@ module RailsLens
|
|
18
18
|
adapter_name = connection.adapter_name
|
19
19
|
|
20
20
|
lines = []
|
21
|
+
|
22
|
+
# Get connection name
|
23
|
+
begin
|
24
|
+
connection_name = connection.pool.db_config.name
|
25
|
+
lines << "connection = \"#{connection_name}\""
|
26
|
+
rescue StandardError
|
27
|
+
lines << 'connection = "unknown"'
|
28
|
+
end
|
29
|
+
|
21
30
|
lines << "database_dialect = \"#{adapter_name}\""
|
22
31
|
|
23
|
-
# Add
|
32
|
+
# Add database version information
|
33
|
+
begin
|
34
|
+
db_version = connection.database_version
|
35
|
+
lines << "database_version = \"#{db_version}\""
|
36
|
+
rescue StandardError
|
37
|
+
lines << 'database_version = "unknown"'
|
38
|
+
end
|
39
|
+
|
40
|
+
# Add database name if available
|
24
41
|
begin
|
25
|
-
db_name =
|
26
|
-
|
27
|
-
rescue StandardError
|
28
|
-
'unknown'
|
29
|
-
end
|
30
|
-
lines << "database_version = \"#{db_name}\""
|
42
|
+
db_name = connection.current_database
|
43
|
+
lines << "database_name = \"#{db_name}\"" if db_name
|
31
44
|
rescue StandardError
|
32
|
-
# Skip if can't get
|
45
|
+
# Skip if can't get database name
|
33
46
|
end
|
34
47
|
|
35
48
|
lines << ''
|
@@ -117,7 +117,7 @@ module RailsLens
|
|
117
117
|
rescue ActiveRecord::StatementInvalid => e
|
118
118
|
Rails.logger.debug { "Failed to fetch storage engine for #{table_name}: #{e.message}" }
|
119
119
|
nil
|
120
|
-
rescue
|
120
|
+
rescue => e
|
121
121
|
Rails.logger.debug { "MySQL error fetching storage engine: #{e.message}" }
|
122
122
|
nil
|
123
123
|
end
|
@@ -137,7 +137,7 @@ module RailsLens
|
|
137
137
|
rescue ActiveRecord::StatementInvalid => e
|
138
138
|
Rails.logger.debug { "Failed to fetch charset for #{table_name}: #{e.message}" }
|
139
139
|
nil
|
140
|
-
rescue
|
140
|
+
rescue => e
|
141
141
|
Rails.logger.debug { "MySQL error fetching charset: #{e.message}" }
|
142
142
|
nil
|
143
143
|
end
|
@@ -155,7 +155,7 @@ module RailsLens
|
|
155
155
|
rescue ActiveRecord::StatementInvalid => e
|
156
156
|
Rails.logger.debug { "Failed to fetch collation for #{table_name}: #{e.message}" }
|
157
157
|
nil
|
158
|
-
rescue
|
158
|
+
rescue => e
|
159
159
|
Rails.logger.debug { "MySQL error fetching collation: #{e.message}" }
|
160
160
|
nil
|
161
161
|
end
|
@@ -202,7 +202,7 @@ module RailsLens
|
|
202
202
|
# Table doesn't exist or no permission to query information_schema
|
203
203
|
Rails.logger.debug { "Failed to check partitions for #{table_name}: #{e.message}" }
|
204
204
|
false
|
205
|
-
rescue
|
205
|
+
rescue => e
|
206
206
|
# MySQL specific errors (connection issues, etc)
|
207
207
|
Rails.logger.debug { "MySQL error checking partitions: #{e.message}" }
|
208
208
|
false
|
@@ -231,7 +231,7 @@ module RailsLens
|
|
231
231
|
rescue ActiveRecord::StatementInvalid => e
|
232
232
|
# Permission denied or table doesn't exist
|
233
233
|
Rails.logger.debug { "Failed to fetch partitions for #{table_name}: #{e.message}" }
|
234
|
-
rescue
|
234
|
+
rescue => e
|
235
235
|
# MySQL specific errors
|
236
236
|
Rails.logger.debug { "MySQL error fetching partitions: #{e.message}" }
|
237
237
|
end
|
@@ -269,7 +269,7 @@ module RailsLens
|
|
269
269
|
rescue ActiveRecord::StatementInvalid => e
|
270
270
|
# Permission denied or table doesn't exist
|
271
271
|
Rails.logger.debug { "Failed to fetch partitions for #{table_name}: #{e.message}" }
|
272
|
-
rescue
|
272
|
+
rescue => e
|
273
273
|
# MySQL specific errors
|
274
274
|
Rails.logger.debug { "MySQL error fetching partitions: #{e.message}" }
|
275
275
|
end
|
@@ -112,8 +112,8 @@ module RailsLens
|
|
112
112
|
next
|
113
113
|
end
|
114
114
|
|
115
|
-
# Skip models without tables or with missing tables
|
116
|
-
unless model.table_exists?
|
115
|
+
# Skip models without tables or with missing tables (but not abstract classes)
|
116
|
+
unless model.abstract_class? || model.table_exists?
|
117
117
|
results[:skipped] << model.name
|
118
118
|
warn "Skipping #{model.name} - table does not exist" if options[:verbose]
|
119
119
|
next
|
data/lib/rails_lens/version.rb
CHANGED