propel_api 0.3.1.5 → 0.3.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.
@@ -13,6 +13,17 @@ module PropelApi
13
13
  include PropelApi::ConfigurationMethods
14
14
  include PropelApi::PathGenerationMethods
15
15
 
16
+ # Shared class options for dependency management
17
+ class_option :no_auto_fix,
18
+ type: :boolean,
19
+ default: false,
20
+ desc: "Skip automatic dependency cleanup (not recommended)"
21
+
22
+ class_option :dry_run,
23
+ type: :boolean,
24
+ default: false,
25
+ desc: "Show what would be destroyed without actually doing it"
26
+
16
27
  protected
17
28
 
18
29
  def model_exists?
@@ -250,37 +261,33 @@ module PropelApi
250
261
  end
251
262
  end
252
263
 
253
- def create_propel_tests(test_types: [])
264
+ # Rails-reversible shared methods - no conditional logic!
265
+ def create_propel_model_test
266
+ template "tests/model_test_template.rb.tt", "test/models/#{file_name}_test.rb"
267
+ end
268
+
269
+ def create_propel_controller_test
270
+ template "tests/controller_test_template.rb.tt", "test/controllers/#{controller_path}/#{controller_file_name}_test.rb"
271
+ end
272
+
273
+ def create_propel_integration_test
274
+ template "tests/integration_test_template.rb.tt", "test/integration/#{file_name}_api_test.rb"
275
+ end
276
+
277
+ def create_propel_fixtures
254
278
  if behavior == :revoke
255
- # Remove test files
256
- test_types.each do |test_type|
257
- case test_type
258
- when :model
259
- remove_file "test/models/#{file_name}_test.rb"
260
- when :controller
261
- remove_file "test/controllers/#{controller_path}/#{controller_file_name}_test.rb"
262
- when :integration
263
- remove_file "test/integration/#{file_name}_api_test.rb"
264
- when :fixtures
265
- remove_file "test/fixtures/#{table_name}.yml"
266
- end
267
- end
279
+ # Rails convention: Keep stub fixture for 'fixtures :all' compatibility
280
+ empty_fixture_content = "# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html\n\n# This file was emptied when #{class_name} resource was destroyed\n# Keeping stub file so Rails fixtures :all doesn't break\n"
281
+ create_file "test/fixtures/#{table_name}.yml", empty_fixture_content
268
282
  else
269
- # Generate test files
270
- test_types.each do |test_type|
271
- case test_type
272
- when :model
273
- template "tests/model_test_template.rb.tt", "test/models/#{file_name}_test.rb"
274
- when :controller
275
- template "tests/controller_test_template.rb.tt", "test/controllers/#{controller_path}/#{controller_file_name}_test.rb"
276
- when :integration
277
- template "tests/integration_test_template.rb.tt", "test/integration/#{file_name}_api_test.rb"
278
- when :fixtures
279
- template "tests/fixtures_template.yml.tt", "test/fixtures/#{table_name}.yml"
280
- end
281
- end
283
+ template "tests/fixtures_template.yml.tt", "test/fixtures/#{table_name}.yml"
282
284
  end
283
285
  end
286
+
287
+ def create_propel_controller_template
288
+ initialize_propel_api_settings
289
+ template "scaffold/facet_controller_template.rb.tt", "app/controllers/#{controller_path}/#{controller_file_name}.rb"
290
+ end
284
291
 
285
292
  def show_propel_completion_message(resource_type:, generated_files:, next_steps:)
286
293
  if behavior == :revoke
@@ -376,6 +383,886 @@ module PropelApi
376
383
  false
377
384
  end
378
385
 
386
+ # Smart dependency management - automatically fixes dependencies by default
387
+ def check_for_critical_dependencies
388
+ if options[:dry_run]
389
+ show_dry_run_preview
390
+ return
391
+ end
392
+
393
+ critical_dependencies = find_critical_dependencies
394
+
395
+ if critical_dependencies.any?
396
+ if options[:no_auto_fix]
397
+ show_dependency_solution(critical_dependencies)
398
+ else
399
+ # Default behavior: Auto-fix dependencies (Rails convention over configuration)
400
+ attempt_auto_fix(critical_dependencies)
401
+ end
402
+ end
403
+
404
+ # Note: Targeted fixture repair is handled by individual dependency fixes
405
+ # No need for system-wide fixture validation
406
+ end
407
+
408
+ def find_critical_dependencies
409
+ # Only look for actual breaking dependencies, not every mention
410
+ critical_deps = []
411
+
412
+ # 1. Look for has_many relationships pointing to this resource
413
+ Dir.glob("#{destination_root}/app/models/*.rb").each do |file|
414
+ content = File.read(file) rescue next
415
+ if content.match?(/has_many\s+:#{table_name}/)
416
+ critical_deps << {
417
+ type: :has_many_relationship,
418
+ file: file.gsub(destination_root + "/", ""),
419
+ issue: "has_many :#{table_name} relationship will break",
420
+ auto_fixable: true
421
+ }
422
+ end
423
+
424
+ # EXACT belongs_to detection - avoid false positives like :meeting_link_parent
425
+ # Only match EXACT association names: belongs_to :meeting (not :meeting_link_parent)
426
+ if content.match?(/belongs_to\s+:#{file_name}\b(?!_)/)
427
+ critical_deps << {
428
+ type: :belongs_to_relationship,
429
+ file: file.gsub(destination_root + "/", ""),
430
+ issue: "belongs_to :#{file_name} relationship will break when #{class_name} model is removed",
431
+ auto_fixable: true
432
+ }
433
+ end
434
+
435
+ # NEW: Model configuration dependencies (facets, validations, etc.)
436
+ if content.match?(/json_facet.*:#{file_name}/) || content.match?(/json_facet.*:#{file_name}_id/) || content.match?(/include:\s*\[.*:#{file_name}/)
437
+ critical_deps << {
438
+ type: :model_configuration,
439
+ file: file.gsub(destination_root + "/", ""),
440
+ issue: "Model configuration references #{file_name} in json_facet or other config",
441
+ auto_fixable: true
442
+ }
443
+ end
444
+ end
445
+
446
+ # 2. Look for test setup methods that reference this resource's fixtures
447
+ Dir.glob("#{destination_root}/test/**/*.rb").each do |file|
448
+ next if file.include?("#{file_name}_test.rb") # Skip the test we're destroying
449
+ next if file.include?("#{file_name}_controller_test.rb")
450
+ next if file.include?("#{file_name}_api_test.rb")
451
+
452
+ content = File.read(file) rescue next
453
+ # Look for fixture references: transcripts(:one), transcripts(:some_name)
454
+ if content.match?(/#{table_name}\(:[^)]+\)/)
455
+ critical_deps << {
456
+ type: :test_setup_dependency,
457
+ file: file.gsub(destination_root + "/", ""),
458
+ issue: "Test setup references #{table_name} fixtures which will no longer exist",
459
+ auto_fixable: true
460
+ }
461
+ end
462
+
463
+ # Use DRY utility for comprehensive test dependency detection
464
+ if test_reference_patterns(file_name).any? { |pattern| content.match?(Regexp.new(pattern)) }
465
+ critical_deps << {
466
+ type: :test_method_dependency,
467
+ file: file.gsub(destination_root + "/", ""),
468
+ issue: "Test methods reference destroyed #{file_name} fields/associations",
469
+ auto_fixable: true
470
+ }
471
+ end
472
+ end
473
+
474
+ # 3. Look for fixtures referencing this resource
475
+ Dir.glob("#{destination_root}/test/fixtures/*.yml").each do |file|
476
+ next if file.include?("#{table_name}.yml") # Skip the fixture we're destroying
477
+
478
+ content = File.read(file) rescue next
479
+ # Use DRY utility for exact fixture pattern detection
480
+ fixture_patterns = ["#{file_name}:", "#{file_name}_id:", "#{file_name}_type:"]
481
+ if fixture_patterns.any? { |pattern| content.include?(pattern) }
482
+ critical_deps << {
483
+ type: :fixture_reference,
484
+ file: file.gsub(destination_root + "/", ""),
485
+ issue: "Fixture references #{file_name} which will no longer exist",
486
+ auto_fixable: true
487
+ }
488
+ end
489
+
490
+ # NEW: Polymorphic fixture syntax: meeting_link_parent: one (Meeting)
491
+ if content.match?(/.*_parent:\s+\w+\s+\(#{class_name}\)/)
492
+ critical_deps << {
493
+ type: :polymorphic_fixture_reference,
494
+ file: file.gsub(destination_root + "/", ""),
495
+ issue: "Polymorphic fixture references #{class_name} which will no longer exist",
496
+ auto_fixable: true
497
+ }
498
+ end
499
+ end
500
+
501
+ # 4. Look for class constant references in test files (more common and safer to fix)
502
+ Dir.glob("#{destination_root}/test/**/*.rb").each do |file|
503
+ next if file.include?("#{file_name}_test.rb") # Skip the test we're destroying
504
+ next if file.include?("#{file_name}_controller_test.rb")
505
+ next if file.include?("#{file_name}_api_test.rb")
506
+
507
+ content = File.read(file) rescue next
508
+ # Look for is_a?(Agenda) patterns in assertions and polymorphic type checks
509
+ if content.match?(/\.is_a\?\(#{class_name}\)/)
510
+ critical_deps << {
511
+ type: :class_constant_in_test,
512
+ file: file.gsub(destination_root + "/", ""),
513
+ issue: "Test assertions reference #{class_name} class in polymorphic type checks",
514
+ auto_fixable: true
515
+ }
516
+ end
517
+ end
518
+
519
+ # 5. Look for direct class constant references (in models, controllers, serialization)
520
+ Dir.glob("#{destination_root}/app/**/*.rb").each do |file|
521
+ content = File.read(file) rescue next
522
+ # Look for direct class references: Agenda, ::Agenda, class_name: 'Agenda'
523
+ if content.match?(/\b#{class_name}\b/) && !content.match?(/class\s+#{class_name}/)
524
+ critical_deps << {
525
+ type: :class_constant_reference,
526
+ file: file.gsub(destination_root + "/", ""),
527
+ issue: "Code directly references #{class_name} class which will no longer exist",
528
+ auto_fixable: false # Too dangerous to auto-fix class references
529
+ }
530
+ end
531
+ end
532
+
533
+ # 6. Look for polymorphic parent configurations that mention this resource
534
+ Dir.glob("#{destination_root}/app/models/*.rb").each do |file|
535
+ content = File.read(file) rescue next
536
+ # Look for polymorphic parent type arrays or configurations
537
+ if content.match?(/#{class_name}/) && content.match?(/polymorphic|parent_types|commentable_types/)
538
+ critical_deps << {
539
+ type: :polymorphic_parent_config,
540
+ file: file.gsub(destination_root + "/", ""),
541
+ issue: "Polymorphic configuration references #{class_name} as valid parent type",
542
+ auto_fixable: true
543
+ }
544
+ end
545
+ end
546
+
547
+ # 7. Look for migration files that create references to this resource
548
+ Dir.glob("#{destination_root}/db/migrate/*.rb").each do |file|
549
+ content = File.read(file) rescue next
550
+ if content.match?(/t\.references\s+:#{file_name}/) || content.match?(/add_foreign_key.*#{table_name}/)
551
+ critical_deps << {
552
+ type: :migration_reference,
553
+ file: file.gsub(destination_root + "/", ""),
554
+ issue: "Migration creates references to #{table_name} table which will not exist",
555
+ auto_fixable: true # We can generate cleanup migrations
556
+ }
557
+ end
558
+ end
559
+
560
+ critical_deps
561
+ end
562
+
563
+ def show_dependency_solution(deps)
564
+ say "\n" + "="*80, :red
565
+ say "🚫 CANNOT SAFELY DESTROY #{class_name.upcase()}", :red
566
+ say "="*80, :red
567
+ say "\n💥 This would break #{deps.size} critical dependencies:", :red
568
+
569
+ deps.each do |dep|
570
+ say "\n 📁 #{dep[:file]}", :yellow
571
+ say " ❌ #{dep[:issue]}", :red
572
+ if dep[:auto_fixable]
573
+ say " ✅ Auto-fixable", :green
574
+ else
575
+ say " ⚠️ Requires manual fix", :yellow
576
+ end
577
+ end
578
+
579
+ auto_fixable_count = deps.count { |d| d[:auto_fixable] }
580
+
581
+ if auto_fixable_count > 0
582
+ say "\n🛠️ Suggested approach:", :blue
583
+ say " rails destroy propel_api:resource #{class_name}", :green
584
+ say " (Auto-fixes #{auto_fixable_count} of #{deps.size} dependencies by default)", :blue
585
+ end
586
+
587
+ say "\n💡 Preview first:", :blue
588
+ say " rails destroy propel_api:resource #{class_name} --dry-run", :cyan
589
+ say " (See exactly what would be destroyed and fixed)", :blue
590
+
591
+ say "\n" + "="*80, :red
592
+ exit 1
593
+ end
594
+
595
+ def attempt_auto_fix(deps)
596
+ say "\n🔧 Auto-fixing #{deps.size} dependencies...", :blue
597
+
598
+ deps.each do |dep|
599
+ if dep[:auto_fixable]
600
+ case dep[:type]
601
+ when :has_many_relationship
602
+ fix_has_many_relationship(dep)
603
+ when :belongs_to_relationship
604
+ fix_belongs_to_relationship(dep)
605
+ when :test_setup_dependency
606
+ fix_test_setup_dependency(dep)
607
+ when :class_constant_in_test
608
+ fix_class_constant_in_test(dep)
609
+ when :polymorphic_parent_config
610
+ fix_polymorphic_parent_config(dep)
611
+ when :migration_reference
612
+ fix_migration_reference(dep)
613
+ when :fixture_reference
614
+ fix_fixture_reference(dep)
615
+ when :model_configuration
616
+ fix_model_configuration(dep)
617
+ when :test_method_dependency
618
+ fix_test_method_dependency(dep)
619
+ when :polymorphic_fixture_reference
620
+ fix_polymorphic_fixture_reference(dep)
621
+ end
622
+ else
623
+ say "⚠️ Cannot auto-fix #{dep[:file]}: #{dep[:issue]}", :yellow
624
+ end
625
+ end
626
+ end
627
+
628
+ def fix_has_many_relationship(dep)
629
+ file_path = File.join(destination_root, dep[:file])
630
+ content = File.read(file_path)
631
+
632
+ # Much more precise pattern that preserves file structure
633
+ # Match entire line including leading whitespace but preserve surrounding newlines
634
+ patterns_to_try = [
635
+ # Pattern 1: Full line with newline
636
+ /^(\s*)has_many\s+:#{table_name}[^\n]*\n/,
637
+ # Pattern 2: Line without trailing newline (end of file)
638
+ /^(\s*)has_many\s+:#{table_name}[^\n]*$/,
639
+ # Pattern 3: Inline in comment (like our bug)
640
+ /(\s+)has_many\s+:#{table_name}[^\n]*/
641
+ ]
642
+
643
+ updated_content = content
644
+ removed = false
645
+
646
+ patterns_to_try.each do |pattern|
647
+ if updated_content.match?(pattern)
648
+ updated_content = updated_content.gsub(pattern) do |match|
649
+ # If it's an inline match, preserve the leading whitespace as a newline
650
+ if match.include?('#') && !match.start_with?('#')
651
+ # This is an inline comment case, replace with just newline
652
+ "\n"
653
+ else
654
+ # This is a full line, remove completely but preserve structure
655
+ ''
656
+ end
657
+ end
658
+ removed = true
659
+ break
660
+ end
661
+ end
662
+
663
+ if removed
664
+ # Clean up any resulting double newlines
665
+ updated_content = updated_content.gsub(/\n\n+/, "\n\n")
666
+ File.write(file_path, updated_content)
667
+ say " ✅ Removed has_many :#{table_name} from #{dep[:file]}", :green
668
+ else
669
+ say " ⚠️ Could not find has_many :#{table_name} in #{dep[:file]}", :yellow
670
+ end
671
+ end
672
+
673
+ def fix_belongs_to_relationship(dep)
674
+ file_path = File.join(destination_root, dep[:file])
675
+ content = File.read(file_path)
676
+
677
+ # Remove belongs_to relationships to the deleted resource
678
+ # belongs_to :meeting → (removed completely)
679
+ patterns_to_try = [
680
+ # Pattern 1: Full line with newline
681
+ /^(\s*)belongs_to\s+:#{file_name}[^\n]*\n/,
682
+ # Pattern 2: Line without trailing newline (end of file)
683
+ /^(\s*)belongs_to\s+:#{file_name}[^\n]*$/
684
+ ]
685
+
686
+ updated_content = content
687
+ removed = false
688
+
689
+ patterns_to_try.each do |pattern|
690
+ if updated_content.match?(pattern)
691
+ updated_content = updated_content.gsub(pattern, '')
692
+ removed = true
693
+ break
694
+ end
695
+ end
696
+
697
+ if removed
698
+ # Clean up any resulting double newlines
699
+ updated_content = updated_content.gsub(/\n\n+/, "\n\n")
700
+ File.write(file_path, updated_content)
701
+ say " ✅ Removed belongs_to :#{file_name} from #{dep[:file]}", :green
702
+ else
703
+ say " ⚠️ Could not find belongs_to :#{file_name} in #{dep[:file]}", :yellow
704
+ end
705
+ end
706
+
707
+ def fix_test_setup_dependency(dep)
708
+ file_path = File.join(destination_root, dep[:file])
709
+ content = File.read(file_path)
710
+ original_content = content.dup
711
+
712
+ # Smart semantic analysis: understand variable relationships
713
+ # Example: @agenda = agendas(:one); @section_parent = @agenda
714
+
715
+ changes_made = []
716
+ alternative_fixture = find_alternative_fixture_for_tests
717
+
718
+ # 1. Find what variables use the deleted resource (BROADER: any variable name)
719
+ # Match ANY variable assigned from destroyed fixtures: @any_name = meetings(:fixture)
720
+ resource_var_pattern = /@(\w+)\s*=\s*#{table_name}\([^)]+\)/
721
+ resource_variables = content.scan(resource_var_pattern).flatten
722
+
723
+ # 2. For each variable, find what other variables depend on it
724
+ dependent_variables = {}
725
+ resource_variables.each do |var_name|
726
+ # Look for: @section_parent = @agenda
727
+ dependent_pattern = /@(\w+)\s*=\s*@#{var_name}\b/
728
+ dependencies = content.scan(dependent_pattern).flatten
729
+ dependent_variables[var_name] = dependencies
730
+ end
731
+
732
+ # 3. Handle fixture assignments with proper Rails polymorphic pattern recognition
733
+ assignment_pattern = /^(\s*)(@\w+)\s*=\s*#{table_name}\([^)]+\)(.*?)$/
734
+
735
+ updated_content = content.gsub(assignment_pattern) do |match|
736
+ indentation = $1
737
+ variable_name = $2 # "@meeting" or "@agenda_parent" or "@commentable"
738
+ comment_part = $3 # " # was @meeting # Default polymorphic parent for tests"
739
+
740
+ # Rails polymorphic pattern recognition: _parent, _able, _ible suffixes
741
+ is_polymorphic = variable_name.match?(/_parent$|_able$|_ible$/)
742
+
743
+ if is_polymorphic && alternative_fixture
744
+ # Polymorphic association: Keep same variable name, change fixture
745
+ "#{indentation}#{variable_name} = #{alternative_fixture} # was #{table_name}(:one)#{comment_part}"
746
+ elsif alternative_fixture
747
+ # Direct association: Rename variable to match new fixture type and update assignment
748
+ # @meeting = meetings(:one) → @organization = organizations(:acme_org)
749
+ new_fixture_type = alternative_fixture.split('(').first # "organizations(:acme_org)" → "organizations"
750
+ new_variable_name = "@#{simple_singularize(new_fixture_type)}" # "@organization"
751
+ "#{indentation}#{new_variable_name} = #{alternative_fixture} # was #{variable_name} = #{table_name}(:one)#{comment_part}"
752
+ else
753
+ # No alternative available: Comment out entirely
754
+ "#{indentation}# #{variable_name} = #{table_name}(:one) - REMOVED: #{table_name} resource destroyed#{comment_part}"
755
+ end
756
+ end
757
+
758
+ if updated_content != content
759
+ changes_made << "handled #{table_name} fixture assignments (polymorphic vs direct)"
760
+ content = updated_content
761
+ end
762
+
763
+ # 4. Update dependent variables to use alternative fixtures
764
+ resource_variables.each do |var_name|
765
+ dependent_variables[var_name].each do |dependent_var|
766
+ if alternative_fixture
767
+ # @section_parent = @agenda → @section_parent = meetings(:one)
768
+ pattern = /(@#{dependent_var}\s*=\s*)@#{var_name}\b/
769
+ updated_content = content.gsub(pattern) do |match|
770
+ assignment_part = $1
771
+ "#{assignment_part}#{alternative_fixture} # was @#{var_name}"
772
+ end
773
+ if updated_content != content
774
+ changes_made << "updated @#{dependent_var} to use #{alternative_fixture.split('(').first}"
775
+ content = updated_content
776
+ end
777
+ else
778
+ # Remove the dependent assignment if no alternative available
779
+ updated_content = content.gsub(/^(\s*)@#{dependent_var}\s*=\s*@#{var_name}\b.*$\n?/, '')
780
+ if updated_content != content
781
+ changes_made << "removed @#{dependent_var} assignment"
782
+ content = updated_content
783
+ end
784
+ end
785
+ end
786
+ end
787
+
788
+ # 5. Remove any remaining references to the deleted variables
789
+ resource_variables.each do |var_name|
790
+ # Remove lines that call methods on the deleted variable
791
+ var_usage_pattern = /^.*@#{var_name}\..*$\n?/
792
+ updated_content = content.gsub(var_usage_pattern, '')
793
+ if updated_content != content
794
+ changes_made << "removed lines using @#{var_name}"
795
+ content = updated_content
796
+ end
797
+ end
798
+
799
+ # 6. Clean up formatting
800
+ updated_content = content.gsub(/\n\n+/, "\n\n")
801
+
802
+ if updated_content != original_content
803
+ File.write(file_path, updated_content)
804
+ say " ✅ Cleaned up #{dep[:file]} (#{changes_made.join(', ')})", :green
805
+ else
806
+ say " ⚠️ No changes needed in #{dep[:file]}", :blue
807
+ end
808
+ end
809
+
810
+ def find_alternative_fixture_for_tests
811
+ # Dynamically find available fixture files (works for any Rails app schema)
812
+ fixture_files = Dir.glob("#{destination_root}/test/fixtures/*.yml")
813
+
814
+ # Filter out the resource we're destroying and system fixtures
815
+ available_fixtures = fixture_files.map { |f| File.basename(f, '.yml') }
816
+ .reject { |name| name == table_name } # Skip the one we're destroying
817
+ .reject { |name| name.start_with?('noticed_') } # Skip notification system fixtures
818
+ .reject { |name| name == 'active_storage_blobs' || name == 'active_storage_attachments' } # Skip Active Storage
819
+
820
+ return nil if available_fixtures.empty?
821
+
822
+ # Prioritize by Rails naming patterns (generic approach)
823
+ # 1. Look for fixtures that commonly serve as polymorphic parents (by naming patterns)
824
+ organization_like = available_fixtures.find { |name| name.include?('organization') || name.include?('company') || name.include?('account') }
825
+ user_like = available_fixtures.find { |name| name.include?('user') || name.include?('person') || name.include?('member') }
826
+ project_like = available_fixtures.find { |name| name.include?('project') || name.include?('workspace') || name.include?('team') }
827
+
828
+ # Get the prioritized fixture file name
829
+ chosen_fixture_file = [organization_like, user_like, project_like].compact.first ||
830
+ available_fixtures.sort.first
831
+
832
+ return nil unless chosen_fixture_file
833
+
834
+ # CRITICAL FIX: Read the actual fixture file to find real fixture names
835
+ fixture_path = "#{destination_root}/test/fixtures/#{chosen_fixture_file}.yml"
836
+ begin
837
+ require 'yaml'
838
+ fixture_data = YAML.load_file(fixture_path) || {}
839
+ first_fixture_name = fixture_data.keys.first
840
+
841
+ return first_fixture_name ? "#{chosen_fixture_file}(:#{first_fixture_name})" : nil
842
+ rescue => e
843
+ # Fallback: assume :one exists (old behavior)
844
+ say " ⚠️ Could not read #{fixture_path}: #{e.message}. Using fallback.", :yellow
845
+ return "#{chosen_fixture_file}(:one)"
846
+ end
847
+ end
848
+
849
+ def fix_class_constant_in_test(dep)
850
+ file_path = File.join(destination_root, dep[:file])
851
+ content = File.read(file_path)
852
+ original_content = content.dup
853
+
854
+ # Remove this class from polymorphic type assertions
855
+ # assert(@obj.is_a?(Agenda) || @obj.is_a?(Meeting)) → assert(@obj.is_a?(Meeting))
856
+
857
+ changes_made = []
858
+
859
+ # Pattern 1: Remove from OR chains: @obj.section_parent.is_a?(Agenda) || @obj.is_a?(Other)
860
+ # Handle complex object paths like @section.section_parent.is_a?(Class)
861
+ or_pattern = /(\|\|\s*)?@[\w.]+\.is_a\?\(#{class_name}\)(\s*\|\|)?/
862
+ updated_content = content.gsub(or_pattern) do |match|
863
+ has_leading_or = $1.present?
864
+ has_trailing_or = $2.present?
865
+
866
+ if has_leading_or && has_trailing_or
867
+ # Middle of chain: || @obj.is_a?(Agenda) || → ||
868
+ ' || '
869
+ elsif has_leading_or
870
+ # End of chain: || @obj.is_a?(Agenda) → (remove)
871
+ ''
872
+ elsif has_trailing_or
873
+ # Start of chain: @obj.is_a?(Agenda) || → (remove)
874
+ ''
875
+ else
876
+ # Only item: @obj.is_a?(Agenda) → (remove completely)
877
+ ''
878
+ end
879
+ end
880
+
881
+ if updated_content != content
882
+ changes_made << "removed #{class_name} from polymorphic type assertions"
883
+ content = updated_content
884
+ end
885
+
886
+ # Pattern 2: Clean up malformed OR chains (|| ||, leading ||, trailing ||)
887
+ updated_content = content.gsub(/\|\|\s*\|\|/, '||') # Fix double ORs
888
+ updated_content = updated_content.gsub(/\(\s*\|\|/, '(') # Fix leading OR in parentheses
889
+ updated_content = updated_content.gsub(/\|\|\s*\)/, ')') # Fix trailing OR in parentheses
890
+
891
+ if updated_content != content
892
+ changes_made << "cleaned up assertion syntax"
893
+ content = updated_content
894
+ end
895
+
896
+ if changes_made.any?
897
+ File.write(file_path, content)
898
+ say " ✅ Fixed class constants in #{dep[:file]} (#{changes_made.join(', ')})", :green
899
+ else
900
+ say " ⚠️ Could not find class constant patterns in #{dep[:file]}", :yellow
901
+ end
902
+ end
903
+
904
+ def fix_polymorphic_parent_config(dep)
905
+ file_path = File.join(destination_root, dep[:file])
906
+ content = File.read(file_path)
907
+ original_content = content.dup
908
+
909
+ changes_made = []
910
+
911
+ # 1. Look for inclusion validations: validates :type, inclusion: { in: %w[Agenda Meeting] }
912
+ inclusion_pattern = /(validates\s+:\w+.*inclusion:\s*\{\s*in:\s*%w\[)([^\]]*)(.*)/
913
+ updated_content = content.gsub(inclusion_pattern) do |match|
914
+ prefix = $1
915
+ type_list = $2
916
+ suffix = $3
917
+
918
+ # Remove our class from the list
919
+ updated_list = type_list.split.reject { |type| type == class_name }.join(' ')
920
+
921
+ if updated_list != type_list
922
+ changes_made << "removed #{class_name} from validation inclusion list"
923
+ "#{prefix}#{updated_list}#{suffix}"
924
+ else
925
+ match # No change needed
926
+ end
927
+ end
928
+
929
+ # 2. Look for other array configurations that might list parent types
930
+ # VALID_PARENT_TYPES = %w[Agenda Meeting Project]
931
+ array_config_pattern = /(\w+\s*=\s*%w\[)([^\]]*)(.*)/
932
+ updated_content = content.gsub(array_config_pattern) do |match|
933
+ prefix = $1
934
+ type_list = $2
935
+ suffix = $3
936
+
937
+ if type_list.include?(class_name)
938
+ updated_list = type_list.split.reject { |type| type == class_name }.join(' ')
939
+ changes_made << "removed #{class_name} from parent type configuration"
940
+ "#{prefix}#{updated_list}#{suffix}"
941
+ else
942
+ match # No change needed
943
+ end
944
+ end
945
+
946
+ if updated_content != original_content
947
+ File.write(file_path, updated_content)
948
+ say " ✅ Updated polymorphic configurations in #{dep[:file]} (#{changes_made.join(', ')})", :green
949
+ else
950
+ say " ⚠️ Could not find polymorphic configurations to fix in #{dep[:file]}", :yellow
951
+ say " Look for #{class_name} in parent type lists and remove it manually", :cyan
952
+ end
953
+ end
954
+
955
+ def fix_migration_reference(dep)
956
+ # Generate a cleanup migration to remove foreign key constraints
957
+ # This is safer than editing existing migration files
958
+ migration_file = dep[:file]
959
+ table_with_reference = extract_table_name_from_migration_file(migration_file)
960
+
961
+ if table_with_reference
962
+ # Generate cleanup migration with CURRENT timestamp (Rails-standard sequential approach)
963
+ # This runs AFTER problematic migrations and cleans up orphaned foreign keys
964
+ base_timestamp = Time.current.utc.strftime("%Y%m%d%H%M%S")
965
+ timestamp = generate_unique_timestamp(base_timestamp)
966
+ cleanup_migration_name = "RemoveOrphanedForeignKeyFrom#{table_with_reference.classify}To#{class_name.pluralize}"
967
+ cleanup_migration_file = "#{timestamp}_#{cleanup_migration_name.underscore}.rb"
968
+ cleanup_migration_path = "db/migrate/#{cleanup_migration_file}"
969
+
970
+ cleanup_content = <<~MIGRATION
971
+ class #{cleanup_migration_name} < ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]
972
+ def up
973
+ # Rails-standard sequential cleanup: Remove orphaned foreign key references
974
+ # This runs AFTER creation migrations that may have failed due to missing #{table_name} table
975
+
976
+ # Remove foreign key constraint if it exists (may not exist if creation migration failed)
977
+ if foreign_key_exists?(:#{table_with_reference}, :#{table_name})
978
+ remove_foreign_key :#{table_with_reference}, :#{table_name}
979
+ say "✅ Removed foreign key constraint from #{table_with_reference} to #{table_name}"
980
+ else
981
+ say "ℹ️ No foreign key constraint found from #{table_with_reference} to #{table_name} (may have failed during creation)"
982
+ end
983
+
984
+ # Remove the #{file_name}_id column if it exists
985
+ if column_exists?(:#{table_with_reference}, :#{file_name}_id)
986
+ remove_column :#{table_with_reference}, :#{file_name}_id
987
+ say "✅ Removed #{file_name}_id column from #{table_with_reference}"
988
+ else
989
+ say "ℹ️ No #{file_name}_id column found in #{table_with_reference}"
990
+ end
991
+ end
992
+
993
+ def down
994
+ # Cannot reverse removal of orphaned foreign key references
995
+ raise ActiveRecord::IrreversibleMigration, "Cannot restore foreign key to deleted #{table_name} table"
996
+ end
997
+ end
998
+ MIGRATION
999
+
1000
+ File.write(File.join(destination_root, cleanup_migration_path), cleanup_content)
1001
+ say " ✅ Generated cleanup migration: #{cleanup_migration_file}", :green
1002
+ say " Rails-standard approach: Cleans up orphaned #{file_name}_id references", :blue
1003
+ else
1004
+ say " ⚠️ Could not determine table name from #{migration_file}", :yellow
1005
+ end
1006
+ end
1007
+
1008
+ def extract_table_name_from_migration_file(migration_file)
1009
+ # Extract table name from migration file name: create_action_item_reports.rb → action_item_reports
1010
+ basename = File.basename(migration_file, '.rb')
1011
+ if basename.match?(/create_(\w+)/)
1012
+ basename.match(/create_(\w+)/)[1]
1013
+ else
1014
+ nil
1015
+ end
1016
+ end
1017
+
1018
+ def generate_unique_timestamp(base_timestamp)
1019
+ # Ensure we generate a unique 14-digit timestamp by incrementing seconds if needed
1020
+ timestamp = base_timestamp
1021
+ counter = 0
1022
+
1023
+ while Dir.glob("#{destination_root}/db/migrate/#{timestamp}_*.rb").any?
1024
+ # Increment timestamp by 1 second to avoid collision
1025
+ time = Time.strptime(base_timestamp, "%Y%m%d%H%M%S")
1026
+ timestamp = (time + counter + 1).utc.strftime("%Y%m%d%H%M%S")
1027
+ counter += 1
1028
+
1029
+ # Safety check to prevent infinite loop
1030
+ break if counter > 60
1031
+ end
1032
+
1033
+ timestamp
1034
+ end
1035
+
1036
+ def fix_fixture_reference(dep)
1037
+ file_path = File.join(destination_root, dep[:file])
1038
+ content = File.read(file_path)
1039
+ original_content = content.dup
1040
+
1041
+ changes_made = []
1042
+
1043
+ # Parse and fix YAML fixture file using simple, direct approach
1044
+ begin
1045
+ require 'yaml'
1046
+
1047
+ # Load YAML while preserving structure and comments
1048
+ yaml_data = YAML.load(content) || {}
1049
+
1050
+ # Direct removal of destroyed resource references (simplified approach)
1051
+ yaml_data.each do |fixture_name, fixture_data|
1052
+ next unless fixture_data.is_a?(Hash)
1053
+
1054
+ # Use DRY utility for exact fixture key cleanup
1055
+ [file_name, "#{file_name}_id", "#{file_name}_type"].each do |exact_key|
1056
+ if fixture_data.key?(exact_key)
1057
+ fixture_data.delete(exact_key)
1058
+ changes_made << "removed #{exact_key} reference from #{fixture_name}"
1059
+ end
1060
+ end
1061
+ end
1062
+
1063
+ if changes_made.any?
1064
+ # Write back to file while preserving YAML structure
1065
+ updated_content = YAML.dump(yaml_data)
1066
+
1067
+ # Preserve original file header if it exists
1068
+ if content.start_with?('# Read about fixtures')
1069
+ header_lines = content.lines.take_while { |line| line.strip.start_with?('#') || line.strip.empty? }
1070
+ updated_content = header_lines.join + "\n" + updated_content
1071
+ end
1072
+
1073
+ File.write(file_path, updated_content)
1074
+ say " ✅ Fixed fixture references in #{dep[:file]} (#{changes_made.join(', ')})", :green
1075
+ else
1076
+ say " ℹ️ No #{file_name} references found in #{dep[:file]}", :blue
1077
+ end
1078
+
1079
+ rescue => e
1080
+ # Fallback to manual review if YAML parsing fails
1081
+ say " ⚠️ Could not auto-fix fixture file #{dep[:file]}: #{e.message}", :yellow
1082
+ say " Please manually remove '#{file_name}:' and '#{file_name}_id:' references", :cyan
1083
+ end
1084
+ end
1085
+
1086
+ def get_valid_fixture_keys_for_model(model_class)
1087
+ # Get valid fixture keys: database columns + Rails association names
1088
+ valid_keys = []
1089
+
1090
+ # 1. Add all actual database columns
1091
+ valid_keys += model_class.columns.map(&:name)
1092
+
1093
+ # 2. Add Rails association names (fixtures can reference associations)
1094
+ # belongs_to :organization allows fixture to use: organization: fixture_name
1095
+ model_class.reflect_on_all_associations.each do |association|
1096
+ # Only include associations where the target model actually exists
1097
+ begin
1098
+ association.class_name.constantize # Check if target model exists
1099
+ valid_keys << association.name.to_s
1100
+ rescue NameError
1101
+ # Target model doesn't exist - this IS an invalid reference
1102
+ # Don't include it in valid_keys so it gets removed
1103
+ end
1104
+ end
1105
+
1106
+ valid_keys.uniqnt w
1107
+ end
1108
+
1109
+ def show_dry_run_preview
1110
+ say "\n" + "="*70, :blue
1111
+ say "🔍 DRY RUN: What would be destroyed", :blue
1112
+ say "="*70, :blue
1113
+
1114
+ files_to_destroy = [
1115
+ "app/models/#{file_name}.rb",
1116
+ "app/controllers/#{controller_path}/#{controller_file_name}.rb",
1117
+ "test/models/#{file_name}_test.rb",
1118
+ "test/controllers/#{controller_path}/#{controller_file_name}_test.rb",
1119
+ "test/integration/#{file_name}_api_test.rb",
1120
+ "test/fixtures/#{table_name}.yml"
1121
+ ]
1122
+
1123
+ say "\n📁 Files that would be removed:", :green
1124
+ files_to_destroy.each { |file| say " ❌ #{file}", :red }
1125
+
1126
+ critical_deps = find_critical_dependencies
1127
+ if critical_deps.any?
1128
+ say "\n💥 Dependencies that would break:", :red
1129
+ critical_deps.each do |dep|
1130
+ say " 📁 #{dep[:file]} - #{dep[:issue]}", :yellow
1131
+ end
1132
+ else
1133
+ say "\n✅ No critical dependencies found - safe to destroy", :green
1134
+ end
1135
+
1136
+ say "\n" + "="*70, :blue
1137
+ exit 0
1138
+ end
1139
+
1140
+ # Database migration management methods - available to all generators
1141
+ def generate_removal_migration
1142
+ # Generate a proper Rails migration to remove the table and foreign keys
1143
+ # This follows Rails conventions and maintains database integrity
1144
+
1145
+ # Use current timestamp - Rails-standard sequential approach
1146
+ base_timestamp = Time.current.utc.strftime("%Y%m%d%H%M%S")
1147
+ timestamp = generate_unique_timestamp(base_timestamp)
1148
+ migration_name = "Remove#{class_name.pluralize}Table"
1149
+ migration_file = "#{timestamp}_#{migration_name.underscore}.rb"
1150
+ migration_path = "db/migrate/#{migration_file}"
1151
+
1152
+ # Check if table exists in database before generating removal migration
1153
+ if table_exists_in_database?
1154
+ say "Generating removal migration for #{class_name}...", :blue
1155
+
1156
+ migration_content = build_removal_migration_content(migration_name)
1157
+ # Use File.write instead of create_file to avoid Rails auto-reversal
1158
+ File.write(File.join(destination_root, migration_path), migration_content)
1159
+
1160
+ say "✅ Generated removal migration: #{migration_file}", :green
1161
+ say "💡 Run 'rails db:migrate' to execute the removal", :blue
1162
+ else
1163
+ say "Table #{table_name} doesn't exist in database - skipping removal migration", :yellow
1164
+ say "💡 Note: If you have unused creation migrations, remove them manually when safe", :blue
1165
+ end
1166
+ end
1167
+
1168
+ def table_exists_in_database?
1169
+ return false unless defined?(ActiveRecord::Base)
1170
+
1171
+ begin
1172
+ ActiveRecord::Base.connection.table_exists?(table_name)
1173
+ rescue => e
1174
+ # If we can't check (database not connected, etc.), assume it doesn't exist
1175
+ false
1176
+ end
1177
+ end
1178
+
1179
+ def build_removal_migration_content(migration_name)
1180
+ # Note: Don't duplicate foreign key removal here since cleanup migrations handle it
1181
+ # The cleanup migrations generated by fix_migration_reference already remove foreign keys safely
1182
+
1183
+ content = <<~MIGRATION
1184
+ class #{migration_name} < ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]
1185
+ def up
1186
+ # Drop the #{table_name} table (foreign keys already removed by cleanup migrations)
1187
+ drop_table :#{table_name} if table_exists?(:#{table_name})
1188
+ end
1189
+
1190
+ def down
1191
+ # Note: This is a destructive migration.
1192
+ # Reversing it would require recreating the original table structure
1193
+ # and data, which is beyond the scope of automated generation.
1194
+ raise ActiveRecord::IrreversibleMigration, "Cannot reverse removal of #{table_name}"
1195
+ end
1196
+ end
1197
+ MIGRATION
1198
+
1199
+ content
1200
+ end
1201
+
1202
+ def find_foreign_keys_to_table
1203
+ return [] unless defined?(ActiveRecord::Base)
1204
+
1205
+ begin
1206
+ # Find all foreign keys that point to our table
1207
+ foreign_keys = []
1208
+
1209
+ ActiveRecord::Base.connection.tables.each do |table|
1210
+ next if table == table_name # Skip our own table
1211
+
1212
+ ActiveRecord::Base.connection.foreign_keys(table).each do |fk|
1213
+ if fk.to_table == table_name
1214
+ foreign_keys << {
1215
+ from_table: table,
1216
+ to_table: table_name,
1217
+ column: fk.column
1218
+ }
1219
+ end
1220
+ end
1221
+ end
1222
+
1223
+ foreign_keys
1224
+ rescue => e
1225
+ # If we can't introspect the database, return empty array
1226
+ []
1227
+ end
1228
+ end
1229
+
1230
+ def show_destroy_completion_message
1231
+ say "\n" + "="*70, :red
1232
+ say "🗑️ Resource Destroyed Successfully!", :red
1233
+ say "="*70, :red
1234
+
1235
+ say "\n📦 Files removed:", :red
1236
+ removed_files = [
1237
+ "📄 Model: app/models/#{file_name}.rb",
1238
+ "🎮 Controller: app/controllers/#{controller_path}/#{controller_file_name}.rb",
1239
+ "🧪 Tests: test/models/#{file_name}_test.rb and related",
1240
+ "📋 Fixtures: test/fixtures/#{table_name}.yml",
1241
+ "🌱 Seeds: db/seeds/#{table_name}_seeds.rb"
1242
+ ]
1243
+ removed_files.each { |file| say " #{file}", :red }
1244
+
1245
+ if table_exists_in_database?
1246
+ say "\n🗄️ Database removal:", :blue
1247
+ say " 📋 Generated removal migration for #{table_name} table", :blue
1248
+ say " 🧹 Generated cleanup migrations for orphaned foreign keys", :blue
1249
+ say " ⚙️ Run 'rails db:migrate' to execute removal and cleanup", :yellow
1250
+ else
1251
+ say "\n🗄️ Database cleanup:", :blue
1252
+ say " 🧹 Generated cleanup migrations for orphaned foreign key references", :blue
1253
+ say " ⚙️ Run 'rails db:migrate' to clean up broken references", :yellow
1254
+ say " 💡 This follows Rails-standard sequential migration approach", :cyan
1255
+ end
1256
+
1257
+ say "\n💡 Next steps (Rails-standard workflow):", :blue
1258
+ say " 1. rails db:migrate (apply cleanup migrations)", :yellow
1259
+ say " 2. Check for any manual customizations to remove", :blue
1260
+ say " 3. Manually remove old migration files when safe", :cyan
1261
+ say " (#{class_name} creation migration can be removed after cleanup)"
1262
+
1263
+ say "="*70, :red
1264
+ end
1265
+
379
1266
  # Route management helper methods
380
1267
  def insert_routes
381
1268
  routes_file = File.join(destination_root, "config/routes.rb")
@@ -527,6 +1414,11 @@ module PropelApi
527
1414
  @attributes.any? { |attr| attr.name == 'agency' && attr.type == :references }
528
1415
  end
529
1416
 
1417
+ def has_agent_reference?
1418
+ return false unless defined?(@attributes) && @attributes
1419
+ @attributes.any? { |attr| attr.name == 'agent' && attr.type == :references }
1420
+ end
1421
+
530
1422
  def has_user_reference?
531
1423
  return false unless defined?(@attributes) && @attributes
532
1424
  @attributes.any? { |attr| (attr.name == 'user' && attr.type == :references) || attr.name == 'user_id' }
@@ -592,12 +1484,74 @@ module PropelApi
592
1484
  type_field: poly_assoc[:type_field],
593
1485
  parent_type: parent_type,
594
1486
  parent_table: parent_type.underscore.pluralize,
595
- fixture_name: fixture_name
1487
+ fixture_name: fixture_name,
1488
+ fixture_id: fixture_index + 1 # Rails standard: fixtures one=1, two=2, three=3
596
1489
  }
597
1490
  end
598
1491
 
599
1492
  private
600
1493
 
1494
+ # === REUSABLE REGEX UTILITIES ===
1495
+ # DRY: Common pattern matching used across multiple fix methods
1496
+
1497
+ def exact_field_patterns(field_name)
1498
+ # EXACT symbol matches only (prevents false positives like :meeting_link_parent)
1499
+ [
1500
+ ":#{field_name}_id\\b", # :meeting_id (exact symbol)
1501
+ ":#{field_name}\\b(?!\\w)", # :meeting (exact, not :meeting_history)
1502
+ ":#{field_name}_type\\b" # :meeting_type (polymorphic)
1503
+ ]
1504
+ end
1505
+
1506
+ def test_reference_patterns(field_name)
1507
+ # Common test reference patterns across all test types
1508
+ [
1509
+ "\\.#{field_name}_id\\b", # @obj.meeting_id
1510
+ "\\.#{field_name}\\s*=", # @obj.meeting = nil
1511
+ "\\.#{field_name}\\(", # @obj.meeting()
1512
+ ":#{field_name}\\)", # assert_respond_to(@obj, :meeting)
1513
+ "#{field_name}:", # meeting: @meeting (test data)
1514
+ "#{field_name}_id:", # meeting_id: 123 (test data)
1515
+ "\"#{field_name}\"", # "meeting" (string expectations)
1516
+ "'#{field_name}'", # 'meeting' (string expectations)
1517
+ "assert_includes.*#{field_name}", # assert_includes(..., "meeting")
1518
+ ]
1519
+ end
1520
+
1521
+ def apply_content_replacements(content, field_name, class_name)
1522
+ # DRY: Common content replacement logic used across fix methods
1523
+ changes_made = []
1524
+
1525
+ # Field access cleanup
1526
+ if content.gsub!(/.*\.#{field_name}_id\b.*\n/, " # #{field_name}_id field removed when #{class_name} destroyed\n")
1527
+ changes_made << "removed #{field_name}_id field access"
1528
+ end
1529
+
1530
+ if content.gsub!(/.*\.#{field_name}\s*=.*\n/, " # #{field_name} assignment removed when #{class_name} destroyed\n")
1531
+ changes_made << "removed #{field_name} assignment"
1532
+ end
1533
+
1534
+ # Assertion cleanup
1535
+ if content.gsub!(/.*assert_respond_to.*:#{field_name}\b.*\n/, " # #{field_name} assertion removed when #{class_name} destroyed\n")
1536
+ changes_made << "removed #{field_name} assertion"
1537
+ end
1538
+
1539
+ if content.gsub!(/.*assert_includes.*["']#{field_name}["'].*\n/, " # #{field_name} expectation removed when #{class_name} destroyed\n")
1540
+ changes_made << "removed #{field_name} facet expectation"
1541
+ end
1542
+
1543
+ # Test data cleanup
1544
+ if content.gsub!(/.*#{field_name}:.*\n/, " # #{field_name}: removed when #{class_name} destroyed\n")
1545
+ changes_made << "removed #{field_name} test data"
1546
+ end
1547
+
1548
+ if content.gsub!(/.*#{field_name}_id:.*\n/, " # #{field_name}_id: removed when #{class_name} destroyed\n")
1549
+ changes_made << "removed #{field_name}_id test data"
1550
+ end
1551
+
1552
+ changes_made
1553
+ end
1554
+
601
1555
  def parse_polymorphic_parent_types
602
1556
  return {} unless defined?(options) && options[:parents]
603
1557
 
@@ -624,5 +1578,173 @@ module PropelApi
624
1578
  # Ensure we always return at least one model as fallback
625
1579
  models.empty? ? [] : models
626
1580
  end
1581
+
1582
+ # DRY: Fix model configuration using reusable utilities
1583
+ def fix_model_configuration(dep)
1584
+ file_path = File.join(destination_root, dep[:file])
1585
+ content = File.read(file_path)
1586
+ original_content = content.dup
1587
+
1588
+ changes_made = []
1589
+
1590
+ # Use DRY utility for exact field pattern matching
1591
+ exact_field_patterns(file_name).each do |pattern|
1592
+ # Apply pattern with word boundaries to prevent partial matches
1593
+ regex_pattern = /#{pattern},?\s*/
1594
+ updated_content = content.gsub(regex_pattern, '')
1595
+
1596
+ if updated_content != content
1597
+ changes_made << "removed field reference"
1598
+ content = updated_content
1599
+ end
1600
+ end
1601
+
1602
+ # Clean up trailing commas and empty arrays
1603
+ content = content.gsub(/,\s*\]/, ']').gsub(/\[\s*,/, '[').gsub(/,\s*,/, ',')
1604
+
1605
+ if content != original_content
1606
+ File.write(file_path, content)
1607
+ say " ✅ Fixed model configuration in #{dep[:file]} (#{changes_made.join(', ')})", :green
1608
+ else
1609
+ say " ℹ️ No #{file_name} configuration found in #{dep[:file]}", :blue
1610
+ end
1611
+ end
1612
+
1613
+ # DRY: Comprehensive test cleanup using reusable utilities
1614
+ def fix_test_method_dependency(dep)
1615
+ file_path = File.join(destination_root, dep[:file])
1616
+ content = File.read(file_path)
1617
+ original_content = content.dup
1618
+
1619
+ changes_made = []
1620
+
1621
+ # 1. Remove test methods that test destroyed associations (IMPROVED: proper method structure)
1622
+ # Match complete test method from declaration to matching end with proper indentation
1623
+ test_method_pattern = /^(\s*)test\s+"[^"]*#{file_name}[^"]*"\s+do\s*$.*?^\1end\s*$/m
1624
+
1625
+ updated_content = content.gsub(test_method_pattern) do |match|
1626
+ indentation = $1 || " " # Preserve original indentation
1627
+ changes_made << "removed test method testing #{file_name} association"
1628
+ "#{indentation}# Test removed: #{file_name} association no longer exists after resource destruction\n"
1629
+ end
1630
+ content = updated_content if updated_content != content
1631
+
1632
+ # 2. Apply common content replacements using DRY utility
1633
+ replacement_changes = apply_content_replacements(content, file_name, class_name)
1634
+ changes_made.concat(replacement_changes)
1635
+
1636
+ if content != original_content
1637
+ File.write(file_path, content)
1638
+ say " ✅ Enhanced test cleanup in #{dep[:file]} (#{changes_made.join(', ')})", :green
1639
+ else
1640
+ say " ℹ️ No #{file_name} test dependencies found in #{dep[:file]}", :blue
1641
+ end
1642
+ end
1643
+
1644
+ # NEW: Fix polymorphic fixture references (parent: one (DestroyedClass))
1645
+ def fix_polymorphic_fixture_reference(dep)
1646
+ file_path = File.join(destination_root, dep[:file])
1647
+ content = File.read(file_path)
1648
+ original_content = content.dup
1649
+
1650
+ changes_made = []
1651
+
1652
+ begin
1653
+ require 'yaml'
1654
+
1655
+ # Load YAML while preserving structure and comments
1656
+ yaml_data = YAML.load(content) || {}
1657
+
1658
+ # Find and replace polymorphic references while preserving Rails syntax
1659
+ changes_to_apply = []
1660
+ alternative_fixture = find_alternative_fixture_for_tests
1661
+
1662
+ yaml_data.each do |fixture_name, fixture_data|
1663
+ next unless fixture_data.is_a?(Hash)
1664
+
1665
+ # Look for polymorphic patterns: some_parent: one (DestroyedClass)
1666
+ fixture_data.each do |key, value|
1667
+ if key.end_with?('_parent') && value.to_s.include?(class_name)
1668
+ changes_to_apply << {
1669
+ fixture_name: fixture_name,
1670
+ key: key,
1671
+ old_value: value.to_s,
1672
+ new_value: alternative_fixture ? convert_to_polymorphic_syntax(alternative_fixture) : nil
1673
+ }
1674
+ end
1675
+ end
1676
+ end
1677
+
1678
+ # Apply changes safely - preserve Rails polymorphic syntax format
1679
+ changes_to_apply.each do |change|
1680
+ fixture_data = yaml_data[change[:fixture_name]]
1681
+
1682
+ if change[:new_value]
1683
+ # Replace with alternative while preserving Rails format: parent_name (Type)
1684
+ fixture_data[change[:key]] = change[:new_value]
1685
+ changes_made << "replaced #{change[:key]} with #{change[:new_value]} in #{change[:fixture_name]}"
1686
+ else
1687
+ # Comment out if no alternative available
1688
+ fixture_data.delete(change[:key])
1689
+ changes_made << "removed #{change[:key]} reference from #{change[:fixture_name]} (no alternative available)"
1690
+ end
1691
+ end
1692
+
1693
+ if changes_made.any?
1694
+ # Write back to file while preserving YAML structure
1695
+ updated_content = YAML.dump(yaml_data)
1696
+
1697
+ # Preserve original file header if it exists
1698
+ if content.start_with?('# Read about fixtures')
1699
+ header_lines = content.lines.take_while { |line| line.strip.start_with?('#') || line.strip.empty? }
1700
+ updated_content = header_lines.join + "\n" + updated_content
1701
+ end
1702
+
1703
+ File.write(file_path, updated_content)
1704
+ say " ✅ Fixed polymorphic fixture references in #{dep[:file]} (#{changes_made.join(', ')})", :green
1705
+ else
1706
+ say " ℹ️ No polymorphic #{class_name} references found in #{dep[:file]}", :blue
1707
+ end
1708
+
1709
+ rescue => e
1710
+ say " ⚠️ Could not auto-fix polymorphic fixture #{dep[:file]}: #{e.message}", :yellow
1711
+ say " Please manually review polymorphic references to #{class_name}", :cyan
1712
+ end
1713
+ end
1714
+
1715
+ private
1716
+
1717
+ def simple_singularize(word)
1718
+ # Simple singularization for common Rails patterns (fallback if ActiveSupport not available)
1719
+ return word.singularize if word.respond_to?(:singularize)
1720
+
1721
+ # Basic singularization rules
1722
+ case word
1723
+ when /ies$/
1724
+ word.sub(/ies$/, 'y')
1725
+ when /s$/
1726
+ word.sub(/s$/, '')
1727
+ else
1728
+ word
1729
+ end
1730
+ end
1731
+
1732
+ def convert_to_polymorphic_syntax(alternative_fixture)
1733
+ # Convert "organizations(:acme_org)" to "acme_org (Organization)"
1734
+ # Extract fixture type and name using regex
1735
+ if match = alternative_fixture.match(/^(\w+)\(:(\w+)\)$/)
1736
+ fixture_type = match[1] # "organizations"
1737
+ fixture_name = match[2] # "acme_org"
1738
+
1739
+ # Singularize and capitalize type
1740
+ singular_type = simple_singularize(fixture_type).capitalize
1741
+
1742
+ # Return Rails polymorphic format
1743
+ "#{fixture_name} (#{singular_type})"
1744
+ else
1745
+ # Fallback if pattern doesn't match
1746
+ alternative_fixture
1747
+ end
1748
+ end
627
1749
  end
628
1750
  end