✨ (core): Añade Route, RoutePath y Waypoint
`Route` resuelve URLs sensibles al `Context`/idioma en propiedades de componentes; `RoutePath` (renombrado desde html/route.rs) es el valor concreto de ruta + query. `Waypoint` transporta la URL de retorno entre pantallas de listado/alta/edición vía query string. Ajusta `Context::route()`, `Redirect` y los componentes que aceptan rutas para integrarse con el nuevo tipo `Route`.
This commit is contained in:
parent
6f82da220b
commit
af548b03c9
25 changed files with 819 additions and 216 deletions
|
|
@ -119,7 +119,7 @@ impl JavaScript {
|
|||
/// Equivale a `<script>...</script>`. El parámetro `name` se usa como identificador interno del
|
||||
/// script.
|
||||
///
|
||||
/// La función closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||
/// Un closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||
pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||
|
|
@ -139,7 +139,7 @@ impl JavaScript {
|
|||
///
|
||||
/// En condiciones normales, los scripts con `defer` se ejecutan antes de `DOMContentLoaded`.
|
||||
///
|
||||
/// La función closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||
/// Un closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||
pub fn on_load<F>(name: impl Into<CowStr>, f: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||
|
|
@ -153,11 +153,10 @@ impl JavaScript {
|
|||
/// Crea un **script embebido** con un **handler asíncrono**.
|
||||
///
|
||||
/// El código se envuelve en un `addEventListener('DOMContentLoaded',async()=>{...})`, que
|
||||
/// emplea una función `async` para que el cuerpo devuelto por la función closure pueda usar
|
||||
/// `await`. Ideal para hidratar la interfaz, cargar módulos dinámicos o realizar lecturas
|
||||
/// iniciales.
|
||||
/// emplea una función `async` para que el cuerpo devuelto por un closure pueda usar `await`.
|
||||
/// Ideal para hidratar la interfaz, cargar módulos dinámicos o realizar lecturas iniciales.
|
||||
///
|
||||
/// La función closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||
/// Un closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||
pub fn on_load_async<F>(name: impl Into<CowStr>, f: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ impl StyleSheet {
|
|||
/// Equivale a `<style>...</style>`. El parámetro `name` se usa como identificador interno del
|
||||
/// recurso.
|
||||
///
|
||||
/// La función closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||
/// Un closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||
pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ use crate::core::TypeInfo;
|
|||
use crate::html::maud::{Escaper, Render};
|
||||
use crate::{AutoDefault, CowStr, builder_fn, trace, util};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{self, Write};
|
||||
|
|
@ -37,16 +39,15 @@ impl fmt::Debug for PropsExtra {
|
|||
// **< PropsError >*********************************************************************************
|
||||
|
||||
/// Errores de acceso a valores extra de [`Props`].
|
||||
///
|
||||
/// - [`PropsError::ExtraNotFound`]: la clave no existe. Incluye la clave (`key`).
|
||||
/// - [`PropsError::ExtraTypeMismatch`]: la clave existe pero el tipo solicitado no coincide con el
|
||||
/// almacenado. Incluye la clave (`key`), tipo esperado (`expected`) y tipo realmente encontrado
|
||||
/// (`found`) para facilitar el diagnóstico.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, Error)]
|
||||
pub enum PropsError {
|
||||
ExtraNotFound {
|
||||
key: &'static str,
|
||||
},
|
||||
/// La clave no existe. Incluye la clave (`key`).
|
||||
#[error("extra \"{key}\" not found")]
|
||||
ExtraNotFound { key: &'static str },
|
||||
/// La clave existe pero el tipo solicitado no coincide con el almacenado. Incluye la clave
|
||||
/// (`key`), tipo esperado (`expected`) y tipo realmente encontrado (`found`) para facilitar el
|
||||
/// diagnóstico.
|
||||
#[error("type mismatch for extra \"{key}\": expected \"{expected}\", found \"{found}\"")]
|
||||
ExtraTypeMismatch {
|
||||
key: &'static str,
|
||||
expected: &'static str,
|
||||
|
|
@ -54,24 +55,6 @@ pub enum PropsError {
|
|||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for PropsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PropsError::ExtraNotFound { key } => write!(f, "extra \"{key}\" not found"),
|
||||
PropsError::ExtraTypeMismatch {
|
||||
key,
|
||||
expected,
|
||||
found,
|
||||
} => write!(
|
||||
f,
|
||||
"type mismatch for extra \"{key}\": expected \"{expected}\", found \"{found}\""
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PropsError {}
|
||||
|
||||
// **< PropsOp >************************************************************************************
|
||||
|
||||
/// Operaciones sobre el identificador, clases CSS, atributos HTML y valores extra en [`Props`].
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{AutoDefault, CowStr, builder_fn};
|
||||
|
||||
use std::fmt;
|
||||
use std::fmt::{self, Write as _};
|
||||
|
||||
/// Representa una ruta como un *path* inicial más una lista opcional de parámetros.
|
||||
///
|
||||
|
|
@ -8,13 +8,22 @@ use std::fmt;
|
|||
/// pensadas para usarse en atributos HTML como `href`, `action` o `src`.
|
||||
///
|
||||
/// `RoutePath` no valida ni interpreta la estructura del *path*; simplemente concatena los
|
||||
/// parámetros de consulta sobre el valor proporcionado.
|
||||
/// parámetros de consulta sobre el valor proporcionado. El *path* tampoco se codifica: se asume
|
||||
/// que ya es válido (rutas propias de la aplicación, normalmente literales o formadas a partir de
|
||||
/// identificadores conocidos).
|
||||
///
|
||||
/// # Codificación de los valores
|
||||
///
|
||||
/// El método [`with_param()`](Self::with_param) codifica el **valor** (no la clave) según RFC 3986
|
||||
/// antes de insertarlo. Así, cualquier valor (como una búsqueda de usuario, un destino con su
|
||||
/// propia *query string*, etc.) puede pasarse tal cual, sin que quien llama tenga que codificarlo
|
||||
/// primero.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// // Ruta relativa con parámetros y una *flag* sin valor.
|
||||
/// // Ruta relativa con parámetros y un *flag* sin valor.
|
||||
/// let route = RoutePath::new("/search")
|
||||
/// .with_param("q", "rust")
|
||||
/// .with_param("page", "2")
|
||||
|
|
@ -24,8 +33,12 @@ use std::fmt;
|
|||
/// // Ruta absoluta a un recurso externo.
|
||||
/// let external = RoutePath::new("https://example.com/export").with_param("format", "csv");
|
||||
/// assert_eq!(external.to_string(), "https://example.com/export?format=csv");
|
||||
///
|
||||
/// // Un valor con espacios o símbolos se codifica automáticamente.
|
||||
/// let search = RoutePath::new("/search").with_param("q", "rust & htmx");
|
||||
/// assert_eq!(search.to_string(), "/search?q=rust%20%26%20htmx");
|
||||
/// ```
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub struct RoutePath {
|
||||
/// *Path* inicial sobre el que se añadirán los parámetros.
|
||||
///
|
||||
|
|
@ -52,9 +65,17 @@ impl RoutePath {
|
|||
}
|
||||
|
||||
/// Añade o sustituye un parámetro `key=value`. Si la clave ya existe, el valor se sobrescribe.
|
||||
///
|
||||
/// El valor se codifica según RFC 3986, los caracteres no reservados (alfanuméricos ASCII, `-`,
|
||||
/// `_`, `.`, `~`) quedan intactos, el resto se sustituye por su secuencia `%XX`. La clave se
|
||||
/// inserta tal cual, sin codificar.
|
||||
///
|
||||
/// Un `value` vacío no se distingue de [`with_flag()`](Self::with_flag): ambos se renderizan
|
||||
/// como `?key`, sin `=`.
|
||||
#[builder_fn]
|
||||
pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
self.query.insert(key.into(), value.into());
|
||||
self.query
|
||||
.insert(key.into(), Self::encode_query_value(&value.into()));
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +90,29 @@ impl RoutePath {
|
|||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Indica si el *path* **parece** una URL externa por su prefijo (ver
|
||||
/// [`util::url_looks_external()`](crate::util::url_looks_external)).
|
||||
pub fn is_external(&self) -> bool {
|
||||
crate::util::url_looks_external(&self.path)
|
||||
}
|
||||
|
||||
// **< RoutePath HELPERS >**********************************************************************
|
||||
|
||||
// Codifica un valor para su uso seguro como parte de una *query string* según RFC 3986: los
|
||||
// caracteres no reservados quedan intactos y el resto se codifica como `%XX`.
|
||||
fn encode_query_value(value: &str) -> String {
|
||||
let mut out = String::with_capacity(value.len());
|
||||
for byte in value.bytes() {
|
||||
match byte {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||
out.push(byte as char);
|
||||
}
|
||||
_ => write!(out, "%{byte:02X}").unwrap(),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RoutePath {
|
||||
|
|
@ -91,9 +135,13 @@ impl fmt::Display for RoutePath {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for RoutePath {
|
||||
fn from(path: &'static str) -> Self {
|
||||
RoutePath::new(path)
|
||||
// Cualquier `&str`, sea cual sea su vida, se acepta copiándolo a un `String` propio: así, por
|
||||
// ejemplo, una función hipotética que devuelva un `&str` con una vida atada a una petición, no
|
||||
// `'static` (del estilo `fn resolve_target<'a>(next: &'a str, fallback: &'a str) -> &'a str`),
|
||||
// sigue pudiendo construir un `RoutePath` sin que quien llama tenga que convertir el valor a mano.
|
||||
impl From<&str> for RoutePath {
|
||||
fn from(path: &str) -> Self {
|
||||
RoutePath::new(path.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue