rails_lens 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/LICENSE.txt +2 -2
- data/README.md +393 -9
- data/exe/rails_lens +25 -0
- data/lib/rails_lens/analyzers/association_analyzer.rb +111 -0
- data/lib/rails_lens/analyzers/base.rb +35 -0
- data/lib/rails_lens/analyzers/best_practices_analyzer.rb +114 -0
- data/lib/rails_lens/analyzers/column_analyzer.rb +97 -0
- data/lib/rails_lens/analyzers/composite_keys.rb +62 -0
- data/lib/rails_lens/analyzers/database_constraints.rb +35 -0
- data/lib/rails_lens/analyzers/delegated_types.rb +124 -0
- data/lib/rails_lens/analyzers/enums.rb +34 -0
- data/lib/rails_lens/analyzers/error_handling.rb +66 -0
- data/lib/rails_lens/analyzers/foreign_key_analyzer.rb +47 -0
- data/lib/rails_lens/analyzers/generated_columns.rb +56 -0
- data/lib/rails_lens/analyzers/index_analyzer.rb +128 -0
- data/lib/rails_lens/analyzers/inheritance.rb +212 -0
- data/lib/rails_lens/analyzers/notes.rb +325 -0
- data/lib/rails_lens/analyzers/performance_analyzer.rb +110 -0
- data/lib/rails_lens/annotation_pipeline.rb +87 -0
- data/lib/rails_lens/cli.rb +170 -0
- data/lib/rails_lens/cli_error_handler.rb +86 -0
- data/lib/rails_lens/commands.rb +164 -0
- data/lib/rails_lens/connection.rb +133 -0
- data/lib/rails_lens/erd/column_type_formatter.rb +32 -0
- data/lib/rails_lens/erd/domain_color_mapper.rb +40 -0
- data/lib/rails_lens/erd/mysql_column_type_formatter.rb +19 -0
- data/lib/rails_lens/erd/postgresql_column_type_formatter.rb +19 -0
- data/lib/rails_lens/erd/visualizer.rb +301 -0
- data/lib/rails_lens/errors.rb +78 -0
- data/lib/rails_lens/extension_loader.rb +261 -0
- data/lib/rails_lens/extensions/base.rb +194 -0
- data/lib/rails_lens/extensions/closure_tree_ext.rb +157 -0
- data/lib/rails_lens/file_insertion_helper.rb +168 -0
- data/lib/rails_lens/mailer/annotator.rb +226 -0
- data/lib/rails_lens/mailer/extractor.rb +201 -0
- data/lib/rails_lens/model_detector.rb +241 -0
- data/lib/rails_lens/parsers/class_info.rb +46 -0
- data/lib/rails_lens/parsers/module_info.rb +33 -0
- data/lib/rails_lens/parsers/parser_result.rb +55 -0
- data/lib/rails_lens/parsers/prism_parser.rb +90 -0
- data/lib/rails_lens/parsers.rb +10 -0
- data/lib/rails_lens/providers/association_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/base.rb +37 -0
- data/lib/rails_lens/providers/best_practices_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/column_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/composite_keys_provider.rb +11 -0
- data/lib/rails_lens/providers/database_constraints_provider.rb +11 -0
- data/lib/rails_lens/providers/delegated_types_provider.rb +11 -0
- data/lib/rails_lens/providers/enums_provider.rb +11 -0
- data/lib/rails_lens/providers/extension_notes_provider.rb +20 -0
- data/lib/rails_lens/providers/extensions_provider.rb +22 -0
- data/lib/rails_lens/providers/foreign_key_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/generated_columns_provider.rb +11 -0
- data/lib/rails_lens/providers/index_notes_provider.rb +20 -0
- data/lib/rails_lens/providers/inheritance_provider.rb +23 -0
- data/lib/rails_lens/providers/notes_provider_base.rb +25 -0
- data/lib/rails_lens/providers/performance_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/schema_provider.rb +48 -0
- data/lib/rails_lens/providers/section_provider_base.rb +28 -0
- data/lib/rails_lens/railtie.rb +17 -0
- data/lib/rails_lens/rake_bootstrapper.rb +18 -0
- data/lib/rails_lens/route/annotator.rb +268 -0
- data/lib/rails_lens/route/extractor.rb +133 -0
- data/lib/rails_lens/route/parser.rb +59 -0
- data/lib/rails_lens/schema/adapters/base.rb +345 -0
- data/lib/rails_lens/schema/adapters/database_info.rb +118 -0
- data/lib/rails_lens/schema/adapters/mysql.rb +279 -0
- data/lib/rails_lens/schema/adapters/postgresql.rb +197 -0
- data/lib/rails_lens/schema/adapters/sqlite3.rb +96 -0
- data/lib/rails_lens/schema/annotation.rb +144 -0
- data/lib/rails_lens/schema/annotation_manager.rb +202 -0
- data/lib/rails_lens/tasks/annotate.rake +35 -0
- data/lib/rails_lens/tasks/erd.rake +24 -0
- data/lib/rails_lens/tasks/mailers.rake +27 -0
- data/lib/rails_lens/tasks/routes.rake +27 -0
- data/lib/rails_lens/tasks/schema.rake +108 -0
- data/lib/rails_lens/version.rb +5 -0
- data/lib/rails_lens.rb +138 -5
- metadata +215 -11
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
class Connection
|
5
|
+
class << self
|
6
|
+
def adapter_for(model_class)
|
7
|
+
connection = model_class.connection
|
8
|
+
adapter_name = detect_adapter_name(connection)
|
9
|
+
|
10
|
+
adapter_class = resolve_adapter_class(adapter_name)
|
11
|
+
adapter_class.new(connection, model_class.table_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def resolve_adapter_class(adapter_name)
|
15
|
+
class_name = "RailsLens::Schema::Adapters::#{adapter_name.to_s.camelize}"
|
16
|
+
|
17
|
+
begin
|
18
|
+
class_name.constantize
|
19
|
+
rescue NameError
|
20
|
+
raise RailsLens::UnsupportedAdapterError,
|
21
|
+
"Unsupported database adapter: #{adapter_name}. " \
|
22
|
+
"Expected adapter class #{class_name} not found. " \
|
23
|
+
'Consider adding support by creating the adapter class.'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def detect_adapter_name(connection)
|
28
|
+
adapter_name = connection.adapter_name.downcase
|
29
|
+
|
30
|
+
case adapter_name
|
31
|
+
when /postgresql/, /postgis/
|
32
|
+
:postgresql
|
33
|
+
when /mysql2/, /trilogy/, /mariadb/
|
34
|
+
:mysql
|
35
|
+
when /sqlite/
|
36
|
+
:sqlite3
|
37
|
+
else
|
38
|
+
# Return the normalized adapter name for constantize
|
39
|
+
adapter_name.to_sym
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def database_dialect(connection)
|
44
|
+
adapter_name = connection.adapter_name
|
45
|
+
|
46
|
+
case adapter_name.downcase
|
47
|
+
when /postgresql/, /postgis/
|
48
|
+
'PostgreSQL'
|
49
|
+
when /mysql2/, /trilogy/
|
50
|
+
'MySQL'
|
51
|
+
when /mariadb/
|
52
|
+
'MariaDB'
|
53
|
+
when /sqlite/
|
54
|
+
'SQLite'
|
55
|
+
else
|
56
|
+
adapter_name # Return the original adapter name if unknown
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def connection_config(model_class)
|
61
|
+
if model_class.connection.respond_to?(:connection_db_config)
|
62
|
+
# Rails 6.1+
|
63
|
+
model_class.connection.connection_db_config.configuration_hash
|
64
|
+
elsif model_class.connection.respond_to?(:config)
|
65
|
+
# Older Rails versions
|
66
|
+
model_class.connection.config
|
67
|
+
else
|
68
|
+
{}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def database_version(model_class)
|
73
|
+
connection = model_class.connection
|
74
|
+
|
75
|
+
if connection.respond_to?(:database_version)
|
76
|
+
connection.database_version
|
77
|
+
elsif connection.respond_to?(:version)
|
78
|
+
connection.version
|
79
|
+
else
|
80
|
+
'Unknown'
|
81
|
+
end
|
82
|
+
rescue StandardError
|
83
|
+
'Unknown'
|
84
|
+
end
|
85
|
+
|
86
|
+
def connection_info(model_class)
|
87
|
+
{
|
88
|
+
adapter: detect_adapter_name(model_class.connection),
|
89
|
+
database: connection_config(model_class)[:database],
|
90
|
+
version: database_version(model_class),
|
91
|
+
encoding: connection_encoding(model_class)
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
def supports_foreign_keys?(model_class)
|
96
|
+
model_class.connection.supports_foreign_keys?
|
97
|
+
end
|
98
|
+
|
99
|
+
def supports_check_constraints?(model_class)
|
100
|
+
model_class.connection.supports_check_constraints?
|
101
|
+
end
|
102
|
+
|
103
|
+
def supports_comments?(model_class)
|
104
|
+
model_class.connection.supports_comments?
|
105
|
+
end
|
106
|
+
|
107
|
+
def supports_views?(model_class)
|
108
|
+
model_class.connection.supports_views?
|
109
|
+
end
|
110
|
+
|
111
|
+
def supports_materialized_views?(model_class)
|
112
|
+
model_class.connection.supports_materialized_views?
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def connection_encoding(model_class)
|
118
|
+
connection = model_class.connection
|
119
|
+
|
120
|
+
if connection.respond_to?(:encoding)
|
121
|
+
connection.encoding
|
122
|
+
elsif connection.respond_to?(:charset)
|
123
|
+
connection.charset
|
124
|
+
else
|
125
|
+
config = connection_config(model_class)
|
126
|
+
config[:encoding] || config[:charset] || 'Unknown'
|
127
|
+
end
|
128
|
+
rescue StandardError
|
129
|
+
'Unknown'
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
module ERD
|
5
|
+
class ColumnTypeFormatter
|
6
|
+
def self.format(column)
|
7
|
+
new(column).format
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(column)
|
11
|
+
@column = column
|
12
|
+
end
|
13
|
+
|
14
|
+
def format
|
15
|
+
# Default to the generic type
|
16
|
+
case @column.type
|
17
|
+
when :integer, :bigint then 'int'
|
18
|
+
when :string, :text then 'varchar'
|
19
|
+
when :boolean then 'boolean'
|
20
|
+
when :decimal, :float then 'decimal'
|
21
|
+
when :date then 'date'
|
22
|
+
when :datetime, :timestamp then 'datetime'
|
23
|
+
when :time then 'time'
|
24
|
+
when :binary then 'blob'
|
25
|
+
when :json, :jsonb then 'json'
|
26
|
+
else
|
27
|
+
@column.type.to_s
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
module ERD
|
5
|
+
# Assigns a consistent color to a domain from a predefined palette.
|
6
|
+
class DomainColorMapper
|
7
|
+
# Default color palette for domains using CSS named colors.
|
8
|
+
DEFAULT_COLORS = %w[
|
9
|
+
lightblue
|
10
|
+
lightcoral
|
11
|
+
lightgreen
|
12
|
+
lightyellow
|
13
|
+
plum
|
14
|
+
lightcyan
|
15
|
+
lightgray
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
# The color used for domains not found in the initial list.
|
19
|
+
FALLBACK_COLOR = 'lightgray'
|
20
|
+
|
21
|
+
# @param domains [Array<Symbol>] A unique list of domain names.
|
22
|
+
# @param colors [Array<String>] A list of hex color codes to cycle through.
|
23
|
+
def initialize(domains, colors: DEFAULT_COLORS)
|
24
|
+
@colors = colors.empty? ? [FALLBACK_COLOR] : colors
|
25
|
+
@domain_map = domains.uniq.each_with_index.to_h
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the color for a given domain.
|
29
|
+
#
|
30
|
+
# @param domain [Symbol] The domain name.
|
31
|
+
# @return [String] The hex color code.
|
32
|
+
def color_for(domain)
|
33
|
+
domain_index = @domain_map[domain]
|
34
|
+
return FALLBACK_COLOR unless domain_index
|
35
|
+
|
36
|
+
@colors[domain_index % @colors.length]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
module ERD
|
5
|
+
class MysqlColumnTypeFormatter < ColumnTypeFormatter
|
6
|
+
def format
|
7
|
+
case @column.sql_type
|
8
|
+
when /json/i then 'json'
|
9
|
+
when /enum/i then 'enum'
|
10
|
+
when /set/i then 'set'
|
11
|
+
when /mediumtext/i then 'mediumtext'
|
12
|
+
when /tinyint\(1\)/i then 'boolean'
|
13
|
+
else
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
module ERD
|
5
|
+
class PostgresqlColumnTypeFormatter < ColumnTypeFormatter
|
6
|
+
def format
|
7
|
+
case @column.sql_type
|
8
|
+
when /jsonb/i then 'jsonb'
|
9
|
+
when /uuid/i then 'uuid'
|
10
|
+
when /inet/i then 'inet'
|
11
|
+
when /array/i then 'array'
|
12
|
+
when /tsvector/i then 'tsvector'
|
13
|
+
else
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,301 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
module ERD
|
5
|
+
class Visualizer
|
6
|
+
require_relative 'domain_color_mapper'
|
7
|
+
attr_reader :options, :config
|
8
|
+
|
9
|
+
def initialize(options: {})
|
10
|
+
@options = options
|
11
|
+
@config = RailsLens.config.erd.merge(options.compact.transform_keys(&:to_sym))
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate
|
15
|
+
models = load_models
|
16
|
+
generate_mermaid(models)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def load_models
|
22
|
+
ModelDetector.detect_models(options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate_mermaid(models)
|
26
|
+
if models.blank?
|
27
|
+
# Still need to save the output even if no models found
|
28
|
+
mermaid_output = "erDiagram\n %% No models found"
|
29
|
+
return save_output(mermaid_output, 'mmd')
|
30
|
+
end
|
31
|
+
|
32
|
+
output = ['erDiagram']
|
33
|
+
|
34
|
+
# Add theme configuration
|
35
|
+
if config[:theme] || config[:colors]
|
36
|
+
output << ''
|
37
|
+
output << ' %% Theme Configuration'
|
38
|
+
add_theme_configuration(output)
|
39
|
+
output << ''
|
40
|
+
end
|
41
|
+
|
42
|
+
# Choose grouping strategy based on configuration
|
43
|
+
grouped_models = if config[:group_by_database]
|
44
|
+
# Group models by database connection
|
45
|
+
group_models_by_database(models)
|
46
|
+
else
|
47
|
+
# Group models by domain (existing behavior)
|
48
|
+
group_models_by_domain(models)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create color mapper for domains (for future extensibility)
|
52
|
+
unless config[:group_by_database]
|
53
|
+
domain_list = grouped_models.keys.sort
|
54
|
+
@color_mapper = create_domain_color_mapper(domain_list)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Add entities
|
58
|
+
grouped_models.each do |group_key, group_models|
|
59
|
+
if config[:group_by_database]
|
60
|
+
output << " %% Database: #{group_key}"
|
61
|
+
elsif group_key != :general
|
62
|
+
output << " %% #{group_key.to_s.humanize} Domain"
|
63
|
+
end
|
64
|
+
|
65
|
+
group_models.each do |model|
|
66
|
+
model_display_name = format_model_name(model)
|
67
|
+
output << " #{model_display_name} {"
|
68
|
+
|
69
|
+
model.columns.each do |column|
|
70
|
+
type_str = format_column_type(column)
|
71
|
+
name_str = column.name
|
72
|
+
keys = determine_keys(model, column)
|
73
|
+
key_str = keys.map(&:to_s).join(' ')
|
74
|
+
|
75
|
+
output << " #{type_str} #{name_str}#{" #{key_str}" unless key_str.empty?}"
|
76
|
+
end
|
77
|
+
|
78
|
+
output << ' }'
|
79
|
+
output << ''
|
80
|
+
|
81
|
+
Rails.logger.debug { "Added entity: #{model_display_name}" } if options[:verbose]
|
82
|
+
rescue StandardError => e
|
83
|
+
Rails.logger.debug { "Warning: Could not add entity #{model.name}: #{e.message}" }
|
84
|
+
# Don't add partial entity if there's an error
|
85
|
+
# Remove the opening brace line if it was added
|
86
|
+
output.pop if output.last&.end_with?(' {')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Add relationships
|
91
|
+
output << ' %% Relationships'
|
92
|
+
models.each do |model|
|
93
|
+
add_model_relationships(output, model, models)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Generate mermaid syntax
|
97
|
+
mermaid_output = output.join("\n")
|
98
|
+
|
99
|
+
# Save output
|
100
|
+
filename = save_output(mermaid_output, 'mmd')
|
101
|
+
|
102
|
+
Rails.logger.debug 'ERD generated successfully!'
|
103
|
+
filename # Return the filename instead of content
|
104
|
+
end
|
105
|
+
|
106
|
+
def format_column_type(column)
|
107
|
+
formatter_class = case column.sql_type
|
108
|
+
when /jsonb|uuid|inet|array|tsvector/i
|
109
|
+
PostgresqlColumnTypeFormatter
|
110
|
+
when /json|enum|set|mediumtext|tinyint\(1\)/i
|
111
|
+
MysqlColumnTypeFormatter
|
112
|
+
else
|
113
|
+
ColumnTypeFormatter
|
114
|
+
end
|
115
|
+
|
116
|
+
formatter_class.format(column)
|
117
|
+
end
|
118
|
+
|
119
|
+
def determine_keys(model, column)
|
120
|
+
keys = []
|
121
|
+
keys << :PK if column.name == model.primary_key
|
122
|
+
|
123
|
+
# Check foreign keys
|
124
|
+
if model.respond_to?(:reflect_on_all_associations)
|
125
|
+
model.reflect_on_all_associations(:belongs_to).each do |assoc|
|
126
|
+
keys << :FK if assoc.foreign_key.to_s == column.name
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Check unique indexes
|
131
|
+
if model.connection.indexes(model.table_name).any? do |idx|
|
132
|
+
idx.unique && idx.columns.include?(column.name)
|
133
|
+
end && keys.exclude?(:PK)
|
134
|
+
keys << :UK
|
135
|
+
end
|
136
|
+
|
137
|
+
keys
|
138
|
+
end
|
139
|
+
|
140
|
+
def add_model_relationships(output, model, models)
|
141
|
+
model.reflect_on_all_associations.each do |association|
|
142
|
+
next if association.options[:through] # Skip through associations for now
|
143
|
+
next if association.polymorphic? # Skip polymorphic associations
|
144
|
+
|
145
|
+
# Check if target model exists and has table
|
146
|
+
target_model = nil
|
147
|
+
begin
|
148
|
+
target_model = association.klass
|
149
|
+
rescue NameError, ArgumentError
|
150
|
+
next # Skip if class can't be loaded
|
151
|
+
end
|
152
|
+
|
153
|
+
next unless target_model && models.include?(target_model)
|
154
|
+
|
155
|
+
case association.macro
|
156
|
+
when :belongs_to
|
157
|
+
add_belongs_to_relationship(output, model, association, target_model)
|
158
|
+
when :has_one
|
159
|
+
add_has_one_relationship(output, model, association, target_model)
|
160
|
+
when :has_many
|
161
|
+
add_has_many_relationship(output, model, association, target_model)
|
162
|
+
when :has_and_belongs_to_many
|
163
|
+
add_habtm_relationship(output, model, association, target_model)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Check for closure_tree self-reference
|
168
|
+
return unless model.respond_to?(:_ct)
|
169
|
+
|
170
|
+
output << " #{format_model_name(model)} }o--o{ #{format_model_name(model)} : \"closure_tree\""
|
171
|
+
end
|
172
|
+
|
173
|
+
def add_belongs_to_relationship(output, model, association, target_model)
|
174
|
+
output << " #{format_model_name(model)} }o--|| #{format_model_name(target_model)} : \"#{association.name}\""
|
175
|
+
rescue StandardError => e
|
176
|
+
Rails.logger.debug do
|
177
|
+
"Warning: Could not add belongs_to relationship #{model.name} -> #{association.name}: #{e.message}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def add_has_one_relationship(output, model, association, target_model)
|
182
|
+
output << " #{format_model_name(model)} ||--o| #{format_model_name(target_model)} : \"#{association.name}\""
|
183
|
+
rescue StandardError => e
|
184
|
+
Rails.logger.debug do
|
185
|
+
"Warning: Could not add has_one relationship #{model.name} -> #{association.name}: #{e.message}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def add_has_many_relationship(output, model, association, target_model)
|
190
|
+
output << " #{format_model_name(model)} ||--o{ #{format_model_name(target_model)} : \"#{association.name}\""
|
191
|
+
rescue StandardError => e
|
192
|
+
Rails.logger.debug do
|
193
|
+
"Warning: Could not add has_many relationship #{model.name} -> #{association.name}: #{e.message}"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def add_habtm_relationship(output, model, association, target_model)
|
198
|
+
output << " #{format_model_name(model)} }o--o{ #{format_model_name(target_model)} : \"#{association.name}\""
|
199
|
+
rescue StandardError => e
|
200
|
+
Rails.logger.debug do
|
201
|
+
"Warning: Could not add habtm relationship #{model.name} -> #{association.name}: #{e.message}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def add_theme_configuration(output)
|
206
|
+
# Get default color palette
|
207
|
+
default_colors = config[:default_colors] || DomainColorMapper::DEFAULT_COLORS
|
208
|
+
|
209
|
+
# Use first few colors for Mermaid theme
|
210
|
+
primary_color = default_colors[0] || 'lightgray'
|
211
|
+
secondary_color = default_colors[1] || 'lightblue'
|
212
|
+
tertiary_color = default_colors[2] || 'lightcoral'
|
213
|
+
|
214
|
+
# Mermaid theme directives
|
215
|
+
output << ' %%{init: {'
|
216
|
+
output << ' "theme": "default",'
|
217
|
+
output << ' "themeVariables": {'
|
218
|
+
output << " \"primaryColor\": \"#{primary_color}\","
|
219
|
+
output << ' "primaryTextColor": "#333",'
|
220
|
+
output << ' "primaryBorderColor": "#666",'
|
221
|
+
output << ' "lineColor": "#666",'
|
222
|
+
output << " \"secondaryColor\": \"#{secondary_color}\","
|
223
|
+
output << " \"tertiaryColor\": \"#{tertiary_color}\""
|
224
|
+
output << ' }'
|
225
|
+
output << ' }}%%'
|
226
|
+
end
|
227
|
+
|
228
|
+
def group_models_by_database(models)
|
229
|
+
grouped = Hash.new { |h, k| h[k] = [] }
|
230
|
+
|
231
|
+
models.each do |model|
|
232
|
+
# Get the database name from the model's connection
|
233
|
+
db_name = model.connection.pool.db_config.name
|
234
|
+
grouped[db_name] << model
|
235
|
+
rescue StandardError => e
|
236
|
+
Rails.logger.debug { "Warning: Could not determine database for #{model.name}: #{e.message}" }
|
237
|
+
grouped['unknown'] << model
|
238
|
+
end
|
239
|
+
|
240
|
+
# Sort databases for consistent output
|
241
|
+
grouped.sort_by { |db_name, _| db_name.to_s }.to_h
|
242
|
+
end
|
243
|
+
|
244
|
+
def group_models_by_domain(models)
|
245
|
+
grouped = Hash.new { |h, k| h[k] = [] }
|
246
|
+
|
247
|
+
models.each do |model|
|
248
|
+
domain = determine_model_domain(model)
|
249
|
+
grouped[domain] << model
|
250
|
+
end
|
251
|
+
|
252
|
+
# Sort domains for consistent output
|
253
|
+
grouped.sort_by { |domain, _| domain.to_s }.to_h
|
254
|
+
end
|
255
|
+
|
256
|
+
def determine_model_domain(model)
|
257
|
+
model_name = model.name.downcase
|
258
|
+
|
259
|
+
# Basic domain detection based on common patterns
|
260
|
+
return :auth if model_name.match?(/user|account|session|authentication|authorization/)
|
261
|
+
return :content if model_name.match?(/post|article|comment|blog|page|content/)
|
262
|
+
return :commerce if model_name.match?(/product|order|payment|cart|invoice|transaction/)
|
263
|
+
return :core if model_name.match?(/category|tag|setting|configuration|notification/)
|
264
|
+
|
265
|
+
# Default domain
|
266
|
+
:general
|
267
|
+
end
|
268
|
+
|
269
|
+
def create_domain_color_mapper(domains)
|
270
|
+
# Get colors from config or use defaults
|
271
|
+
colors = config[:default_colors] || DomainColorMapper::DEFAULT_COLORS
|
272
|
+
DomainColorMapper.new(domains, colors: colors)
|
273
|
+
end
|
274
|
+
|
275
|
+
def format_model_name(model)
|
276
|
+
return model.name unless config[:include_all_databases] || config[:show_database_labels]
|
277
|
+
|
278
|
+
# Get database name from the model's connection
|
279
|
+
begin
|
280
|
+
db_name = model.connection.pool.db_config.name
|
281
|
+
return model.name if db_name == 'primary' # Don't prefix primary database models
|
282
|
+
|
283
|
+
"#{model.name}[#{db_name}]"
|
284
|
+
rescue StandardError
|
285
|
+
model.name
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def save_output(content, extension)
|
290
|
+
output_dir = config[:output_dir] || 'doc/erd'
|
291
|
+
FileUtils.mkdir_p(output_dir)
|
292
|
+
|
293
|
+
filename = File.join(output_dir, "erd.#{extension}")
|
294
|
+
File.write(filename, content)
|
295
|
+
|
296
|
+
Rails.logger.debug { "ERD saved to: #{filename}" }
|
297
|
+
filename # Return the filename
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
# Base error class for all Rails Lens errors
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Configuration-related errors
|
8
|
+
class ConfigurationError < Error; end
|
9
|
+
|
10
|
+
# Model detection errors
|
11
|
+
class ModelDetectionError < Error; end
|
12
|
+
class ModelNotFoundError < ModelDetectionError; end
|
13
|
+
class InvalidModelError < ModelDetectionError; end
|
14
|
+
|
15
|
+
# Database-related errors
|
16
|
+
class DatabaseError < Error; end
|
17
|
+
class ConnectionError < DatabaseError; end
|
18
|
+
class SchemaError < DatabaseError; end
|
19
|
+
class TableNotFoundError < DatabaseError; end
|
20
|
+
class UnsupportedAdapterError < DatabaseError; end
|
21
|
+
|
22
|
+
# Annotation-related errors
|
23
|
+
class AnnotationError < Error; end
|
24
|
+
class FileNotFoundError < AnnotationError; end
|
25
|
+
class ParseError < AnnotationError; end
|
26
|
+
class InsertionError < AnnotationError; end
|
27
|
+
|
28
|
+
# Extension-related errors
|
29
|
+
class ExtensionError < Error; end
|
30
|
+
class ExtensionLoadError < ExtensionError; end
|
31
|
+
class ExtensionConfigError < ExtensionError; end
|
32
|
+
|
33
|
+
# Analysis errors
|
34
|
+
class AnalysisError < Error; end
|
35
|
+
class AnalyzerError < AnalysisError; end
|
36
|
+
|
37
|
+
# ERD generation errors
|
38
|
+
class ERDError < Error; end
|
39
|
+
class VisualizationError < ERDError; end
|
40
|
+
|
41
|
+
# Error reporter for centralized error handling
|
42
|
+
class ErrorReporter
|
43
|
+
class << self
|
44
|
+
def report(error, context = {})
|
45
|
+
return unless RailsLens.verbose || RailsLens.debug
|
46
|
+
|
47
|
+
message = build_error_message(error, context)
|
48
|
+
|
49
|
+
# Use Rails logger for verbose mode
|
50
|
+
Rails.logger&.error message
|
51
|
+
|
52
|
+
# Use kernel output for debug mode to ensure visibility
|
53
|
+
return unless RailsLens.debug
|
54
|
+
|
55
|
+
Rails.logger.debug message
|
56
|
+
end
|
57
|
+
|
58
|
+
def handle(context = {})
|
59
|
+
yield
|
60
|
+
rescue StandardError => e
|
61
|
+
report(e, context)
|
62
|
+
raise if RailsLens.raise_on_error
|
63
|
+
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def build_error_message(error, context)
|
70
|
+
message = ['[RailsLens Error]']
|
71
|
+
message << "Context: #{context.inspect}" if context.any?
|
72
|
+
message << "#{error.class}: #{error.message}"
|
73
|
+
message << error.backtrace.first(5).join("\n") if error.backtrace && RailsLens.config.debug
|
74
|
+
message.join("\n")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|