blast_radius 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlastRadius
4
+ class Node
5
+ attr_reader :model_class, :association_name, :dependent_type,
6
+ :association_type, :children, :parent, :depth,
7
+ :through, :polymorphic
8
+
9
+ def initialize(model_class, options = {})
10
+ @model_class = model_class
11
+ @association_name = options[:association_name]
12
+ @dependent_type = options[:dependent_type]
13
+ @association_type = options[:association_type]
14
+ @through = options[:through]
15
+ @polymorphic = options[:polymorphic] || false
16
+ @parent = options[:parent]
17
+ @depth = options[:depth] || 0
18
+ @children = []
19
+ end
20
+
21
+ def add_child(child_node)
22
+ @children << child_node
23
+ end
24
+
25
+ # Check if this node creates a circular reference
26
+ # @return [Boolean] true if circular reference detected
27
+ def circular_reference?
28
+ ancestors.any? { |ancestor| ancestor.model_class == @model_class }
29
+ end
30
+
31
+ # Get all ancestor nodes
32
+ # @return [Array<Node>] array of ancestor nodes
33
+ def ancestors
34
+ return [] unless @parent
35
+
36
+ [@parent] + @parent.ancestors
37
+ end
38
+
39
+ # Get model name as string
40
+ # @return [String] model class name
41
+ def model_name
42
+ @model_class.name
43
+ end
44
+
45
+ # Check if this is the root node
46
+ # @return [Boolean] true if root node
47
+ def root?
48
+ @parent.nil?
49
+ end
50
+
51
+ # Check if this is a leaf node
52
+ # @return [Boolean] true if leaf node
53
+ def leaf?
54
+ @children.empty?
55
+ end
56
+
57
+ # Get the path from root to this node
58
+ # @return [Array<String>] array of model names
59
+ def path
60
+ return [model_name] if root?
61
+
62
+ @parent.path + [model_name]
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module BlastRadius
6
+ class Railtie < Rails::Railtie
7
+ railtie_name :blast_radius
8
+
9
+ rake_tasks do
10
+ load 'tasks/blast_radius.rake'
11
+ end
12
+
13
+ # Add ActiveRecord extensions
14
+ initializer 'blast_radius.active_record' do
15
+ ActiveSupport.on_load(:active_record) do
16
+ extend BlastRadius::ActiveRecordExtension::ClassMethods
17
+ include BlastRadius::ActiveRecordExtension::InstanceMethods
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlastRadius
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'blast_radius/version'
4
+ require_relative 'blast_radius/configuration'
5
+ require_relative 'blast_radius/analyzer'
6
+ require_relative 'blast_radius/node'
7
+ require_relative 'blast_radius/dependency_tree'
8
+ require_relative 'blast_radius/impact_calculator'
9
+ require_relative 'blast_radius/formatters/base'
10
+ require_relative 'blast_radius/formatters/text_formatter'
11
+ require_relative 'blast_radius/formatters/mermaid_formatter'
12
+ require_relative 'blast_radius/formatters/json_formatter'
13
+ require_relative 'blast_radius/formatters/dot_formatter'
14
+ require_relative 'blast_radius/formatters/html_formatter'
15
+ require_relative 'blast_radius/active_record_extension'
16
+
17
+ # Load Railtie if Rails is available
18
+ require_relative 'blast_radius/railtie' if defined?(Rails::Railtie)
19
+
20
+ module BlastRadius
21
+ class Error < StandardError; end
22
+
23
+ class << self
24
+ attr_writer :configuration
25
+
26
+ def configuration
27
+ @configuration ||= Configuration.new
28
+ end
29
+
30
+ def configure
31
+ yield(configuration)
32
+ end
33
+
34
+ def reset_configuration
35
+ @configuration = Configuration.new
36
+ end
37
+
38
+ # Analyze a model class and return dependency tree
39
+ # @param model_class [Class] ActiveRecord model class
40
+ # @param options [Hash] options for analysis
41
+ # @return [Node] root node of dependency tree
42
+ def analyze(model_class, options = {})
43
+ tree = DependencyTree.new(configuration)
44
+ tree.build(model_class, options)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :blast_radius do
4
+ desc 'Visualize dependency tree for all models or a specific model'
5
+ task :visualize, %i[model_name format] => :environment do |_t, args|
6
+ require 'blast_radius'
7
+
8
+ format = (args[:format] || ENV['FORMAT'] || 'text').to_sym
9
+
10
+ if args[:model_name]
11
+ # Visualize specific model
12
+ begin
13
+ model_class = args[:model_name].constantize
14
+ output = model_class.blast_radius(format: format)
15
+ puts output
16
+ rescue NameError
17
+ puts "Error: Model '#{args[:model_name]}' not found"
18
+ exit 1
19
+ end
20
+ else
21
+ # Visualize all models
22
+ analyzer = BlastRadius::Analyzer.new
23
+ models = analyzer.all_models
24
+
25
+ puts "Found #{models.size} models:"
26
+ puts
27
+
28
+ models.each do |model|
29
+ puts "=== #{model.name} ==="
30
+ output = model.blast_radius(format: format)
31
+ puts output
32
+ puts
33
+ end
34
+ end
35
+ end
36
+
37
+ desc 'Calculate the impact of deleting a specific record'
38
+ task :impact, %i[model_name record_id] => :environment do |_t, args|
39
+ require 'blast_radius'
40
+
41
+ unless args[:model_name] && args[:record_id]
42
+ puts 'Usage: rake blast_radius:impact[ModelName,record_id]'
43
+ exit 1
44
+ end
45
+
46
+ begin
47
+ model_class = args[:model_name].constantize
48
+ record = model_class.find(args[:record_id])
49
+
50
+ calculator = BlastRadius::ImpactCalculator.new
51
+ impact = calculator.dry_run(record)
52
+
53
+ puts "Impact of deleting #{model_class.name}##{record.id}:"
54
+ puts
55
+
56
+ if impact.empty?
57
+ puts 'No dependent records would be affected.'
58
+ else
59
+ total_count = impact.values.sum { |v| v[:count] }
60
+ puts "Total records affected: #{total_count}"
61
+ puts
62
+
63
+ impact.each do |model_name, info|
64
+ puts "#{model_name}:"
65
+ puts " Count: #{info[:count]}"
66
+ puts " Dependent type: #{info[:dependent_type]}"
67
+ puts " Sample IDs: #{info[:sample_ids].join(', ')}"
68
+ puts
69
+ end
70
+ end
71
+ rescue NameError
72
+ puts "Error: Model '#{args[:model_name]}' not found"
73
+ exit 1
74
+ rescue ActiveRecord::RecordNotFound
75
+ puts "Error: #{args[:model_name]}##{args[:record_id]} not found"
76
+ exit 1
77
+ end
78
+ end
79
+
80
+ desc 'Generate HTML visualization and save to file'
81
+ task :html, %i[model_name output_file] => :environment do |_t, args|
82
+ require 'blast_radius'
83
+
84
+ unless args[:model_name]
85
+ puts 'Usage: rake blast_radius:html[ModelName,output_file]'
86
+ exit 1
87
+ end
88
+
89
+ output_file = args[:output_file] || 'blast_radius.html'
90
+
91
+ begin
92
+ model_class = args[:model_name].constantize
93
+ html = model_class.blast_radius(format: :html)
94
+
95
+ File.write(output_file, html)
96
+ puts "HTML visualization saved to: #{output_file}"
97
+ rescue NameError
98
+ puts "Error: Model '#{args[:model_name]}' not found"
99
+ exit 1
100
+ end
101
+ end
102
+ end
metadata ADDED
@@ -0,0 +1,164 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blast_radius
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '6.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '6.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '6.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rails
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '6.1'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '6.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubocop
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubocop-rspec
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: sqlite3
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '1.7'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.7'
110
+ description: 'A gem to visualize which records will be deleted due to dependent: :destroy
111
+ and similar options in ActiveRecord associations'
112
+ email:
113
+ - t.yudai92@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - CHANGELOG.md
119
+ - LICENSE.txt
120
+ - README.md
121
+ - lib/blast_radius.rb
122
+ - lib/blast_radius/active_record_extension.rb
123
+ - lib/blast_radius/analyzer.rb
124
+ - lib/blast_radius/configuration.rb
125
+ - lib/blast_radius/dependency_tree.rb
126
+ - lib/blast_radius/formatters/base.rb
127
+ - lib/blast_radius/formatters/dot_formatter.rb
128
+ - lib/blast_radius/formatters/html_formatter.rb
129
+ - lib/blast_radius/formatters/json_formatter.rb
130
+ - lib/blast_radius/formatters/mermaid_formatter.rb
131
+ - lib/blast_radius/formatters/text_formatter.rb
132
+ - lib/blast_radius/html/graph.js
133
+ - lib/blast_radius/html/styles.css
134
+ - lib/blast_radius/html/template.html.erb
135
+ - lib/blast_radius/impact_calculator.rb
136
+ - lib/blast_radius/node.rb
137
+ - lib/blast_radius/railtie.rb
138
+ - lib/blast_radius/version.rb
139
+ - lib/tasks/blast_radius.rake
140
+ homepage: https://github.com/ydah/blast_radius
141
+ licenses:
142
+ - MIT
143
+ metadata:
144
+ homepage_uri: https://github.com/ydah/blast_radius
145
+ source_code_uri: https://github.com/ydah/blast_radius
146
+ changelog_uri: https://github.com/ydah/blast_radius/blob/main/CHANGELOG.md
147
+ rdoc_options: []
148
+ require_paths:
149
+ - lib
150
+ required_ruby_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: 3.0.0
155
+ required_rubygems_version: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ requirements: []
161
+ rubygems_version: 3.6.9
162
+ specification_version: 4
163
+ summary: Visualize the cascade deletion impact in Rails applications
164
+ test_files: []