rails_lens 0.3.0 → 0.5.0

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: 983d1382bf42b79549a4a504e696814846eccf606cd17f9fc3e521cce211f729
4
+ data.tar.gz: 69714d9bfaf91dc139158649596b8111b6910d7515adb9be9928476ff95e6c3a
5
5
  SHA512:
6
- metadata.gz: 0dde2b4d9087a1221242451d4c9a31bd558c566659ae25115be060618bd318c40a864b291965d9dab8fd16a9a6f05ddbf9a2e901f9c845a15b8b38e93573988b
7
- data.tar.gz: accd1ecbba992e1904c098ad03f946c19b181a42029fb02594efb72af2aee6e169441286cf754ffc866a45de5ed21d24839d77df0ca9cc9fb7a96358b65c5c7c
6
+ metadata.gz: e580de20d715134c6066995921030e9aaf27ecdce188f455b93eb4bfd3b84d1bf9fcce109d75fe0d0fd340fd856c428d8da0e338e5d3b97299efc60098f72ebd
7
+ data.tar.gz: b069e0140d3ff3924b4f0ba18695f7646a88167a797e10bdd35d5fb25783ac2f8c7c305e59b22458b1424be22b75c494d2ab37b6f4d4d79448ee189624c869aa
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0](https://github.com/seuros/rails_lens/compare/rails_lens/v0.3.0...rails_lens/v0.5.0) (2025-12-06)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * refactor the extension modules
9
+
10
+ ### Features
11
+
12
+ * refactor the extension modules ([de80a63](https://github.com/seuros/rails_lens/commit/de80a638f968cc24e1b8c7906054dbb2292df772))
13
+
3
14
  ## [0.3.0](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.13...rails_lens/v0.3.0) (2025-11-29)
4
15
 
5
16
 
@@ -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,53 @@ module RailsLens
148
148
  end
149
149
 
150
150
  def self.annotate_all(options = {})
151
+ results = { annotated: [], skipped: [], failed: [] }
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
+ merge_results(results, source_results)
158
+ end
159
+
160
+ results
161
+ end
162
+
163
+ # Annotate models from a specific source
164
+ def self.annotate_source(source, options = {})
165
+ results = { annotated: [], skipped: [], failed: [] }
166
+
167
+ begin
168
+ models = source.models(options)
169
+ puts " Found #{models.size} #{source.source_name} models" if options[:verbose]
170
+
171
+ models.each do |model|
172
+ result = source.annotate_model(model, options)
173
+ case result[:status]
174
+ when :annotated
175
+ results[:annotated] << result[:model]
176
+ when :skipped
177
+ results[:skipped] << result[:model]
178
+ when :failed
179
+ results[:failed] << { model: result[:model], error: result[:message] }
180
+ end
181
+ end
182
+ rescue StandardError => e
183
+ puts " Error processing #{source.source_name} source: #{e.message}" if options[:verbose]
184
+ end
185
+
186
+ results
187
+ end
188
+
189
+ # Merge source results into main results
190
+ def self.merge_results(main, source)
191
+ main[:annotated].concat(source[:annotated] || [])
192
+ main[:skipped].concat(source[:skipped] || [])
193
+ main[:failed].concat(source[:failed] || [])
194
+ end
195
+
196
+ # Original ActiveRecord-specific annotation logic (used by ActiveRecordSource)
197
+ def self.annotate_active_record_models(options = {})
151
198
  # Convert models option to include option for ModelDetector
152
199
  if options[:models]
153
200
  options[:include] = options[:models]
@@ -265,10 +312,52 @@ module RailsLens
265
312
  end
266
313
 
267
314
  def self.remove_all(options = {})
268
- # Use filesystem-based removal (doesn't require database)
269
- remove_all_by_filesystem(options)
315
+ results = { removed: [], skipped: [], failed: [] }
316
+
317
+ # Iterate through all model sources
318
+ ModelSourceLoader.load_sources.each do |source|
319
+ puts "Removing annotations from #{source.source_name} models..." if options[:verbose]
320
+ source_results = remove_source(source, options)
321
+ merge_remove_results(results, source_results)
322
+ end
323
+
324
+ results
325
+ end
326
+
327
+ # Remove annotations from a specific source
328
+ def self.remove_source(source, options = {})
329
+ results = { removed: [], skipped: [], failed: [] }
330
+
331
+ begin
332
+ models = source.models(options.merge(include_abstract: true))
333
+ puts " Found #{models.size} #{source.source_name} models" if options[:verbose]
334
+
335
+ models.each do |model|
336
+ result = source.remove_annotation(model)
337
+ case result[:status]
338
+ when :removed
339
+ results[:removed] << result[:model]
340
+ when :skipped
341
+ results[:skipped] << result[:model]
342
+ when :failed
343
+ results[:failed] << { model: result[:model], error: result[:message] }
344
+ end
345
+ end
346
+ rescue StandardError => e
347
+ puts " Error removing from #{source.source_name} source: #{e.message}" if options[:verbose]
348
+ end
349
+
350
+ results
351
+ end
352
+
353
+ # Merge removal results into main results
354
+ def self.merge_remove_results(main, source)
355
+ main[:removed].concat(source[:removed] || [])
356
+ main[:skipped].concat(source[:skipped] || [])
357
+ main[:failed].concat(source[:failed] || [])
270
358
  end
271
359
 
360
+ # Original filesystem-based removal (kept for backwards compatibility)
272
361
  def self.remove_all_by_filesystem(options = {})
273
362
  base_path = options[:models_path] || default_models_path
274
363
  results = { removed: [], skipped: [], failed: [] }
@@ -21,9 +21,38 @@ namespace :rails_lens do
21
21
  end
22
22
  end
23
23
 
24
+ desc 'Remove all annotations from models'
25
+ task remove: :environment do
26
+ require 'rails_lens/schema/annotation_manager'
27
+
28
+ results = RailsLens::Schema::AnnotationManager.remove_all
29
+
30
+ puts "Removed annotations from #{results[:removed].length} models" if results[:removed].any?
31
+ puts "Skipped #{results[:skipped].length} models (no annotations)" if results[:skipped].any?
32
+ if results[:failed].any?
33
+ puts "Failed to remove annotations from #{results[:failed].length} models:"
34
+ results[:failed].each do |failure|
35
+ puts " - #{failure[:model]}: #{failure[:error]}"
36
+ end
37
+ end
38
+ end
39
+
40
+ desc 'List registered model sources'
41
+ task sources: :environment do
42
+ require 'rails_lens'
43
+
44
+ puts 'Registered model sources:'
45
+ RailsLens::ModelSourceLoader.list_sources.each do |source|
46
+ puts " - #{source[:name]} (#{source[:class]})"
47
+ source[:patterns].each do |pattern|
48
+ puts " #{pattern}"
49
+ end
50
+ end
51
+ end
52
+
24
53
  desc 'Annotate all Rails files (models, routes, and mailers)'
25
54
  task all: :environment do
26
- # Annotate models
55
+ # Annotate models (includes all registered model sources)
27
56
  Rake::Task['rails_lens:annotate'].invoke
28
57
 
29
58
  # Annotate routes
@@ -32,4 +61,16 @@ namespace :rails_lens do
32
61
  # Annotate mailers
33
62
  Rake::Task['rails_lens:mailers:annotate'].invoke
34
63
  end
64
+
65
+ desc 'Remove all annotations from Rails files (models, routes, and mailers)'
66
+ task remove_all: :environment do
67
+ # Remove model annotations (includes all registered model sources)
68
+ Rake::Task['rails_lens:remove'].invoke
69
+
70
+ # Remove route annotations
71
+ Rake::Task['rails_lens:routes:remove'].invoke
72
+
73
+ # Remove mailer annotations
74
+ Rake::Task['rails_lens:mailers:remove'].invoke
75
+ end
35
76
  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.0'
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.0
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