Попытка понять 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