Попытка понять Journey::Path::Pattern#spec (внутренняя маршрутизация Rails)

Контекст: я решаю проблему, когда мне нужна программа внешнего аудита, чтобы понимать и "применять" маршруты Rails. Одним из вариантов написания этой внешней программы может быть разбор выходных данных rake routes, но это привело бы к ненужному дублированию кода, который анализирует эти маршруты и преобразует их в хорошо структурированные Journey::Route объекты.

Поэтому мой план - вывести Rails.application.routes в общий формат (YAML или JSON), который может понять внешняя программа, и может построить маршрутизатор на основе этих данных.

Вопрос: Учитывая этот контекст, я пытаюсь понять структуру Journey::Path::Paternet#spec атрибут, который встречается внутри Journey::Route объект, и оказывается центром всего действия.

Например, следующий маршрут - /posts/:id - конвертируется в следующую "спецификацию" -

 #<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>>
  • Каковы левые / правые атрибуты в Journey::Nodes::Cat объект? Что решает, какой токен будет "левым", а какой токен "правым"
  • Это выглядит подозрительно как двоичное дерево, но почему самый первый токен (т.е. первый /) "самый внутренний" (или листовой узел)? Разве это не должно быть "самым внешним" (или корневым узлом)?
  • Как эффективный способ пройтись по этой структуре данных при выполнении сопоставления маршрутов?

1 ответ

Путешествие основано на Finite State Machine, который соответствует маршруту, есть встроенный визуализатор (требуется графвиз):

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

Journey::Nodes::Cat это только один из типов узлов, которые вы можете встретить, это двоичный узел, который соответствует expressions Правило в грамматике пути , см. parser.y, слева первый expression, right это все остальные, это производит цикл, который потребляет все выражения.

Другие соображения по поводу анализа внешних маршрутов. Маршруты нельзя выгружать в статический файл в общем случае, поскольку они могут содержать:

  • динамические ограничения с не чистыми функциями (например, get :r, constraints: ->{rand(2)>0} Идея состоит в том, что результат может зависеть от чего-то вне запроса, времени или состояния и т. д.) Если они присутствуют - даже сам маршрутизатор rails может выдавать другой результат при втором запуске по одному и тому же запросу.

  • приложения для смонтированных стоек - могут иметь жестко закодированные или не рельсовые маршрутизаторы

  • rails engine - есть rails router, что проще, чем обычные приложения в стойке, но обман с точками монтирования и слиянием с основной областью приложения

Но для простого случая вы можете подключиться к рельсам ActionDispatch::Routing::RoutesInspector который используется для rake routes и получить информацию о структурированных маршрутах, которая лучше, чем просто анализ последнего вывода.

В жемчужине routes_coverage Я сделал это так:

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
Другие вопросы по тегам