Trying to understand Journey::Path::Pattern#spec (internal of Rails routing)

271 Views Asked by At

Context: I'm solving a problem where I need an external audit program to be able to understand and "apply" Rails routes. One option for writing this external program could been to parse the output of rake routes, but that would unnecessarily end-up duplicating the code which parses these routes and converts them into well-structured Journey::Route objects.

Therefore, my plan is to output Rails.application.routes to a common format (YAML, or JSON), which the external program can understand, and can build a router based on this data.

Question: Given this context, I'm trying to understand the structure of the Journey::Path::Paternet#spec attribute, which occurs inside a Journey::Route object, and happens to be the center of all action.

For example, the following route - /posts/:id - gets converted to the following "spec" -

 #<Journey::Nodes::Cat:0x00007ff193327ee0
 @left=
  #<Journey::Nodes::Cat:0x00007ff193308630
   @left=
    #<Journey::Nodes::Cat:0x00007ff1933087e8
     @left=
      #<Journey::Nodes::Cat:0x00007ff193308bf8
       @left=#<Journey::Nodes::Slash:0x00007ff193308d38 @left="/", @memo=nil>,
       @memo=nil,
       @right=#<Journey::Nodes::Literal:0x00007ff193308c48 @left="posts", @memo=nil>>,
     @memo=nil,
     @right=#<Journey::Nodes::Slash:0x00007ff193308a40 @left="/", @memo=nil>>,
   @memo=nil,
   @right=#<Journey::Nodes::Symbol:0x00007ff1933086d0 @left=":id", @memo=nil, @regexp=/[^\.\/\?]+/>>,
 @memo=nil,
 @right=
  #<Journey::Nodes::Group:0x00007ff193309c10
   @left=
    #<Journey::Nodes::Cat:0x00007ff193308220
     @left=#<Journey::Nodes::Dot:0x00007ff1933084f0 @left=".", @memo=nil>,
     @memo=nil,
     @right=#<Journey::Nodes::Symbol:0x00007ff193308338 @left=":format", @memo=nil, @regexp=/[^\.\/\?]+/>>,
   @memo=nil>>
  • What are the left/right attributes in a Journey::Nodes::Cat object? What decides which token will be "left" and which token will be "right"
  • This looks suspiciously like a binary tree, but why is the very first token (i.e. the first /), the "innermost" (or a leaf node)? Shouldn't it be the "outermost" (or the root node)?
  • What is an efficient way to walk down this data-structure while performing route matching?
1

There are 1 best solutions below

0
On

Journey is based on Finite State Machine that matches route, there's built-in visualizer (requires graphviz):

File.open('routes.html', 'wt'){|f| f.write Rails.application.routes.router.visualizer }

Journey::Nodes::Cat is only one of node types that you can encounter, it is binary node that matches expressions rule in path grammar, see parser.y, left is first expression, right is all others, this produces loop that consumes all expressions.

Other thoughts about external routes analysis: Routes cannot be dumped into a static file in a generic case because they can contain:

  • dynamic constraints with non-pure functions (for example - get :r, constraints: ->{rand(2)>0}, idea is that result can depend on something outside of request, time or state, etc.) When these are present - even rails router itself can produce different result on second run over the same request.

  • mounted rack apps - can have hardcoded or non-rails routers

  • rails engines - have rails router, so easier than generic rack apps, but trickery with mount points and merging into main app scope

But for simple case you can tap into rails' ActionDispatch::Routing::RoutesInspector which is used for rake routes and get structured routes info that is better that just parsing the latter output.

In gem routes_coverage I did this way:

class Inspector < ActionDispatch::Routing::RoutesInspector
  def collect_all_routes
    res = collect_routes(@routes)
    @engines.each do |engine_name, engine_routes|
      res += engine_routes.map{|er|
        er.merge({ engine_name: engine_name })
      }
    end
    res
  end

  def collect_routes(routes)
    routes.collect do |route|
      ActionDispatch::Routing::RouteWrapper.new(route)
    end.reject do |route|
      route.internal?
    end.collect do |route|
      collect_engine_routes(route)

      { name:   route.name,
        verb:   route.verb,
        path:   route.path,
        reqs:   route.reqs,
        original: route,
      }
    end
  end

res = Inspector.new(Rails.application.routes.routes.routes).collect_all_routes