rails_lens 0.3.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e06defab67dbc0cea4db854e6eb7efc9d66f292dfc675c4f30a6df45d757a8ad
4
- data.tar.gz: 621535415ca17f88bc82cc0cf933284e3251fd2d8f7feed01139820b0c62bbfd
3
+ metadata.gz: 1bd96547bfc5ae90c3971f787bbb03cad57e8912ed25c4ea0005ab3652401408
4
+ data.tar.gz: 95f1bcfa75b796a22c1fd5fb861c79e41e048608648388d1d9b255b4a3466157
5
5
  SHA512:
6
- metadata.gz: 0dde2b4d9087a1221242451d4c9a31bd558c566659ae25115be060618bd318c40a864b291965d9dab8fd16a9a6f05ddbf9a2e901f9c845a15b8b38e93573988b
7
- data.tar.gz: accd1ecbba992e1904c098ad03f946c19b181a42029fb02594efb72af2aee6e169441286cf754ffc866a45de5ed21d24839d77df0ca9cc9fb7a96358b65c5c7c
6
+ metadata.gz: 960195b484708f9904f1d96d5f52e5fca70580431deffdc9a5cb56c99ab57af3509c33ad88eb7cdfa84c75067ea04f1d542568152cc60ab6dbcb162b036d1b61
7
+ data.tar.gz: da71bed238e3fa5742039fa0f931d4fc625f52a6d1df3cc831678bd070b65b38e6f3ea4b01b998c7e163409764db0aebfaab83af9a10733081d99513f6a80893
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.1](https://github.com/seuros/rails_lens/compare/rails_lens/v0.5.0...rails_lens/v0.5.1) (2025-12-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * add by_source ([f6693f3](https://github.com/seuros/rails_lens/commit/f6693f384761de505eac0e525c7b70f4c053d14c))
9
+
10
+ ## [0.5.0](https://github.com/seuros/rails_lens/compare/rails_lens/v0.3.0...rails_lens/v0.5.0) (2025-12-06)
11
+
12
+
13
+ ### ⚠ BREAKING CHANGES
14
+
15
+ * refactor the extension modules
16
+
17
+ ### Features
18
+
19
+ * refactor the extension modules ([de80a63](https://github.com/seuros/rails_lens/commit/de80a638f968cc24e1b8c7906054dbb2292df772))
20
+
3
21
  ## [0.3.0](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.13...rails_lens/v0.3.0) (2025-11-29)
4
22
 
5
23
 
@@ -211,14 +211,14 @@ module RailsLens
211
211
  File.write(rake_file, rake_task_template)
212
212
 
213
213
  output.say "Created rake task at #{rake_file}", :green
214
- output.say '', :reset
214
+ output.say ''
215
215
  output.say 'The following task has been installed:', :blue
216
216
  output.say ' • rails_lens:annotate - Annotate models after migrations', :green
217
- output.say '', :reset
217
+ output.say ''
218
218
  output.say 'Configuration options in lib/tasks/rails_lens.rake:', :blue
219
219
  output.say ' • AUTO_ANNOTATE (default: true in development)', :cyan
220
220
  output.say ' • RAILS_LENS_ENV (default: development)', :cyan
221
- output.say '', :reset
221
+ output.say ''
222
222
  output.say 'Disable auto-annotation:', :blue
223
223
  output.say ' export AUTO_ANNOTATE=false', :cyan
224
224
 
@@ -3,7 +3,7 @@
3
3
  module RailsLens
4
4
  class Config
5
5
  attr_accessor :verbose, :debug, :raise_on_error, :logger,
6
- :annotations, :erd, :schema, :extensions, :routes, :mailers
6
+ :annotations, :erd, :schema, :extensions, :routes, :mailers, :model_sources
7
7
 
8
8
  def initialize
9
9
  @verbose = false
@@ -76,6 +76,11 @@ module RailsLens
76
76
  pattern: '**/*_mailer.rb',
77
77
  exclusion_pattern: 'vendor/**/*_mailer.rb'
78
78
  }
79
+
80
+ @model_sources = {
81
+ enabled: true, # Enable gem-provided model sources
82
+ error_reporting: :warn # :silent, :warn, :verbose
83
+ }
79
84
  end
80
85
  end
81
86
 
@@ -138,13 +138,14 @@ module RailsLens
138
138
  # Check each loaded gem for RailsLens extensions
139
139
 
140
140
  Gem.loaded_specs.each_key do |gem_name|
141
- # Try to find extension in the gem
142
- gem_constant_name = gem_name.gsub('-', '_').split('_').map(&:capitalize).join
143
- extension_constant_name = "#{gem_constant_name}::RailsLensExtension"
144
-
145
141
  # Skip gems that are likely to cause autoload issues
146
142
  next if %w[digest openssl uri net json].include?(gem_name)
147
143
 
144
+ # Try to find extension in the gem
145
+ # Use ActiveSupport's camelize for proper Rails-style conversion (e.g., 'activecypher' -> 'ActiveCypher')
146
+ gem_constant_name = gem_name.gsub('-', '_').camelize
147
+ extension_constant_name = "#{gem_constant_name}::RailsLensExtension"
148
+
148
149
  # First check if the gem constant exists without triggering autoload
149
150
  next unless Object.const_defined?(gem_constant_name, false)
150
151
 
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ # Base class for model sources
5
+ # Model sources provide pluggable discovery of different model types
6
+ # (e.g., ActiveRecord, ActiveCypher, etc.)
7
+ #
8
+ # Gems can register their own model source by defining:
9
+ # GemName::RailsLensModelSource < RailsLens::ModelSource
10
+ #
11
+ # Example:
12
+ # module MyOrm
13
+ # class ModelSource < ::RailsLens::ModelSource
14
+ # def self.models(options = {})
15
+ # # Return array of model classes
16
+ # end
17
+ #
18
+ # def self.file_patterns
19
+ # ['app/my_models/**/*.rb']
20
+ # end
21
+ #
22
+ # def self.annotate_model(model, options = {})
23
+ # # Return { status: :annotated/:skipped/:failed, model: name, ... }
24
+ # end
25
+ #
26
+ # def self.remove_annotation(model)
27
+ # # Return { status: :removed/:skipped, model: name, ... }
28
+ # end
29
+ # end
30
+ #
31
+ # RailsLensModelSource = ModelSource
32
+ # end
33
+ #
34
+ class ModelSource
35
+ class << self
36
+ # Return array of model classes to annotate
37
+ # @param options [Hash] Options passed from CLI
38
+ # @return [Array<Class>] Array of model classes
39
+ def models(_options = {})
40
+ raise NotImplementedError, "#{name} must implement .models"
41
+ end
42
+
43
+ # Return file patterns for annotation removal
44
+ # Used when removing annotations by filesystem scan
45
+ # @return [Array<String>] Glob patterns relative to Rails.root
46
+ def file_patterns
47
+ raise NotImplementedError, "#{name} must implement .file_patterns"
48
+ end
49
+
50
+ # Annotate a single model
51
+ # @param model [Class] The model class to annotate
52
+ # @param options [Hash] Options passed from CLI
53
+ # @return [Hash] Result with :status (:annotated, :skipped, :failed), :model, :file, :message
54
+ def annotate_model(_model, _options = {})
55
+ raise NotImplementedError, "#{name} must implement .annotate_model"
56
+ end
57
+
58
+ # Remove annotation from a single model
59
+ # @param model [Class] The model class
60
+ # @return [Hash] Result with :status (:removed, :skipped), :model, :file
61
+ def remove_annotation(_model)
62
+ raise NotImplementedError, "#{name} must implement .remove_annotation"
63
+ end
64
+
65
+ # Human-readable name for this source (used in logging)
66
+ # @return [String]
67
+ def source_name
68
+ name.demodulize.sub(/Source$/, '').underscore.humanize
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ # Discovers and loads model sources from gems
5
+ # Gems can register sources in two ways:
6
+ # 1. Define GemName::RailsLensModelSource (auto-discovery)
7
+ # 2. Call RailsLens::ModelSourceLoader.register(SourceClass) explicitly
8
+ class ModelSourceLoader
9
+ @registered_sources = []
10
+
11
+ class << self
12
+ # Register a model source explicitly
13
+ # Use this when gem naming doesn't follow conventions
14
+ # @param source [Class] Model source class
15
+ def register(source)
16
+ return unless valid_source?(source)
17
+
18
+ @registered_sources ||= []
19
+ @registered_sources << source unless @registered_sources.include?(source)
20
+ end
21
+
22
+ # Load all available model sources
23
+ # @return [Array<Class>] Array of model source classes
24
+ def load_sources
25
+ sources = []
26
+
27
+ # Always include ActiveRecord source first
28
+ sources << ModelSources::ActiveRecordSource
29
+
30
+ # Include explicitly registered sources
31
+ @registered_sources ||= []
32
+ sources.concat(@registered_sources)
33
+
34
+ # Load gem-provided sources via auto-discovery if enabled
35
+ sources.concat(load_gem_sources) if config_enabled?
36
+
37
+ # Deduplicate in case a source was both registered and auto-discovered
38
+ sources.uniq
39
+ end
40
+
41
+ # List all loaded sources (for debugging/info)
42
+ # @return [Array<Hash>] Source info with name and class
43
+ def list_sources
44
+ load_sources.map do |source|
45
+ {
46
+ name: source.source_name,
47
+ class: source.name,
48
+ patterns: source.file_patterns
49
+ }
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def config_enabled?
56
+ config = RailsLens.config.model_sources
57
+ config && config[:enabled]
58
+ end
59
+
60
+ def load_gem_sources
61
+ sources = []
62
+
63
+ Gem.loaded_specs.each_key do |gem_name|
64
+ source = find_source_for_gem(gem_name)
65
+ sources << source if source && valid_source?(source)
66
+ end
67
+
68
+ sources
69
+ end
70
+
71
+ def find_source_for_gem(gem_name)
72
+ # Skip gems that might cause autoload issues
73
+ return nil if %w[digest openssl uri net json].include?(gem_name)
74
+
75
+ # Convert gem name to constant (e.g., 'activecypher' -> 'ActiveCypher')
76
+ # Use ActiveSupport's camelize for proper Rails-style conversion
77
+ gem_constant_name = gem_name.gsub('-', '_').camelize
78
+
79
+ # Check if gem constant exists without triggering autoload
80
+ return nil unless Object.const_defined?(gem_constant_name, false)
81
+
82
+ gem_constant = Object.const_get(gem_constant_name)
83
+ return nil unless gem_constant.is_a?(Module)
84
+
85
+ # Check if it has a RailsLensModelSource
86
+ return nil unless gem_constant.const_defined?('RailsLensModelSource', false)
87
+
88
+ gem_constant.const_get('RailsLensModelSource')
89
+ rescue NameError
90
+ nil
91
+ rescue StandardError => e
92
+ log_error("Error loading model source from #{gem_name}: #{e.message}")
93
+ nil
94
+ end
95
+
96
+ def valid_source?(klass)
97
+ return false unless klass.is_a?(Class)
98
+
99
+ required_methods = %i[models file_patterns annotate_model remove_annotation]
100
+ required_methods.all? { |m| klass.respond_to?(m) }
101
+ end
102
+
103
+ def log_error(message)
104
+ error_reporting = RailsLens.config.model_sources[:error_reporting] || :warn
105
+
106
+ case error_reporting
107
+ when :silent
108
+ # Do nothing
109
+ when :warn
110
+ RailsLens.logger.warn "[RailsLens ModelSources] #{message}"
111
+ when :verbose
112
+ RailsLens.logger.error "[RailsLens ModelSources] #{message}"
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ module ModelSources
5
+ # Built-in model source for ActiveRecord models
6
+ class ActiveRecordSource < ModelSource
7
+ class << self
8
+ def models(options = {})
9
+ # Convert models option to include option for ModelDetector
10
+ opts = options.dup
11
+ opts[:include] = opts.delete(:models) if opts[:models]
12
+
13
+ models = ModelDetector.detect_models(opts)
14
+
15
+ # Filter abstract classes based on options
16
+ if opts[:include_abstract]
17
+ # Include all models
18
+ elsif opts[:abstract_only]
19
+ models = models.select(&:abstract_class?)
20
+ else
21
+ # Default: exclude abstract classes
22
+ models = models.reject(&:abstract_class?)
23
+ end
24
+
25
+ models
26
+ end
27
+
28
+ def file_patterns
29
+ ['app/models/**/*.rb']
30
+ end
31
+
32
+ def annotate_model(model, options = {})
33
+ # Use the optimized connection-pooled annotation
34
+ results = { annotated: [], skipped: [], failed: [] }
35
+
36
+ # Group this single model by connection pool for consistency
37
+ begin
38
+ pool = model.connection_pool
39
+ pool.with_connection do |connection|
40
+ Schema::AnnotationManager.process_model_with_connection(model, connection, results, options)
41
+ end
42
+ rescue StandardError
43
+ # Fallback without connection management
44
+ Schema::AnnotationManager.process_model_with_connection(model, nil, results, options)
45
+ end
46
+
47
+ if results[:annotated].include?(model.name)
48
+ { status: :annotated, model: model.name, file: model_file_path(model) }
49
+ elsif results[:failed].any? { |f| f[:model] == model.name }
50
+ failure = results[:failed].find { |f| f[:model] == model.name }
51
+ { status: :failed, model: model.name, message: failure[:error] }
52
+ else
53
+ { status: :skipped, model: model.name }
54
+ end
55
+ end
56
+
57
+ def remove_annotation(model)
58
+ manager = Schema::AnnotationManager.new(model)
59
+ if manager.remove_annotations
60
+ { status: :removed, model: model.name, file: model_file_path(model) }
61
+ else
62
+ { status: :skipped, model: model.name }
63
+ end
64
+ rescue StandardError => e
65
+ { status: :failed, model: model.name, message: e.message }
66
+ end
67
+
68
+ def source_name
69
+ 'ActiveRecord'
70
+ end
71
+
72
+ private
73
+
74
+ def model_file_path(model)
75
+ return nil unless model.name
76
+
77
+ const_source_location = Object.const_source_location(model.name)
78
+ return const_source_location.first if const_source_location
79
+
80
+ if defined?(Rails.root)
81
+ Rails.root.join('app', 'models', "#{model.name.underscore}.rb").to_s
82
+ end
83
+ rescue StandardError
84
+ nil
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -148,6 +148,54 @@ module RailsLens
148
148
  end
149
149
 
150
150
  def self.annotate_all(options = {})
151
+ results = { annotated: [], skipped: [], failed: [], by_source: {} }
152
+
153
+ # Iterate through all model sources
154
+ ModelSourceLoader.load_sources.each do |source|
155
+ puts "Annotating #{source.source_name} models..." if options[:verbose]
156
+ source_results = annotate_source(source, options)
157
+ results[:by_source][source.source_name] = source_results[:annotated].length
158
+ merge_results(results, source_results)
159
+ end
160
+
161
+ results
162
+ end
163
+
164
+ # Annotate models from a specific source
165
+ def self.annotate_source(source, options = {})
166
+ results = { annotated: [], skipped: [], failed: [] }
167
+
168
+ begin
169
+ models = source.models(options)
170
+ puts " Found #{models.size} #{source.source_name} models" if options[:verbose]
171
+
172
+ models.each do |model|
173
+ result = source.annotate_model(model, options)
174
+ case result[:status]
175
+ when :annotated
176
+ results[:annotated] << result[:model]
177
+ when :skipped
178
+ results[:skipped] << result[:model]
179
+ when :failed
180
+ results[:failed] << { model: result[:model], error: result[:message] }
181
+ end
182
+ end
183
+ rescue StandardError => e
184
+ puts " Error processing #{source.source_name} source: #{e.message}" if options[:verbose]
185
+ end
186
+
187
+ results
188
+ end
189
+
190
+ # Merge source results into main results
191
+ def self.merge_results(main, source)
192
+ main[:annotated].concat(source[:annotated] || [])
193
+ main[:skipped].concat(source[:skipped] || [])
194
+ main[:failed].concat(source[:failed] || [])
195
+ end
196
+
197
+ # Original ActiveRecord-specific annotation logic (used by ActiveRecordSource)
198
+ def self.annotate_active_record_models(options = {})
151
199
  # Convert models option to include option for ModelDetector
152
200
  if options[:models]
153
201
  options[:include] = options[:models]
@@ -265,10 +313,53 @@ module RailsLens
265
313
  end
266
314
 
267
315
  def self.remove_all(options = {})
268
- # Use filesystem-based removal (doesn't require database)
269
- remove_all_by_filesystem(options)
316
+ results = { removed: [], skipped: [], failed: [], by_source: {} }
317
+
318
+ # Iterate through all model sources
319
+ ModelSourceLoader.load_sources.each do |source|
320
+ puts "Removing annotations from #{source.source_name} models..." if options[:verbose]
321
+ source_results = remove_source(source, options)
322
+ results[:by_source][source.source_name] = source_results[:removed].length
323
+ merge_remove_results(results, source_results)
324
+ end
325
+
326
+ results
327
+ end
328
+
329
+ # Remove annotations from a specific source
330
+ def self.remove_source(source, options = {})
331
+ results = { removed: [], skipped: [], failed: [] }
332
+
333
+ begin
334
+ models = source.models(options.merge(include_abstract: true))
335
+ puts " Found #{models.size} #{source.source_name} models" if options[:verbose]
336
+
337
+ models.each do |model|
338
+ result = source.remove_annotation(model)
339
+ case result[:status]
340
+ when :removed
341
+ results[:removed] << result[:model]
342
+ when :skipped
343
+ results[:skipped] << result[:model]
344
+ when :failed
345
+ results[:failed] << { model: result[:model], error: result[:message] }
346
+ end
347
+ end
348
+ rescue StandardError => e
349
+ puts " Error removing from #{source.source_name} source: #{e.message}" if options[:verbose]
350
+ end
351
+
352
+ results
353
+ end
354
+
355
+ # Merge removal results into main results
356
+ def self.merge_remove_results(main, source)
357
+ main[:removed].concat(source[:removed] || [])
358
+ main[:skipped].concat(source[:skipped] || [])
359
+ main[:failed].concat(source[:failed] || [])
270
360
  end
271
361
 
362
+ # Original filesystem-based removal (kept for backwards compatibility)
272
363
  def self.remove_all_by_filesystem(options = {})
273
364
  base_path = options[:models_path] || default_models_path
274
365
  results = { removed: [], skipped: [], failed: [] }
@@ -11,7 +11,13 @@ namespace :rails_lens do
11
11
 
12
12
  results = RailsLens::Schema::AnnotationManager.annotate_all(options)
13
13
 
14
- puts "Annotated #{results[:annotated].length} models"
14
+ if results[:by_source]&.any?
15
+ results[:by_source].each do |source_name, count|
16
+ puts "Annotated #{count} #{source_name} models" if count.positive?
17
+ end
18
+ else
19
+ puts "Annotated #{results[:annotated].length} models"
20
+ end
15
21
  puts "Skipped #{results[:skipped].length} models" if results[:skipped].any?
16
22
  if results[:failed].any?
17
23
  puts "Failed to annotate #{results[:failed].length} models:"
@@ -21,9 +27,44 @@ namespace :rails_lens do
21
27
  end
22
28
  end
23
29
 
30
+ desc 'Remove all annotations from models'
31
+ task remove: :environment do
32
+ require 'rails_lens/schema/annotation_manager'
33
+
34
+ results = RailsLens::Schema::AnnotationManager.remove_all
35
+
36
+ if results[:by_source]&.any?
37
+ results[:by_source].each do |source_name, count|
38
+ puts "Removed annotations from #{count} #{source_name} models" if count.positive?
39
+ end
40
+ elsif results[:removed].any?
41
+ puts "Removed annotations from #{results[:removed].length} models"
42
+ end
43
+ puts "Skipped #{results[:skipped].length} models (no annotations)" if results[:skipped].any?
44
+ if results[:failed].any?
45
+ puts "Failed to remove annotations from #{results[:failed].length} models:"
46
+ results[:failed].each do |failure|
47
+ puts " - #{failure[:model]}: #{failure[:error]}"
48
+ end
49
+ end
50
+ end
51
+
52
+ desc 'List registered model sources'
53
+ task sources: :environment do
54
+ require 'rails_lens'
55
+
56
+ puts 'Registered model sources:'
57
+ RailsLens::ModelSourceLoader.list_sources.each do |source|
58
+ puts " - #{source[:name]} (#{source[:class]})"
59
+ source[:patterns].each do |pattern|
60
+ puts " #{pattern}"
61
+ end
62
+ end
63
+ end
64
+
24
65
  desc 'Annotate all Rails files (models, routes, and mailers)'
25
66
  task all: :environment do
26
- # Annotate models
67
+ # Annotate models (includes all registered model sources)
27
68
  Rake::Task['rails_lens:annotate'].invoke
28
69
 
29
70
  # Annotate routes
@@ -32,4 +73,16 @@ namespace :rails_lens do
32
73
  # Annotate mailers
33
74
  Rake::Task['rails_lens:mailers:annotate'].invoke
34
75
  end
76
+
77
+ desc 'Remove all annotations from Rails files (models, routes, and mailers)'
78
+ task remove_all: :environment do
79
+ # Remove model annotations (includes all registered model sources)
80
+ Rake::Task['rails_lens:remove'].invoke
81
+
82
+ # Remove route annotations
83
+ Rake::Task['rails_lens:routes:remove'].invoke
84
+
85
+ # Remove mailer annotations
86
+ Rake::Task['rails_lens:mailers:remove'].invoke
87
+ end
35
88
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsLens
4
- VERSION = '0.3.0'
4
+ VERSION = '0.5.1'
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.3.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -198,6 +198,9 @@ files:
198
198
  - lib/rails_lens/mailer/annotator.rb
199
199
  - lib/rails_lens/mailer/extractor.rb
200
200
  - lib/rails_lens/model_detector.rb
201
+ - lib/rails_lens/model_source.rb
202
+ - lib/rails_lens/model_source_loader.rb
203
+ - lib/rails_lens/model_sources/active_record_source.rb
201
204
  - lib/rails_lens/note_codes.rb
202
205
  - lib/rails_lens/parsers.rb
203
206
  - lib/rails_lens/parsers/class_info.rb