Nombres de URL y reverse
Codificar URLs a mano (/posts/42) por todos los handlers y templates es frágil —
cambia una ruta y cada literal se rompe en silencio. Rustango te da la
respuesta de Django: nombra un patrón de URL una vez, luego construye la URL por
su nombre en todas partes — en Rust con reverse(...), en templates con
{{ url(...) }}, y en redirecciones con redirect_to_view(...). La superficie de
la API refleja los reverse() / {% url %} / resolve_url() / redirect() de
Django.
Fuente:
rustango::urls(register_url!,reverse,reverse_owned,all_routes,duplicates,register_url_tag) yrustango::shortcuts(resolve_url,redirect_to_view).
¿Nuevo con algún término aquí? ruta, reverse, namespacing — consulta el glosario.
Tabla de contenidos
- Registrar una URL con nombre
- Reverse en Rust · Reverse en templates
- Redirigir por nombre · Namespacing
- Inspeccionar el mapa de URLs · Errores
- Patrones regex y de ruta tipados · Notas y límites
Registrar una URL con nombre
register_url!("name", "/pattern") registra un mapeo nombre → patrón. Se ejecuta
al cargar el módulo (vía inventory), así que la ruta aterriza en un registro
global en el momento en que su módulo se enlaza — sin urls.py central que editar,
y sin include() que cablear.
use rustango::register_url;
register_url!("post-detail", "/posts/{id}");
register_url!("user-posts", "/users/{user_id}/posts/{post_id}");
register_url!("home", "/");
Los placeholders usan la sintaxis de ruta {name} de axum. El patrón es la misma
cadena en la que montas el handler — mantenlos sincronizados (registra el nombre
junto a donde construyes la ruta).
Reverse en Rust
reverse(name, ¶ms) sustituye los {placeholders} del patrón por los valores
dados (percent-encodando cada uno) y devuelve la URL:
use std::collections::HashMap;
use rustango::urls::reverse;
let mut params = HashMap::new();
params.insert("id", "42".to_string());
let url = reverse("post-detail", ¶ms)?; // → "/posts/42"
Para claves dinámicas (p. ej. valores ensamblados a partir de una petición),
reverse_owned toma HashMap<String, String> en lugar de
HashMap<&str, String>:
use rustango::urls::reverse_owned;
let url = reverse_owned("post-detail", &owned_params)?;
reverse es estricto: un placeholder faltante, o una clave params extra que
el patrón no tiene, es un error (no un desajuste silencioso) — consulta
Errores.
Reverse en templates
Los templates obtienen el {% url %} de Django como función Tera. Regístrala una
vez en tu instancia Tera durante el setup (está detrás del feature
template_views):
rustango::urls::register_url_tag(&mut tera);
Luego llama a url(name=..., <param>=...) en cualquier template — name es
obligatorio, y cada otro argumento con nombre es un parámetro de ruta (se aceptan
cadenas, números y booleanos):
<a href="{{ url(name='post-detail', id=42) }}">View post</a>
<a href="{{ url(name='user-posts', user_id=7, post_id=42) }}">…</a>
Ese es el equivalente del {% url 'post-detail' id=42 %} de Django. Para el patrón
de captura {% url 'x' as var %}, usa el {% set %} de Tera:
{% set post_url = url(name='post-detail', id=post.id) %}
<a href="{{ post_url }}">{{ post.title }}</a>
Un argumento null (normalmente una variable de template no definida) falla
ruidosamente en lugar de producir en silencio una URL rota.
Redirigir por nombre
rustango::shortcuts refleja los helpers de redirección por nombre de vista de
Django, de modo que los handlers nunca codifican un Location a mano:
use std::collections::HashMap;
use rustango::shortcuts::{redirect_to_view, resolve_url};
// redirect('post-detail', id=42) → 302 Location: /posts/42
let mut params = HashMap::new();
params.insert("id", "42".to_string());
let response = redirect_to_view("post-detail", ¶ms)?;
resolve_url(spec, ¶ms) es el resolve_url de Django: si spec ya parece una
URL (/…, http://, https://, ./, ../) se devuelve sin cambios; en caso
contrario se trata como un nombre de ruta y se resuelve por reverse. Útil para un
parámetro ?next= o un ajuste que pueda contener o bien una ruta o bien un
nombre:
let url = resolve_url("post-detail", ¶ms)?; // name → "/posts/42"
let url = resolve_url("/dashboard", ¶ms)?; // path → "/dashboard" (as-is)
(Para redirecciones crudas a una URL conocida, rustango::shortcuts::redirect(url)
devuelve un simple 302.)
Namespacing
No hay include() ni un namespace de app auto-aplicado — cada register_url!
aterriza en un único registro global. El namespacing es una convención en el
nombre mismo: prefija con app:, exactamente como llamarías al
reverse("app:detail") de Django.
register_url!("blog:post-detail", "/blog/posts/{id}");
register_url!("shop:product", "/shop/products/{slug}");
reverse("blog:post-detail", ¶ms)?; // "/blog/posts/42"
Los dos puntos son simplemente parte de la cadena registrada — elige un prefijo consistente por app para evitar colisiones.
Inspeccionar el mapa de URLs
Lista cada ruta registrada desde la CLI — útil para una auditoría rápida o para scriptear:
cargo run -- showurls # plain table of name → pattern
cargo run -- showurls --format json # machine-readable
En código, all_routes() devuelve todo el registro, y duplicates() devuelve
cualquier nombre registrado más de una vez (gana el primero en caso contrario —
conviene aseverarlo al arrancar):
use rustango::urls::{all_routes, duplicates};
for route in all_routes() {
println!("{} → {}", route.name, route.pattern);
}
let dups = duplicates();
assert!(dups.is_empty(), "duplicate URL names: {dups:?}");
Errores
reverse / reverse_owned / resolve_url / redirect_to_view devuelven
Result<_, rustango::urls::ReverseError>:
| Variante | Cuándo |
|---|---|
UnknownName(name) | Ningún register_url! se ejecutó para ese nombre (errata, o su módulo no se enlazó). |
MissingParam { name, param } | El patrón tiene {param} pero params no lo proporcionó. |
UnexpectedParam { name, param } | params llevaba una clave que el patrón no tiene (atrapa erratas). |
MalformedPattern { name, detail } | El patrón registrado está malformado (p. ej. una { sin cerrar). |
En los templates estos afloran como errores de render de Tera (un 500 vía
shortcuts::render / template_views), así que un {{ url(...) }} erróneo falla
de forma visible en lugar de renderizar un enlace roto.
Patrones regex y de ruta tipados
Rustango no tiene re_path, y nunca se aplica ningún convertidor de ruta. Un
segmento de patrón es o bien un literal (/posts/new) o un placeholder {name}
que captura exactamente un segmento; {*name} captura el resto de la ruta. Ese es
todo el vocabulario — no hay r'(?P<year>[0-9]{4})', y {int:id} no restringe
id a un entero.
Por qué — el matcher no es un motor de regex
El enrutamiento es axum 0.8, y axum empareja rutas con
matchit, un router de radix-trie (árbol radix).
Recorre la URL un segmento a la vez por un árbol de prefijos, de modo que un match
cuesta O(longitud de la ruta) y es independiente de cuántas rutas hayas registrado.
Un router de regex hace lo contrario: Django evalúa urlpatterns de arriba abajo,
ejecutando la regex de cada entrada contra la ruta hasta que una coincide. El trie
compra emparejamiento en tiempo constante y una precedencia inequívoca de «gana el
literal más específico» — a costa de no expresar restricciones de clase de
caracteres en la ruta misma.
Rustango hereda ese matcher por completo. No hay un segundo resolvedor basado en
regex superpuesto, y register_url! registra deliberadamente las mismas
cadenas {name} que el router ya entiende — nunca compila una regex. Así que las
rutas regex no están «apagadas»; la capa de enrutamiento simplemente nunca fue un
motor de regex de entrada.
La forma {int:id} se acepta solo como facilidad de portado para reverse():
el constructor divide el placeholder por : y conserva solo el nombre, descartando
el prefijo de tipo (urls.rs). Eso permite que
reverse() funcione sobre un patrón copiado literalmente de un
path("<int:id>/", …) de Django — pero nada valida que el valor suministrado sea
realmente un entero.
Cómo expresar una ruta restringida
Empareja el segmento con un simple {placeholder}, luego impón su forma donde se
usa el valor. El re_path(r'^articles/(?P<year>[0-9]{4})/$', …) de Django se
convierte en:
register_url!("article-by-year", "/articles/{year}");
// router:
.route("/articles/{year}", get(article_by_year))
async fn article_by_year(Path(year): Path<String>) -> impl IntoResponse {
// the router accepted any single segment; enforce [0-9]{4} here
match year.parse::<u16>() {
Ok(y) if (1000..=9999).contains(&y) => render_year(y).await,
_ => StatusCode::NOT_FOUND.into_response(),
}
}
Para rechazar antes de que el handler se ejecute (más cerca de la semántica de
convertidores de Django), pon la comprobación en un extractor axum personalizado
(FromRequestParts) y toma ese tipo como argumento del handler en lugar de
Path<String> — el framework no incluye ninguno, pero el trait de extractor de
axum es la costura prevista. El crate regex ya es una dependencia (el ORM lo usa
para los lookups __regex), así que un extractor validador puede compilar una
Regex una vez y reutilizarla entre peticiones.
Notas y límites
- El registro es en tiempo de enlace. Un
register_url!solo surte efecto si su módulo se compila en el binario. Un errorUnknownNamenormalmente significa que el nombre es una errata o que su módulo no se referencia en ningún sitio (así que el linker lo descartó). - Los patrones no se validan contra tus rutas reales.
register_url!registra un mapeo nombre → cadena; no comprueba que haya realmente un handler montado en ese patrón. Registra el nombre junto a donde montas la ruta para que se mantengan sincronizados. - Los valores se percent-encodan con
reverse, así que son seguros para colocar en un headerLocationo unhref. - Sin convertidores regex/tipados en los patrones (el
<int:pk>de Django); los placeholders son simples{name}y los valores se sustituyen tal cual (después de codificar). Consulta Patrones regex y de ruta tipados para el porqué, y cómo restringir una ruta en su lugar.
