Class: RailsMcpInsight::Analyzers::RouteAnalyzer

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_mcp_insight/analyzers/route_analyzer.rb

Overview

Parses config/routes.rb to extract all routes, supporting resources, namespaces, scopes, and individual route definitions.

Constant Summary collapse

ROUTE_METHODS =
["get", "post", "put", "patch", "delete", "root", "match"].freeze

Instance Method Summary collapse

Constructor Details

#initialize(config, ast_parser: AstParser.new) ⇒ RouteAnalyzer

Returns a new instance of RouteAnalyzer.



10
11
12
13
# File 'lib/rails_mcp_insight/analyzers/route_analyzer.rb', line 10

def initialize(config, ast_parser: AstParser.new)
  @config = config
  @ast_parser = ast_parser
end

Instance Method Details

#analyzeObject

Returns all routes found in the routes file(s)



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/rails_mcp_insight/analyzers/route_analyzer.rb', line 16

def analyze
  routes = []
  routes_file = @config.routes_file

  return routes unless File.exist?(routes_file)

  content = File.read(routes_file)
  lines = content.lines

  current_namespace = []
  current_scope = []

  lines.each_with_index do |line, idx|
    stripped = line.strip
    next if stripped.empty? || stripped.start_with?("#")

    # Track namespace/scope nesting
    if (match = stripped.match(/^\s*namespace\s+:(\w+)/))
      current_namespace.push(match[1])
    elsif (match = stripped.match(/^\s*scope\s+["']?([^"',\s]+)/))
      current_scope.push(match[1].sub(%r{^/}, ""))
    end

    # Match resources/resource
    if (match = stripped.match(/^\s*resources?\s+:(\w+)/))
      resource_name = match[1]
      prefix = build_prefix(current_namespace, current_scope)
      routes.concat(generate_resource_routes(resource_name, prefix, idx + 1, stripped.start_with?("resource ")))
    end

    # Match individual routes
    ROUTE_METHODS.each do |method|
      unless (match = stripped.match(/^\s*#{method}\s+["']([^"']+)["'](?:.*?(?:to:|=>)\s*["']([^"']+)["'])?/))
        next
      end

      path = match[1]
      controller_action = match[2]
      prefix = build_prefix(current_namespace, current_scope)
      full_path = "/#{prefix}#{path}".gsub(%r{//+}, "/")

      routes << {
        method: method.upcase == "MATCH" ? "ANY" : method.upcase,
        path: full_path,
        controller_action: controller_action || infer_controller_action(path, method),
        line: idx + 1,
        source: "config/routes.rb"
      }
    end

    # Root route
    if (match = stripped.match(/^\s*root\s+(?:to:\s*)?["']([^"']+)["']/))
      routes << {
        method: "GET",
        path: "/#{build_prefix(current_namespace, current_scope)}".gsub(%r{//+}, "/"),
        controller_action: match[1],
        line: idx + 1,
        source: "config/routes.rb"
      }
    end

    # Track end of blocks
    if stripped == "end"
      # Pop the most recent nesting (simplified — works for most cases)
      if current_namespace.length > current_scope.length
        current_namespace.pop unless current_namespace.empty?
      else
        current_scope.pop unless current_scope.empty?
      end
    end
  end

  routes
end

#search(query: nil, method: nil) ⇒ Object

Search routes by query string (matches path or controller_action)



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/rails_mcp_insight/analyzers/route_analyzer.rb', line 92

def search(query: nil, method: nil)
  all_routes = analyze

  results = all_routes
  if query
    query_downcase = query.downcase
    results = results.select do |route|
      route[:path].downcase.include?(query_downcase) ||
        route[:controller_action]&.downcase&.include?(query_downcase)
    end
  end

  if method
    method_up = method.upcase
    results = results.select { |route| route[:method] == method_up }
  end

  results
end