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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2df49b10f106235177d40b7029a8519ab45d7ab88395eb26fd56855e69a1ada
4
- data.tar.gz: 483fc63985bf291ff4a069793aaa9ff8a4e82d1c83c405dedce717fddc5d9dc6
3
+ metadata.gz: 41ebdd3c52cca1c77790aa896dcc5962acb92c44eec90a2dceeda98f310aefb7
4
+ data.tar.gz: 72e49695a796e9d030fd80aedab53d7dea055db82c98e8023a6a2bb4293e139f
5
5
  SHA512:
6
- metadata.gz: 20e9000c791458cb09cc530ba00e88b3033290c4b8124e6f10afc0d5ba0cf9af9bee932517753987da5f1955d4a13dd92d6c8315cf651962f45449b88bd02183
7
- data.tar.gz: 70bf1e3d3321abe358bf68d2e99a0d2a018e6f8c8e70cbe86d42e2c7358fdf0870c8d5f806628f63a2913fc431993576480fe03c10f1a8157148c5358ecf67da
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
+ ![Gem Version](https://img.shields.io/gem/v/rails_lens)
4
+ ![GitHub stars](https://img.shields.io/github/stars/seuros/rails_lens)
5
+ ![Downloads](https://img.shields.io/gem/dt/rails_lens)
6
+ ![License](https://img.shields.io/github/license/seuros/rails_lens)
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
- lines << "Types: #{delegated_type_info[:types].join(', ')}"
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
@@ -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
- output << ' }'
79
- output << ''
80
-
81
- Rails.logger.debug { "Added entity: #{model_display_name}" } if options[:verbose]
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
- # Don't add partial entity if there's an error
85
- # Remove the opening brace line if it was added
86
- output.pop if output.last&.end_with?(' {')
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
- return unless model.respond_to?(:_ct)
169
-
170
- output << " #{format_model_name(model)} }o--o{ #{format_model_name(model)} : \"closure_tree\""
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 basic database information
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 = begin
26
- connection.database_version
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 version
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 Mysql2::Error => e
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 Mysql2::Error => e
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 Mysql2::Error => e
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 Mysql2::Error => e
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 Mysql2::Error => e
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 Mysql2::Error => e
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsLens
4
- VERSION = '0.2.0'
4
+ VERSION = '0.2.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_lens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih