lagoon 0.1.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +39 -0
  3. data/CHANGELOG.md +14 -2
  4. data/README.md +111 -146
  5. data/Rakefile +17 -3
  6. data/exe/lagoon +3 -3
  7. data/gemfiles/adapters.gemfile +15 -0
  8. data/gemfiles/rails_6.1.gemfile +23 -0
  9. data/gemfiles/rails_7.0.gemfile +23 -0
  10. data/gemfiles/rails_7.1.gemfile +23 -0
  11. data/gemfiles/rails_7.2.gemfile +23 -0
  12. data/gemfiles/rails_8.0.gemfile +23 -0
  13. data/gemfiles/rails_8.1.gemfile +23 -0
  14. data/lib/lagoon/analyzer/action_controller_analyzer.rb +76 -0
  15. data/lib/lagoon/analyzer/active_record_analyzer.rb +202 -0
  16. data/lib/lagoon/analyzer/ast/controller_scope_collector.rb +104 -0
  17. data/lib/lagoon/analyzer/ast/method_reference_visitor.rb +170 -0
  18. data/lib/lagoon/analyzer/ast_model_reference_analyzer.rb +44 -0
  19. data/lib/lagoon/analyzer/database_schema_analyzer.rb +83 -0
  20. data/lib/lagoon/cli.rb +109 -80
  21. data/lib/lagoon/configuration.rb +42 -6
  22. data/lib/lagoon/diagram/base.rb +48 -8
  23. data/lib/lagoon/diagram/controller_diagram.rb +10 -16
  24. data/lib/lagoon/diagram/controller_model_diagram.rb +28 -0
  25. data/lib/lagoon/diagram/er_diagram.rb +10 -16
  26. data/lib/lagoon/diagram/model_diagram.rb +13 -16
  27. data/lib/lagoon/errors.rb +9 -0
  28. data/lib/lagoon/options.rb +182 -0
  29. data/lib/lagoon/parser/application_class_filter.rb +47 -0
  30. data/lib/lagoon/parser/controller_model_parser.rb +196 -0
  31. data/lib/lagoon/parser/controller_parser.rb +37 -54
  32. data/lib/lagoon/parser/model_parser.rb +57 -112
  33. data/lib/lagoon/parser/schema_parser.rb +120 -67
  34. data/lib/lagoon/railtie.rb +1 -1
  35. data/lib/lagoon/renderer/base_renderer.rb +49 -19
  36. data/lib/lagoon/renderer/class_diagram_renderer.rb +37 -31
  37. data/lib/lagoon/renderer/controller_model_er_renderer.rb +47 -0
  38. data/lib/lagoon/renderer/er_diagram_renderer.rb +43 -42
  39. data/lib/lagoon/result.rb +22 -0
  40. data/lib/lagoon/version.rb +1 -1
  41. data/lib/lagoon.rb +70 -21
  42. data/lib/tasks/lagoon.rake +15 -7
  43. data/sig/lagoon.rbs +107 -0
  44. metadata +57 -7
data/lib/lagoon/cli.rb CHANGED
@@ -1,112 +1,141 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thor"
3
+ require 'thor'
4
4
 
5
5
  module Lagoon
6
6
  class CLI < Thor
7
- class_option :verbose, type: :boolean, aliases: "-v", desc: "Enable verbose output"
8
- class_option :output, type: :string, aliases: "-o", desc: "Output file path"
9
- class_option :direction, type: :string, aliases: "-d", desc: "Diagram direction (TB/BT/LR/RL)"
10
- class_option :root, type: :string, aliases: "-r", desc: "Application root path"
11
-
12
- desc "models", "Generate Mermaid model diagram"
13
- method_option :brief, type: :boolean, aliases: "-b", desc: "Compact diagram (no attributes/methods)"
14
- method_option :inheritance, type: :boolean, aliases: "-i", desc: "Include inheritance relationships"
15
- method_option :exclude, type: :array, aliases: "-e", desc: "Exclude specified models"
16
- method_option :specify, type: :array, aliases: "-s", desc: "Only process specified models"
17
- method_option :all_models, type: :boolean, aliases: "-a", desc: "Include all models"
18
- method_option :show_belongs_to, type: :boolean, desc: "Show belongs_to associations"
19
- method_option :hide_through, type: :boolean, desc: "Hide through associations"
20
- method_option :all_columns, type: :boolean, desc: "Show all columns"
21
- method_option :hide_magic, type: :boolean, desc: "Hide magic fields (id, timestamps)"
22
- method_option :hide_types, type: :boolean, desc: "Hide attribute types"
7
+ class_option :verbose, type: :boolean, aliases: '-v', desc: 'Enable verbose output'
8
+ class_option :output, type: :string, aliases: '-o', desc: 'Output file path'
9
+ class_option :root, type: :string, aliases: '-r', desc: 'Application root path'
10
+ class_option :strict, type: :boolean, desc: 'Stop on the first analysis error'
11
+
12
+ desc 'models', 'Generate Mermaid model diagram'
13
+ method_option :direction, type: :string, aliases: '-d', desc: 'Diagram direction (TB/BT/LR/RL)'
14
+ method_option :brief, type: :boolean, aliases: '-b', desc: 'Compact diagram (no attributes/methods)'
15
+ method_option :inheritance, type: :boolean, aliases: '-i', desc: 'Include inheritance relationships'
16
+ method_option :exclude, type: :array, aliases: '-e', desc: 'Exclude specified models'
17
+ method_option :specify, type: :array, aliases: '-s', desc: 'Only process specified models'
18
+ method_option :all_models, type: :boolean, aliases: '-a', desc: 'Include models outside app/models'
19
+ method_option :show_belongs_to, type: :boolean, desc: 'Show belongs_to associations'
20
+ method_option :hide_through, type: :boolean, desc: 'Hide through associations'
21
+ method_option :all_columns, type: :boolean, desc: 'Show all columns even when --hide-magic is set'
22
+ method_option :hide_magic, type: :boolean, desc: 'Hide id and timestamp fields'
23
+ method_option :hide_types, type: :boolean, desc: 'Hide attribute types'
23
24
  def models
24
- load_rails_environment
25
- setup_configuration
26
-
27
- output_file = Lagoon.generate_model_diagram(build_options)
28
- say "Model diagram generated: #{output_file}", :green
25
+ with_rails_environment do
26
+ report_result('Model', Lagoon.generate_model_diagram(command_options(:model)))
27
+ end
29
28
  end
30
29
 
31
- desc "controllers", "Generate Mermaid controller diagram"
32
- method_option :brief, type: :boolean, aliases: "-b", desc: "Compact diagram (no attributes/methods)"
33
- method_option :inheritance, type: :boolean, aliases: "-i", desc: "Include inheritance relationships"
34
- method_option :exclude, type: :array, aliases: "-e", desc: "Exclude specified controllers"
35
- method_option :specify, type: :array, aliases: "-s", desc: "Only process specified controllers"
36
- method_option :hide_public, type: :boolean, desc: "Hide public methods"
37
- method_option :hide_protected, type: :boolean, desc: "Hide protected methods"
38
- method_option :hide_private, type: :boolean, desc: "Hide private methods"
30
+ desc 'controllers', 'Generate Mermaid controller diagram'
31
+ method_option :direction, type: :string, aliases: '-d', desc: 'Diagram direction (TB/BT/LR/RL)'
32
+ method_option :brief, type: :boolean, aliases: '-b', desc: 'Compact diagram (no methods)'
33
+ method_option :inheritance, type: :boolean, aliases: '-i', desc: 'Include inheritance relationships'
34
+ method_option :exclude, type: :array, aliases: '-e', desc: 'Exclude specified controllers'
35
+ method_option :specify, type: :array, aliases: '-s', desc: 'Only process specified controllers'
36
+ method_option :all_controllers, type: :boolean, desc: 'Include controllers outside app/controllers'
37
+ method_option :hide_public, type: :boolean, desc: 'Hide public methods'
38
+ method_option :hide_protected, type: :boolean, desc: 'Hide protected methods'
39
+ method_option :hide_private, type: :boolean, desc: 'Hide private methods'
39
40
  def controllers
40
- load_rails_environment
41
- setup_configuration
42
-
43
- output_file = Lagoon.generate_controller_diagram(build_options)
44
- say "Controller diagram generated: #{output_file}", :green
41
+ with_rails_environment do
42
+ report_result('Controller', Lagoon.generate_controller_diagram(command_options(:controller)))
43
+ end
45
44
  end
46
45
 
47
- desc "er", "Generate Mermaid ER diagram"
48
- method_option :exclude, type: :array, aliases: "-e", desc: "Exclude specified tables"
49
- method_option :specify, type: :array, aliases: "-s", desc: "Only process specified tables"
46
+ desc 'er', 'Generate Mermaid ER diagram'
47
+ method_option :exclude, type: :array, aliases: '-e', desc: 'Exclude specified tables'
48
+ method_option :specify, type: :array, aliases: '-s', desc: 'Only process specified tables'
50
49
  def er
51
- load_rails_environment
52
- setup_configuration
50
+ with_rails_environment do
51
+ report_result('ER', Lagoon.generate_er_diagram(command_options(:er)))
52
+ end
53
+ end
53
54
 
54
- output_file = Lagoon.generate_er_diagram(build_options)
55
- say "ER diagram generated: #{output_file}", :green
55
+ desc 'controller_models', 'Generate controller-model dependency diagram'
56
+ method_option :direction, type: :string, aliases: '-d', desc: 'Diagram direction (TB/BT/LR/RL)'
57
+ method_option :exclude, type: :array, aliases: '-e', desc: 'Exclude specified controllers'
58
+ method_option :specify, type: :array, aliases: '-s', desc: 'Only process specified controllers'
59
+ method_option :all_controllers, type: :boolean, desc: 'Include controllers outside app/controllers'
60
+ method_option :show_actions, type: :boolean, default: true, desc: 'Show action names in labels'
61
+ def controller_models
62
+ with_rails_environment do
63
+ result = Lagoon.generate_controller_model_diagram(command_options(:controller_model))
64
+ report_result('Controller-model', result)
65
+ end
56
66
  end
57
67
 
58
- desc "all", "Generate all diagrams"
59
- method_option :brief, type: :boolean, aliases: "-b", desc: "Compact diagrams (no attributes/methods)"
68
+ desc 'all', 'Generate all diagrams'
69
+ method_option :direction, type: :string, aliases: '-d', desc: 'Class diagram direction (TB/BT/LR/RL)'
70
+ method_option :brief, type: :boolean, aliases: '-b', desc: 'Compact class diagrams'
60
71
  def all
61
- load_rails_environment
62
- setup_configuration
63
-
64
- results = Lagoon.generate_all(build_options)
65
- say "All diagrams generated:", :green
66
- say " Models: #{results[:models]}", :green
67
- say " Controllers: #{results[:controllers]}", :green
68
- say " ER: #{results[:er]}", :green
72
+ with_rails_environment do
73
+ results = Lagoon.generate_all(all_options)
74
+ say 'All diagrams generated:', :green
75
+ results.each do |name, result|
76
+ say " #{name}: #{result.path}", :green
77
+ report_warnings(result)
78
+ end
79
+ end
69
80
  end
70
81
 
71
- desc "version", "Show version"
82
+ desc 'version', 'Show version'
72
83
  def version
73
84
  say "Lagoon version #{Lagoon::VERSION}"
74
85
  end
75
86
 
76
- private
87
+ def self.exit_on_failure?
88
+ true
89
+ end
77
90
 
78
- def load_rails_environment
79
- rails_root = options[:root] || Dir.pwd
91
+ no_commands do
92
+ private
93
+
94
+ def with_rails_environment
95
+ rails_root = File.expand_path(options[:root] || Dir.pwd)
96
+ environment_file = File.join(rails_root, 'config', 'environment.rb')
97
+ unless File.file?(environment_file)
98
+ raise Thor::Error,
99
+ 'Rails application not found. Run from a Rails root or pass --root.'
100
+ end
101
+
102
+ Dir.chdir(rails_root) do
103
+ require environment_file
104
+ yield
105
+ end
106
+ rescue Thor::Error
107
+ raise
108
+ rescue LoadError, StandardError => e
109
+ message = "#{e.class}: #{e.message}"
110
+ message += "\n#{e.backtrace.join("\n")}" if options[:verbose] && e.backtrace
111
+ raise Thor::Error, message
112
+ end
80
113
 
81
- unless File.exist?(File.join(rails_root, "config", "environment.rb"))
82
- say "Error: Rails application not found. Please run from Rails root or use --root option.", :red
83
- exit 1
114
+ def command_options(kind)
115
+ raw_options.slice(*Options.allowed_keys(kind))
84
116
  end
85
117
 
86
- Dir.chdir(rails_root)
87
- require File.expand_path("config/environment", rails_root)
88
- rescue LoadError => e
89
- say "Error loading Rails environment: #{e.message}", :red
90
- exit 1
91
- end
118
+ def all_options
119
+ allowed = Options::KEYS.values.flatten.uniq
120
+ raw_options.slice(*allowed)
121
+ end
92
122
 
93
- def setup_configuration
94
- Lagoon.configure do |config|
95
- config.diagram_direction = options[:direction] if options[:direction]
96
- config.show_attributes = !options[:brief] if options.key?(:brief)
97
- config.show_methods = !options[:brief] if options.key?(:brief)
98
- config.include_inheritance = options[:inheritance] if options.key?(:inheritance)
99
- config.exclude_models = options[:exclude] if options[:exclude]
123
+ def raw_options
124
+ options.to_h.transform_keys(&:to_sym).reject do |key, value|
125
+ key == :root || value.nil?
126
+ end
100
127
  end
101
- end
102
128
 
103
- def build_options
104
- opts = options.dup
105
- opts[:show_belongs_to] = true if opts[:show_belongs_to]
106
- opts[:hide_through] = true if opts[:hide_through]
107
- opts[:all_columns] = true if opts[:all_columns]
108
- opts[:hide_magic] = true if opts[:hide_magic]
109
- opts
129
+ def report_result(label, result)
130
+ say "#{label} diagram generated: #{result.path}", :green
131
+ report_warnings(result)
132
+ end
133
+
134
+ def report_warnings(result)
135
+ return unless options[:verbose]
136
+
137
+ result.warnings.each { |warning| say " Warning: #{warning}", :yellow }
138
+ end
110
139
  end
111
140
  end
112
141
  end
@@ -2,19 +2,55 @@
2
2
 
3
3
  module Lagoon
4
4
  class Configuration
5
- attr_accessor :output_dir, :diagram_direction, :show_attributes,
6
- :show_methods, :include_inheritance, :exclude_models,
7
- :exclude_controllers, :diagram_format
5
+ DIRECTIONS = %w[TB BT LR RL].freeze
6
+
7
+ attr_accessor :show_attributes, :show_methods, :include_inheritance,
8
+ :exclude_models, :exclude_controllers, :exclude_tables,
9
+ :internal_tables, :include_framework_bases, :strict,
10
+ :helper_models
11
+ attr_reader :output_dir, :diagram_direction
8
12
 
9
13
  def initialize
10
- @output_dir = "doc/diagrams"
11
- @diagram_direction = "TB"
14
+ self.output_dir = 'doc/diagrams'
15
+ self.diagram_direction = 'TB'
12
16
  @show_attributes = true
13
17
  @show_methods = false
14
18
  @include_inheritance = true
15
19
  @exclude_models = []
16
20
  @exclude_controllers = []
17
- @diagram_format = :class_diagram # or :er_diagram
21
+ @exclude_tables = []
22
+ @internal_tables = %w[schema_migrations ar_internal_metadata]
23
+ @include_framework_bases = false
24
+ @strict = false
25
+ @helper_models = { 'current_user' => 'User' }
26
+ end
27
+
28
+ def output_dir=(value)
29
+ path = value.to_s
30
+ raise ConfigurationError, 'Output directory cannot be empty' if path.empty?
31
+
32
+ @output_dir = path
33
+ end
34
+
35
+ def diagram_direction=(value)
36
+ @diagram_direction = self.class.validate_direction!(value)
37
+ end
38
+
39
+ def self.validate_direction!(value)
40
+ direction = value.to_s.upcase
41
+ return direction if DIRECTIONS.include?(direction)
42
+
43
+ raise ConfigurationError,
44
+ "Invalid diagram direction #{value.inspect}; expected one of #{DIRECTIONS.join(', ')}"
45
+ end
46
+
47
+ def initialize_copy(original)
48
+ super
49
+ @exclude_models = original.exclude_models.dup
50
+ @exclude_controllers = original.exclude_controllers.dup
51
+ @exclude_tables = original.exclude_tables.dup
52
+ @internal_tables = original.internal_tables.dup
53
+ @helper_models = original.helper_models.dup
18
54
  end
19
55
  end
20
56
  end
@@ -1,37 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
4
+ require 'tempfile'
5
+
3
6
  module Lagoon
4
7
  module Diagram
5
8
  class Base
6
9
  attr_reader :options, :config
7
10
 
8
- def initialize(options = {})
9
- @options = options
10
- @config = Lagoon.configuration
11
+ def initialize(options = nil, configuration: Lagoon.configuration.dup, **keyword_options)
12
+ @config = configuration
13
+ raw_options = (options || {}).to_h.merge(keyword_options)
14
+ @options = Options.for(diagram_kind, raw_options, config: @config)
11
15
  end
12
16
 
13
17
  def generate
14
- raise NotImplementedError, "Subclasses must implement #generate"
18
+ parsed_data = parser.parse
19
+ content = renderer.render(parsed_data)
20
+ path = write_to_file(content)
21
+ Result.new(
22
+ path: path,
23
+ content: content,
24
+ warnings: parsed_data.fetch(:warnings, []),
25
+ counts: parsed_data.fetch(:counts, infer_counts(parsed_data))
26
+ )
15
27
  end
16
28
 
17
29
  protected
18
30
 
19
31
  def output_path
20
- @output_path ||= File.join(@config.output_dir, default_filename)
32
+ @output_path ||= @options[:output] || File.join(@config.output_dir, default_filename)
33
+ end
34
+
35
+ def diagram_kind
36
+ raise NotImplementedError, 'Subclasses must implement #diagram_kind'
37
+ end
38
+
39
+ def parser
40
+ raise NotImplementedError, 'Subclasses must implement #parser'
41
+ end
42
+
43
+ def renderer
44
+ raise NotImplementedError, 'Subclasses must implement #renderer'
21
45
  end
22
46
 
23
47
  def default_filename
24
- raise NotImplementedError, "Subclasses must implement #default_filename"
48
+ raise NotImplementedError, 'Subclasses must implement #default_filename'
25
49
  end
26
50
 
27
51
  def ensure_output_directory
28
- FileUtils.mkdir_p(@config.output_dir)
52
+ FileUtils.mkdir_p(File.dirname(output_path))
29
53
  end
30
54
 
31
55
  def write_to_file(content)
32
56
  ensure_output_directory
33
- File.write(output_path, content)
57
+ Tempfile.create(['.lagoon', '.tmp'], File.dirname(output_path)) do |file|
58
+ file.write(content)
59
+ file.flush
60
+ file.fsync
61
+ file.close
62
+ File.rename(file.path, output_path)
63
+ end
34
64
  output_path
65
+ rescue SystemCallError => e
66
+ raise OutputError, "Failed to write #{output_path}: #{e.message}"
67
+ end
68
+
69
+ def infer_counts(parsed_data)
70
+ {
71
+ classes: parsed_data.fetch(:classes, []).size,
72
+ entities: parsed_data.fetch(:entities, []).size,
73
+ relationships: parsed_data.fetch(:relationships, []).size
74
+ }.reject { |_key, count| count.zero? }
35
75
  end
36
76
  end
37
77
  end
@@ -1,30 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fileutils"
4
-
5
3
  module Lagoon
6
4
  module Diagram
7
5
  class ControllerDiagram < Base
8
- def generate
9
- parser = Parser::ControllerParser.new(@options)
10
- parsed_data = parser.parse
11
-
12
- renderer = Renderer::ClassDiagramRenderer.new(
13
- direction: @options[:direction] || @config.diagram_direction
14
- )
15
- content = renderer.render(parsed_data)
6
+ protected
16
7
 
17
- output_file = @options[:output] || output_path
18
- ensure_output_directory
19
- File.write(output_file, content)
8
+ def diagram_kind
9
+ :controller
10
+ end
20
11
 
21
- output_file
12
+ def parser
13
+ @parser ||= Parser::ControllerParser.new(@options)
22
14
  end
23
15
 
24
- protected
16
+ def renderer
17
+ @renderer ||= Renderer::ClassDiagramRenderer.new(direction: @options[:direction])
18
+ end
25
19
 
26
20
  def default_filename
27
- "controllers.mermaid"
21
+ 'controllers.mermaid'
28
22
  end
29
23
  end
30
24
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lagoon
4
+ module Diagram
5
+ class ControllerModelDiagram < Base
6
+ protected
7
+
8
+ def diagram_kind
9
+ :controller_model
10
+ end
11
+
12
+ def parser
13
+ @parser ||= Parser::ControllerModelParser.new(@options)
14
+ end
15
+
16
+ def renderer
17
+ @renderer ||= Renderer::ControllerModelErRenderer.new(
18
+ direction: @options[:direction],
19
+ show_actions: @options[:show_actions]
20
+ )
21
+ end
22
+
23
+ def default_filename
24
+ 'controller_models.mermaid'
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,30 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fileutils"
4
-
5
3
  module Lagoon
6
4
  module Diagram
7
5
  class ErDiagram < Base
8
- def generate
9
- parser = Parser::SchemaParser.new(@options)
10
- parsed_data = parser.parse
11
-
12
- renderer = Renderer::ErDiagramRenderer.new(
13
- direction: @options[:direction] || @config.diagram_direction
14
- )
15
- content = renderer.render(parsed_data)
6
+ protected
16
7
 
17
- output_file = @options[:output] || output_path
18
- ensure_output_directory
19
- File.write(output_file, content)
8
+ def diagram_kind
9
+ :er
10
+ end
20
11
 
21
- output_file
12
+ def parser
13
+ @parser ||= Parser::SchemaParser.new(@options)
22
14
  end
23
15
 
24
- protected
16
+ def renderer
17
+ @renderer ||= Renderer::ErDiagramRenderer.new
18
+ end
25
19
 
26
20
  def default_filename
27
- "er_diagram.mermaid"
21
+ 'er_diagram.mermaid'
28
22
  end
29
23
  end
30
24
  end
@@ -1,30 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fileutils"
4
-
5
3
  module Lagoon
6
4
  module Diagram
7
5
  class ModelDiagram < Base
8
- def generate
9
- parser = Parser::ModelParser.new(@options)
10
- parsed_data = parser.parse
11
-
12
- renderer = Renderer::ClassDiagramRenderer.new(
13
- direction: @options[:direction] || @config.diagram_direction
14
- )
15
- content = renderer.render(parsed_data)
6
+ protected
16
7
 
17
- output_file = @options[:output] || output_path
18
- ensure_output_directory
19
- File.write(output_file, content)
8
+ def diagram_kind
9
+ :model
10
+ end
20
11
 
21
- output_file
12
+ def parser
13
+ @parser ||= Parser::ModelParser.new(@options)
22
14
  end
23
15
 
24
- protected
16
+ def renderer
17
+ @renderer ||= Renderer::ClassDiagramRenderer.new(
18
+ direction: @options[:direction],
19
+ show_types: !@options[:hide_types]
20
+ )
21
+ end
25
22
 
26
23
  def default_filename
27
- "models.mermaid"
24
+ 'models.mermaid'
28
25
  end
29
26
  end
30
27
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lagoon
4
+ class Error < StandardError; end
5
+ class ConfigurationError < Error; end
6
+ class RailsLoadError < Error; end
7
+ class ParseError < Error; end
8
+ class OutputError < Error; end
9
+ end