Class: RailsLens::Schema::AnnotationManager
- Inherits:
-
Object
- Object
- RailsLens::Schema::AnnotationManager
- Includes:
- AnnotationRemoval
- Defined in:
- lib/rails_lens/schema/annotation_manager.rb
Instance Attribute Summary collapse
-
#model_class ⇒ Object
readonly
Returns the value of attribute model_class.
Class Method Summary collapse
-
.annotate_active_record_models(options = {}) ⇒ Object
Original ActiveRecord-specific annotation logic (used by ActiveRecordSource).
- .annotate_all(options = {}) ⇒ Object
-
.annotate_source(source, options = {}) ⇒ Object
Annotate models from a specific source.
- .default_models_path ⇒ Object
-
.merge_results(main, source, primary_key) ⇒ Object
Merge source results into main results.
- .process_model_with_connection(model, connection, results, options) ⇒ Object
-
.process_models_on_pool(pool, pool_models, results, options) ⇒ Object
Process every model in
pool_modelsover a single checked-out connection frompool. -
.record_result(results, result) ⇒ Object
Bucket a single per-model result into the aggregate results hash.
- .remove_all(options = {}) ⇒ Object
-
.remove_all_by_filesystem(options = {}) ⇒ Object
Original filesystem-based removal (kept for backwards compatibility).
-
.remove_source(source, options = {}) ⇒ Object
Remove annotations from a specific source.
Instance Method Summary collapse
- #annotate_file(file_path = nil, allow_external_files: false) ⇒ Object
- #generate_annotation ⇒ Object
-
#initialize(model_class) ⇒ AnnotationManager
constructor
A new instance of AnnotationManager.
Methods included from AnnotationRemoval
Constructor Details
#initialize(model_class) ⇒ AnnotationManager
Returns a new instance of AnnotationManager.
10 11 12 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 10 def initialize(model_class) @model_class = model_class end |
Instance Attribute Details
#model_class ⇒ Object (readonly)
Returns the value of attribute model_class.
8 9 10 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 8 def model_class @model_class end |
Class Method Details
.annotate_active_record_models(options = {}) ⇒ Object
Original ActiveRecord-specific annotation logic (used by ActiveRecordSource)
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 141 def self.annotate_active_record_models( = {}) # Convert models option to include option for ModelDetector if [:models] [:include] = [:models] end models = ModelDetector.detect_models() puts "Detected #{models.size} models for annotation" if [:verbose] # Filter abstract classes based on options if [:include_abstract] # Include all models elsif [:abstract_only] models = models.select(&:abstract_class?) else # Default: exclude abstract classes models = models.reject(&:abstract_class?) end results = { annotated: [], skipped: [], failed: [] } # Group models by their connection pool to process each database separately models_by_connection_pool = models.group_by do |model| pool = model.connection_pool pool rescue StandardError => e puts "Model #{model.name} -> NO POOL (#{e.})" if [:verbose] nil # Models without connection pools will use primary pool end # Force models without connection pools to use the primary connection pool if models_by_connection_pool[nil]&.any? begin primary_pool = ApplicationRecord.connection_pool models_by_connection_pool[primary_pool] ||= [] models_by_connection_pool[primary_pool].concat(models_by_connection_pool[nil]) models_by_connection_pool.delete(nil) rescue StandardError => e puts "Failed to assign to primary pool: #{e.}" if [:verbose] end end models_by_connection_pool.each do |connection_pool, pool_models| if connection_pool # Process all models for this database using a single connection process_models_on_pool(connection_pool, pool_models, results, ) else # This should not happen anymore since we assign orphaned models to primary pool # Use primary connection pool as fallback to avoid creating new connections begin process_models_on_pool(ApplicationRecord.connection_pool, pool_models, results, ) rescue StandardError => e # Last resort: process without connection management (will create multiple connections) pool_models.each do |model| process_model_with_connection(model, nil, results, ) end end end end results end |
.annotate_all(options = {}) ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 88 def self.annotate_all( = {}) results = { annotated: [], skipped: [], failed: [], by_source: {} } # Iterate through all model sources ModelSourceLoader.load_sources.each do |source| puts "Annotating #{source.source_name} models..." if [:verbose] source_results = annotate_source(source, ) results[:by_source][source.source_name] = source_results[:annotated].length merge_results(results, source_results, :annotated) end results end |
.annotate_source(source, options = {}) ⇒ Object
Annotate models from a specific source
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 103 def self.annotate_source(source, = {}) results = { annotated: [], skipped: [], failed: [] } begin models = source.models() puts " Found #{models.size} #{source.source_name} models" if [:verbose] models.each do |model| record_result(results, source.annotate_model(model, )) end rescue StandardError => e puts " Error processing #{source.source_name} source: #{e.}" if [:verbose] end results end |
.default_models_path ⇒ Object
317 318 319 320 321 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 317 def self.default_models_path return Rails.root.join('app/models') if defined?(Rails.root) File.join(Dir.pwd, 'app', 'models') end |
.merge_results(main, source, primary_key) ⇒ Object
Merge source results into main results. primary_key is the source-specific success bucket (:annotated or :removed).
122 123 124 125 126 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 122 def self.merge_results(main, source, primary_key) main[primary_key].concat(source[primary_key] || []) main[:skipped].concat(source[:skipped] || []) main[:failed].concat(source[:failed] || []) end |
.process_model_with_connection(model, connection, results, options) ⇒ Object
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 214 def self.process_model_with_connection(model, connection, results, ) # Ensure model is actually a class, not a hash or other object unless model.is_a?(Class) results[:failed] << { model: model.inspect, error: "Expected Class, got #{model.class}" } return end # Skip models without tables or with missing tables (but not abstract classes) unless model.abstract_class? || model.table_exists? results[:skipped] << model.name return end manager = new(model) # Set the connection in the manager if provided manager.instance_variable_set(:@connection, connection) if connection # Determine file path based on options file_path = if [:models_path] File.join([:models_path], "#{model.name.underscore}.rb") else nil # Use default model_file_path end # Allow external files when models_path is provided (for testing) allow_external = [:models_path].present? if manager.annotate_file(file_path, allow_external_files: allow_external) results[:annotated] << model.name else results[:skipped] << model.name end rescue ActiveRecord::StatementInvalid # Handle database-related errors (missing tables, schemas, etc.) results[:skipped] << model.name rescue StandardError => e model_name = if model.is_a?(Class) && model.respond_to?(:name) model.name else model.inspect end results[:failed] << { model: model_name, error: e. } end |
.process_models_on_pool(pool, pool_models, results, options) ⇒ Object
Process every model in pool_models over a single checked-out connection from pool.
206 207 208 209 210 211 212 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 206 def self.process_models_on_pool(pool, pool_models, results, ) pool.with_connection do |connection| pool_models.each do |model| process_model_with_connection(model, connection, results, ) end end end |
.record_result(results, result) ⇒ Object
Bucket a single per-model result into the aggregate results hash. The status symbol (:annotated/:removed/:skipped) doubles as the bucket key; :failed records carry the error message.
131 132 133 134 135 136 137 138 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 131 def self.record_result(results, result) status = result[:status] if status == :failed results[:failed] << { model: result[:model], error: result[:message] } elsif results.key?(status) results[status] << result[:model] end end |
.remove_all(options = {}) ⇒ Object
259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 259 def self.remove_all( = {}) results = { removed: [], skipped: [], failed: [], by_source: {} } # Iterate through all model sources ModelSourceLoader.load_sources.each do |source| puts "Removing annotations from #{source.source_name} models..." if [:verbose] source_results = remove_source(source, ) results[:by_source][source.source_name] = source_results[:removed].length merge_results(results, source_results, :removed) end results end |
.remove_all_by_filesystem(options = {}) ⇒ Object
Original filesystem-based removal (kept for backwards compatibility)
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 292 def self.remove_all_by_filesystem( = {}) base_path = [:models_path] || default_models_path results = { removed: [], skipped: [], failed: [] } pattern = File.join(base_path, '**', '*.rb') files = Dir.glob(pattern) files.each do |file_path| content = File.read(file_path) next unless Annotation.extract(content) cleaned = Annotation.remove(content) if cleaned == content results[:skipped] << File.basename(file_path, '.rb').camelize else File.write(file_path, cleaned) model_name = File.basename(file_path, '.rb').camelize results[:removed] << model_name end rescue StandardError => e results[:failed] << { model: File.basename(file_path, '.rb').camelize, error: e. } end results end |
.remove_source(source, options = {}) ⇒ Object
Remove annotations from a specific source
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 274 def self.remove_source(source, = {}) results = { removed: [], skipped: [], failed: [] } begin models = source.models(.merge(include_abstract: true)) puts " Found #{models.size} #{source.source_name} models" if [:verbose] models.each do |model| record_result(results, source.remove_annotation(model)) end rescue StandardError => e puts " Error removing from #{source.source_name} source: #{e.}" if [:verbose] end results end |
Instance Method Details
#annotate_file(file_path = nil, allow_external_files: false) ⇒ Object
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 14 def annotate_file(file_path = nil, allow_external_files: false) file_path ||= model_file_path return unless file_path && File.exist?(file_path) # Only annotate files within the Rails application (unless explicitly allowed) # For engines/gems with dummy apps, check if the file is within the parent directory if !allow_external_files && defined?(Rails.root) rails_root = Rails.root.to_s # Check if this is a dummy app inside a gem/engine parent_root = if rails_root.include?('/test/dummy') File.('../..', rails_root) else rails_root end unless file_path.start_with?(rails_root) || file_path.start_with?(parent_root) return end end annotation_text = generate_annotation original_content = File.read(file_path) content = Annotation.extract(original_content) ? Annotation.remove(original_content) : original_content annotated_content = add_annotation(content, file_path, annotation_text) if annotated_content == original_content false else File.write(file_path, annotated_content) true end end |
#generate_annotation ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/rails_lens/schema/annotation_manager.rb', line 48 def generate_annotation providers = AnnotationPipeline.new.instance_variable_get(:@providers) # If we have a connection set by annotate_all, use it to process all providers results = if @connection collect_provider_results(providers, @connection) else # Fallback: Use the model's connection pool with proper management # This path is used when annotating individual models warn "Using fallback connection management for #{model_class.name}" if RailsLens.config.verbose model_class.connection_pool.with_connection do |connection| collect_provider_results(providers, connection) end end annotation = Annotation.new # Add schema content annotation.add_lines(results[:schema].split("\n")) if results[:schema] # Add sections results[:sections].each do |section| next unless section && section[:content] annotation.add_line('') # The provider can optionally provide a title annotation.add_line(section[:title]) if section[:title] annotation.add_lines(section[:content].split("\n")) end # Add notes as TOML array (already in compact format from analyzers) if results[:notes].any? annotation.add_line('') annotation.add_line("notes = #{TomlFormat.quoted_array(results[:notes].uniq)}") end annotation.to_s end |