Class: Rigor::ModuleGraph::CLI::View
- Inherits:
-
Object
- Object
- Rigor::ModuleGraph::CLI::View
- Includes:
- EdgeFilters
- Defined in:
- lib/rigor/module_graph/cli.rb
Overview
‘view` is the one-shot entry point: from the project root type `rigor-module-graph` and it analyses the current directory, writes a self-contained Mermaid HTML report, and opens it in a browser.
Defaults are tuned to need zero flags on a Rails-shaped project. The lower-level subcommands (collect / dot / mermaid) stay available for piped use.
Defined Under Namespace
Classes: RenderError
Constant Summary collapse
- DEFAULT_OUTPUT =
".rigor/module_graph/view.html"- AUTO_COLLAPSE_THRESHOLD =
An auto-collapsed cluster needs at least this many members before it’s worth folding. Three is the sweet spot empirically: a 1500-edge Rails app collapses into roughly the right shape, and a small fixture still leaves trivial Foo / Bar pairs uncollapsed.
3- SUBTITLE_COLLAPSE_PREVIEW =
Cap the visible “collapsed: …” trailer in the subtitle so it doesn’t grow into an unreadable wall on large projects.
6- FORMATS =
The supported output formats, in roughly increasing “wrapping” order: html embeds mermaid; svg embeds dot; the rest are raw text.
%w[html mermaid dot svg class-diagram].freeze
- DEFAULT_HTML_OUTPUT =
Default file destination when format is html and the user didn’t override with -o. Non-html formats default to stdout.
".rigor/module_graph/view.html"
Constants included from EdgeFilters
EdgeFilters::VALID_CONFIDENCES, EdgeFilters::VALID_DIRECTIONS, EdgeFilters::VALID_EDGE_SCOPES, EdgeFilters::VALID_KINDS
Instance Method Summary collapse
- #build_parser ⇒ Object
-
#deliver(payload, binary:, edges:) ⇒ Object
Writes the payload to the configured destination and opens the browser when the html-default flow applies.
-
#effective_collapse(edges) ⇒ Object
Choose collapse prefixes.
-
#effective_output_path ⇒ Object
Resolve the output path.
-
#graphviz_svg(dot_source) ⇒ Object
Shell out to Graphviz ‘dot -Tsvg`.
- #html? ⇒ Boolean
-
#initialize(stdout:, stderr:) ⇒ View
constructor
A new instance of View.
- #open_in_browser(path) ⇒ Object
- #package_groups(edges) ⇒ Object
-
#render_payload(edges, nodes, collapse, groups) ⇒ Object
Builds the rendered payload for the chosen format and signals whether the bytes are binary (svg via Graphviz can return a non-UTF-8 image stream).
- #render_subtitle(edges, collapse, groups) ⇒ Object
-
#restrict_nodes_to_edges(nodes, edges) ⇒ Object
When the user narrows the edge set with ‘–from` / `–kind` / `–confidence`, the class diagram should only show classes that participate in those edges — otherwise every constant declared in the project still shows up as a body-less class.
- #run(argv) ⇒ Object
Methods included from EdgeFilters
#add_filter_options, #apply_filters, #validate!
Constructor Details
#initialize(stdout:, stderr:) ⇒ View
Returns a new instance of View.
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 |
# File 'lib/rigor/module_graph/cli.rb', line 361 def initialize(stdout:, stderr:) @stdout = stdout @stderr = stderr @options = { format: "html", output: nil, cache: false, rigor_cmd: ENV.fetch("RIGOR_CMD", "rigor"), open: true, collapse: nil, kinds: nil, confidences: nil, from: nil, depth: nil, direction: :both, edge_scope: :cluster, package: nil, include_methods: true, include_attributes: true, visibilities: %w[public protected private] } end |
Instance Method Details
#build_parser ⇒ Object
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 |
# File 'lib/rigor/module_graph/cli.rb', line 510 def build_parser OptionParser.new do |opts| opts. = "Usage: rigor-module-graph view [options] [PATHS...]" opts.on("--output FORMAT", FORMATS, "Output format (#{FORMATS.join("|")}; default: html). " \ "Non-html streams to stdout unless -o is given.") do |fmt| @options[:format] = fmt end opts.on("-o", "--save PATH", "Write to PATH instead of stdout / the default html location") do |path| @options[:output] = path end opts.on("--[no-]open", "Open the html in a browser (default: true; ignored for non-html)") do |flag| @options[:open] = flag end opts.on("--collapse PREFIXES", Array, "Manual collapse list (disables auto-detection)") do |prefixes| @options[:collapse] = prefixes end opts.on("--no-collapse", "Disable namespace collapse entirely") do @options[:collapse] = [] end opts.on("--no-methods", "[class-diagram] Don't render methods inside class bodies") do @options[:include_methods] = false end opts.on("--no-attributes", "[class-diagram] Don't render attributes inside class bodies") do @options[:include_attributes] = false end opts.on("--public-only", "[class-diagram] Only show public members") do @options[:visibilities] = %w[public] end opts.on("--no-private", "[class-diagram] Hide private members") do @options[:visibilities] = %w[public protected] end opts.on("--package", "Cluster by Packwerk packages discovered in cwd") do @options[:package] ||= "." end opts.on("--package-root PATH", "Cluster by Packwerk packages discovered under PATH") do |root| @options[:package] = root end opts.on("--[no-]cache", "Pass --cache / --no-cache to rigor (default: --no-cache)") do |cache| @options[:cache] = cache end opts.on("--rigor-cmd CMD", "Override the rigor binary (default: rigor or $RIGOR_CMD)") do |cmd| @options[:rigor_cmd] = cmd end (opts, @options) opts.on("-h", "--help") do @stdout.puts opts exit 0 end end end |
#deliver(payload, binary:, edges:) ⇒ Object
Writes the payload to the configured destination and opens the browser when the html-default flow applies.
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 |
# File 'lib/rigor/module_graph/cli.rb', line 478 def deliver(payload, binary:, edges:) destination = effective_output_path if destination.nil? if binary @stdout.binmode end @stdout.write(payload) return end dir = File.dirname(destination) FileUtils.mkdir_p(dir) unless dir.empty? || dir == "." mode = binary ? "wb" : "w" File.open(destination, mode) { |io| io.write(payload) } @stderr.puts "rigor-module-graph: wrote #{edges.size} edge(s) to #{destination}" open_in_browser(destination) if html? && @options[:open] end |
#effective_collapse(edges) ⇒ Object
Choose collapse prefixes. Explicit ‘–collapse` wins; otherwise we auto-pick top-level namespaces that have at least AUTO_COLLAPSE_THRESHOLD distinct nodes under them, which is what most graphs benefit from.
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 |
# File 'lib/rigor/module_graph/cli.rb', line 578 def effective_collapse(edges) return @options[:collapse] unless @options[:collapse].nil? counts = Hash.new { |h, k| h[k] = Set.new } edges.each do |edge| [edge.from, edge.to].each do |name| head, tail = name.split("::", 2) # Only collapse on the top-level segment so a deep # tree like `Billing::Invoice::Line` still feeds into # the `Billing` cluster — picking inner prefixes # would compete with each other and produce nested # clusters that hurt readability. next if tail.nil? || tail.empty? # Absolute paths (`::Foo::Bar`) split with an empty # head; skip them so they don't surface as the bogus # `""` collapse target. next if head.empty? counts[head] << name end end counts.select { |_, members| members.size >= AUTO_COLLAPSE_THRESHOLD }.keys.sort end |
#effective_output_path ⇒ Object
Resolve the output path. ‘-o PATH` always wins. With no explicit path, html falls back to `.rigor/module_graph/ view.html`; every other format streams to stdout.
499 500 501 502 503 504 |
# File 'lib/rigor/module_graph/cli.rb', line 499 def effective_output_path return @options[:output] if @options[:output] return DEFAULT_HTML_OUTPUT if html? nil end |
#graphviz_svg(dot_source) ⇒ Object
Shell out to Graphviz ‘dot -Tsvg`. Surfacing the binary check as a clear error keeps the message friendlier than the raw `Errno::ENOENT` Open3 would propagate.
464 465 466 467 468 469 470 471 472 473 474 |
# File 'lib/rigor/module_graph/cli.rb', line 464 def graphviz_svg(dot_source) stdout_str, stderr_str, status = Open3.capture3("dot", "-Tsvg", stdin_data: dot_source) unless status.success? raise RenderError, "graphviz `dot` failed (exit #{status.exitstatus}): #{stderr_str}" end stdout_str rescue Errno::ENOENT raise RenderError, "graphviz `dot` not found on PATH; install via " \ "`brew install graphviz` (macOS) or your distro's package manager" end |
#html? ⇒ Boolean
506 507 508 |
# File 'lib/rigor/module_graph/cli.rb', line 506 def html? @options[:format] == "html" end |
#open_in_browser(path) ⇒ Object
640 641 642 643 644 645 646 |
# File 'lib/rigor/module_graph/cli.rb', line 640 def open_in_browser(path) opener = ENV["BROWSER"] || (RUBY_PLATFORM.include?("darwin") ? "open" : "xdg-open") system(opener, path) rescue StandardError => e @stderr.puts "rigor-module-graph view: could not open #{path}: #{e.}" end |
#package_groups(edges) ⇒ Object
627 628 629 630 631 632 633 634 635 636 637 638 |
# File 'lib/rigor/module_graph/cli.rb', line 627 def package_groups(edges) return nil unless @options[:package] = PackwerkOverlay.discover(@options[:package]) unless .any? @stderr.puts "rigor-module-graph view: no package.yml found under " \ "#{@options[:package].inspect}; falling back to namespace collapse" return nil end .groups_for(edges) end |
#render_payload(edges, nodes, collapse, groups) ⇒ Object
Builds the rendered payload for the chosen format and signals whether the bytes are binary (svg via Graphviz can return a non-UTF-8 image stream).
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 |
# File 'lib/rigor/module_graph/cli.rb', line 418 def render_payload(edges, nodes, collapse, groups) case @options[:format] when "html" mermaid = Mermaid.render(edges, collapse: collapse, groups: groups) html = HtmlView.render( title: "rigor-module-graph: #{File.basename(Dir.pwd)}", subtitle: render_subtitle(edges, collapse, groups), mermaid_source: mermaid ) [html, false] when "mermaid" [Mermaid.render(edges, collapse: collapse, groups: groups), false] when "dot" [Dot.render(edges, collapse: collapse, groups: groups), false] when "svg" [graphviz_svg(Dot.render(edges, collapse: collapse, groups: groups)), true] when "class-diagram" [ Uml::ClassDiagram.render( edges, restrict_nodes_to_edges(nodes, edges), include_methods: @options[:include_methods], include_attributes: @options[:include_attributes], visibilities: @options[:visibilities] ), false ] end end |
#render_subtitle(edges, collapse, groups) ⇒ Object
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 |
# File 'lib/rigor/module_graph/cli.rb', line 602 def render_subtitle(edges, collapse, groups) parts = ["#{edges.size} edge(s) from #{Dir.pwd}"] if @options[:from] from_part = +"from: #{Array(@options[:from]).join(", ")}" from_part << " (depth=#{@options[:depth]})" if @options[:depth] from_part << " [#{@options[:direction]}]" unless @options[:direction] == :both parts << from_part end if groups uniq_packages = groups.values.uniq.sort preview = uniq_packages.first(SUBTITLE_COLLAPSE_PREVIEW) label = +"packages: #{preview.join(", ")}" if uniq_packages.size > preview.size label << " (+#{uniq_packages.size - preview.size} more)" end parts << label elsif !collapse.empty? preview = collapse.first(SUBTITLE_COLLAPSE_PREVIEW) label = +"collapsed: #{preview.join(", ")}" label << " (+#{collapse.size - preview.size} more)" if collapse.size > preview.size parts << label end parts.join(" · ") end |
#restrict_nodes_to_edges(nodes, edges) ⇒ Object
When the user narrows the edge set with ‘–from` / `–kind` / `–confidence`, the class diagram should only show classes that participate in those edges — otherwise every constant declared in the project still shows up as a body-less class. The filter is a no-op when the edge set already covers every node (no filters applied).
453 454 455 456 457 458 459 |
# File 'lib/rigor/module_graph/cli.rb', line 453 def restrict_nodes_to_edges(nodes, edges) return nodes if edges.empty? visible = Set.new edges.each { |edge| visible << edge.from << edge.to } nodes.select { |node| visible.include?(node.owner) || visible.include?(node.name) } end |
#run(argv) ⇒ Object
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/rigor/module_graph/cli.rb', line 384 def run(argv) parser = build_parser paths = parser.parse(argv) runner = RigorRunner.new(rigor_cmd: @options[:rigor_cmd], cache: @options[:cache]) edges, nodes = runner.analyse(paths) edges = apply_filters( edges, kinds: @options[:kinds], confidences: @options[:confidences], from: @options[:from], depth: @options[:depth], direction: @options[:direction], edge_scope: @options[:edge_scope] ) groups = package_groups(edges) collapse = groups ? [] : effective_collapse(edges) payload, binary = render_payload(edges, nodes, collapse, groups) deliver(payload, binary: binary, edges: edges) 0 rescue OptionParser::ParseError => e @stderr.puts "rigor-module-graph view: #{e.}" 2 rescue CollectError, RenderError => e @stderr.puts "rigor-module-graph view: #{e.}" 1 end |