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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/lib/generators/propel_api/controller/controller_generator.rb +13 -7
- data/lib/generators/propel_api/core/named_base.rb +1150 -28
- data/lib/generators/propel_api/resource/resource_generator.rb +173 -61
- data/lib/generators/propel_api/templates/scaffold/facet_model_template.rb.tt +10 -0
- data/lib/generators/propel_api/templates/tests/controller_test_template.rb.tt +36 -6
- data/lib/generators/propel_api/templates/tests/fixtures_template.yml.tt +6 -0
- data/lib/generators/propel_api/templates/tests/integration_test_template.rb.tt +77 -25
- data/lib/generators/propel_api/templates/tests/model_test_template.rb.tt +17 -5
- data/lib/propel_api.rb +1 -1
- metadata +2 -2
@@ -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
|
-
|
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
|
-
#
|
256
|
-
|
257
|
-
|
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
|
-
|
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
|