♻️ (util): Simplifica normalize_ascii*
`normalize_ascii_or_empty()` pasa a llamarse `normalize_ascii()` (la variante indulgente, la más usada); la antigua `normalize_ascii()` pasa a `normalize_ascii_non_blank()`.
This commit is contained in:
parent
5939d33964
commit
b163b7726e
5 changed files with 70 additions and 57 deletions
|
|
@ -96,7 +96,7 @@ impl Autocomplete {
|
||||||
/// El prefijo `section-*` sirve para distinguir entre varios grupos del mismo tipo en una misma
|
/// El prefijo `section-*` sirve para distinguir entre varios grupos del mismo tipo en una misma
|
||||||
/// página (p. ej. una dirección de envío y otra de facturación).
|
/// página (p. ej. una dirección de envío y otra de facturación).
|
||||||
pub fn section(name: impl AsRef<str>, field: AutofillField) -> Self {
|
pub fn section(name: impl AsRef<str>, field: AutofillField) -> Self {
|
||||||
match util::normalize_ascii(name.as_ref()) {
|
match util::normalize_ascii_non_blank(name.as_ref()) {
|
||||||
Ok(n) if !n.as_ref().contains(' ') => {
|
Ok(n) if !n.as_ref().contains(' ') => {
|
||||||
Self::custom(util::join!("section-", n.as_ref(), " ", field.as_str()))
|
Self::custom(util::join!("section-", n.as_ref(), " ", field.as_str()))
|
||||||
}
|
}
|
||||||
|
|
@ -196,7 +196,7 @@ impl Autocomplete {
|
||||||
let raw = value.as_ref();
|
let raw = value.as_ref();
|
||||||
|
|
||||||
// Normaliza la entrada.
|
// Normaliza la entrada.
|
||||||
let Some(normalized) = util::normalize_ascii_or_empty(raw, "Autocomplete::custom") else {
|
let Some(normalized) = util::normalize_ascii(raw) else {
|
||||||
return Self::On;
|
return Self::On;
|
||||||
};
|
};
|
||||||
let autocomplete = normalized.as_ref();
|
let autocomplete = normalized.as_ref();
|
||||||
|
|
|
||||||
|
|
@ -539,29 +539,23 @@ impl Props {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PropsOp::AddClasses(classes) => {
|
PropsOp::AddClasses(classes) => {
|
||||||
let Some(normalized) =
|
let Some(normalized) = util::normalize_ascii(classes.as_ref()) else {
|
||||||
util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop")
|
|
||||||
else {
|
|
||||||
return self;
|
return self;
|
||||||
};
|
};
|
||||||
let pos = self.classes.len();
|
let pos = self.classes.len();
|
||||||
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), pos);
|
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), pos);
|
||||||
}
|
}
|
||||||
PropsOp::PrependClasses(classes) => {
|
PropsOp::PrependClasses(classes) => {
|
||||||
let Some(normalized) =
|
let Some(normalized) = util::normalize_ascii(classes.as_ref()) else {
|
||||||
util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop")
|
|
||||||
else {
|
|
||||||
return self;
|
return self;
|
||||||
};
|
};
|
||||||
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0);
|
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0);
|
||||||
}
|
}
|
||||||
PropsOp::ReplaceClasses(old, new) => {
|
PropsOp::ReplaceClasses(old, new) => {
|
||||||
let Some(old) = util::normalize_ascii_or_empty(old.as_ref(), "Props::with_prop")
|
let Some(old) = util::normalize_ascii(old.as_ref()) else {
|
||||||
else {
|
|
||||||
return self;
|
return self;
|
||||||
};
|
};
|
||||||
let Some(new) = util::normalize_ascii_or_empty(new.as_ref(), "Props::with_prop")
|
let Some(new) = util::normalize_ascii(new.as_ref()) else {
|
||||||
else {
|
|
||||||
return self;
|
return self;
|
||||||
};
|
};
|
||||||
let mut pos = self.classes.len();
|
let mut pos = self.classes.len();
|
||||||
|
|
@ -578,12 +572,10 @@ impl Props {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PropsOp::ReplaceAllClasses(old, new) => {
|
PropsOp::ReplaceAllClasses(old, new) => {
|
||||||
let Some(old) = util::normalize_ascii_or_empty(old.as_ref(), "Props::with_prop")
|
let Some(old) = util::normalize_ascii(old.as_ref()) else {
|
||||||
else {
|
|
||||||
return self;
|
return self;
|
||||||
};
|
};
|
||||||
let Some(new) = util::normalize_ascii_or_empty(new.as_ref(), "Props::with_prop")
|
let Some(new) = util::normalize_ascii(new.as_ref()) else {
|
||||||
else {
|
|
||||||
return self;
|
return self;
|
||||||
};
|
};
|
||||||
if !self.has_all_classes(old.as_ref()) {
|
if !self.has_all_classes(old.as_ref()) {
|
||||||
|
|
@ -599,9 +591,7 @@ impl Props {
|
||||||
self.insert_classes(new.as_ref().split_ascii_whitespace(), pos);
|
self.insert_classes(new.as_ref().split_ascii_whitespace(), pos);
|
||||||
}
|
}
|
||||||
PropsOp::RemoveClasses(classes) => {
|
PropsOp::RemoveClasses(classes) => {
|
||||||
let Some(normalized) =
|
let Some(normalized) = util::normalize_ascii(classes.as_ref()) else {
|
||||||
util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop")
|
|
||||||
else {
|
|
||||||
return self;
|
return self;
|
||||||
};
|
};
|
||||||
self.classes.retain(|c| {
|
self.classes.retain(|c| {
|
||||||
|
|
@ -621,9 +611,7 @@ impl Props {
|
||||||
if name.as_ref() == "id" {
|
if name.as_ref() == "id" {
|
||||||
self.apply_id(value.as_ref());
|
self.apply_id(value.as_ref());
|
||||||
} else if name.as_ref() == "class" {
|
} else if name.as_ref() == "class" {
|
||||||
if let Some(normalized) =
|
if let Some(normalized) = util::normalize_ascii(value.as_ref()) {
|
||||||
util::normalize_ascii_or_empty(value.as_ref(), "Props::with_prop")
|
|
||||||
{
|
|
||||||
self.classes.clear();
|
self.classes.clear();
|
||||||
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0);
|
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0);
|
||||||
}
|
}
|
||||||
|
|
@ -765,7 +753,7 @@ impl Props {
|
||||||
|
|
||||||
/// Devuelve `true` si la clase o **alguna** de las clases indicadas está presente.
|
/// Devuelve `true` si la clase o **alguna** de las clases indicadas está presente.
|
||||||
pub fn has_classes(&self, classes: impl AsRef<str>) -> bool {
|
pub fn has_classes(&self, classes: impl AsRef<str>) -> bool {
|
||||||
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
|
let Ok(normalized) = util::normalize_ascii_non_blank(classes.as_ref()) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
normalized
|
normalized
|
||||||
|
|
@ -776,7 +764,7 @@ impl Props {
|
||||||
|
|
||||||
/// Devuelve `true` si la clase o **todas** las clases indicadas están presentes.
|
/// Devuelve `true` si la clase o **todas** las clases indicadas están presentes.
|
||||||
pub fn has_all_classes(&self, classes: impl AsRef<str>) -> bool {
|
pub fn has_all_classes(&self, classes: impl AsRef<str>) -> bool {
|
||||||
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
|
let Ok(normalized) = util::normalize_ascii_non_blank(classes.as_ref()) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
normalized
|
normalized
|
||||||
|
|
@ -922,15 +910,17 @@ impl Props {
|
||||||
|
|
||||||
// Añade o sustituye una declaración "propiedad: valor". Si la propiedad ya existe, sustituye
|
// Añade o sustituye una declaración "propiedad: valor". Si la propiedad ya existe, sustituye
|
||||||
// su valor conservando la posición; si no, la añade al final. Ignora la declaración si la
|
// su valor conservando la posición; si no, la añade al final. Ignora la declaración si la
|
||||||
// propiedad o el valor quedan vacíos tras recortar espacios. No aplica
|
// propiedad o el valor quedan vacíos tras recortar espacios. No aplica `normalize_ascii`: ver
|
||||||
// normalize_ascii_or_empty: ver la documentación de `PropsOp::AddStyle` sobre por qué los
|
// la documentación de `PropsOp::AddStyle` sobre por qué los valores de estilo no se restringen
|
||||||
// valores de estilo no se restringen a ASCII.
|
// a ASCII.
|
||||||
fn set_style(&mut self, property: &str, value: &str) {
|
fn set_style(&mut self, property: &str, value: &str) {
|
||||||
let property = property.trim().to_ascii_lowercase();
|
let Some(property) = util::non_blank(property) else {
|
||||||
let value = value.trim();
|
|
||||||
if property.is_empty() || value.is_empty() {
|
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
let property = property.to_ascii_lowercase();
|
||||||
|
let Some(value) = util::non_blank(value) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
if let Some(pos) = self.styles.iter().position(|(k, _)| k.as_ref() == property) {
|
if let Some(pos) = self.styles.iter().position(|(k, _)| k.as_ref() == property) {
|
||||||
self.styles[pos].1 = value.to_string().into();
|
self.styles[pos].1 = value.to_string().into();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -943,10 +933,9 @@ impl Props {
|
||||||
// `style`) y aplica cada declaración con `set_style`. Ignora las declaraciones sin ":".
|
// `style`) y aplica cada declaración con `set_style`. Ignora las declaraciones sin ":".
|
||||||
fn parse_styles(&mut self, styles: &str) {
|
fn parse_styles(&mut self, styles: &str) {
|
||||||
for style in Self::split_style_declarations(styles) {
|
for style in Self::split_style_declarations(styles) {
|
||||||
let style = style.trim();
|
let Some(style) = util::non_blank(style) else {
|
||||||
if style.is_empty() {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
};
|
||||||
let Some((property, value)) = style.split_once(':') else {
|
let Some((property, value)) = style.split_once(':') else {
|
||||||
trace::debug!(
|
trace::debug!(
|
||||||
target = "Props::with_prop",
|
target = "Props::with_prop",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::global;
|
use crate::global;
|
||||||
|
use crate::util;
|
||||||
use crate::web::HttpRequest;
|
use crate::web::HttpRequest;
|
||||||
|
|
||||||
use super::{LangId, LanguageIdentifier, Locale};
|
use super::{LangId, LanguageIdentifier, Locale};
|
||||||
|
|
@ -82,7 +83,7 @@ impl RequestLocale {
|
||||||
|
|
||||||
// En este primer elemento también puede aparecer `;q=...`, así que se
|
// En este primer elemento también puede aparecer `;q=...`, así que se
|
||||||
// extrae únicamente la etiqueta de idioma: "es-ES;q=0.9" -> "es-ES".
|
// extrae únicamente la etiqueta de idioma: "es-ES;q=0.9" -> "es-ES".
|
||||||
let tag = first.split(';').next()?.trim();
|
let tag = util::non_blank(first.split(';').next()?)?;
|
||||||
|
|
||||||
// TODO: Mejorar el soporte de `Accept-Language` en el futuro:
|
// TODO: Mejorar el soporte de `Accept-Language` en el futuro:
|
||||||
//
|
//
|
||||||
|
|
@ -92,9 +93,7 @@ impl RequestLocale {
|
||||||
// - Tener en cuenta rangos de idioma (`es`, `en`, etc.) y variantes
|
// - Tener en cuenta rangos de idioma (`es`, `en`, etc.) y variantes
|
||||||
// regionales.
|
// regionales.
|
||||||
// - Añadir tests unitarios para distintas combinaciones de cabecera.
|
// - Añadir tests unitarios para distintas combinaciones de cabecera.
|
||||||
if tag.is_empty() {
|
if let Locale::Resolved(langid) = Locale::resolve(tag) {
|
||||||
None
|
|
||||||
} else if let Locale::Resolved(langid) = Locale::resolve(tag) {
|
|
||||||
Some(langid)
|
Some(langid)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
||||||
51
src/util.rs
51
src/util.rs
|
|
@ -5,6 +5,7 @@ use crate::trace;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::panic::Location;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
// **< MACROS INTEGRADAS >**************************************************************************
|
// **< MACROS INTEGRADAS >**************************************************************************
|
||||||
|
|
@ -39,7 +40,7 @@ pub fn build_runtime() -> tokio::runtime::Runtime {
|
||||||
tokio::runtime::Runtime::new().expect("Failed to build the Tokio runtime")
|
tokio::runtime::Runtime::new().expect("Failed to build the Tokio runtime")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errores posibles al normalizar una cadena ASCII con [`normalize_ascii()`].
|
/// Errores al normalizar una cadena ASCII con [`normalize_ascii_non_blank()`].
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum NormalizeAsciiError {
|
pub enum NormalizeAsciiError {
|
||||||
/// La entrada está vacía (`""`).
|
/// La entrada está vacía (`""`).
|
||||||
|
|
@ -69,9 +70,10 @@ pub fn non_blank(s: &str) -> Option<&str> {
|
||||||
if s.is_empty() { None } else { Some(s) }
|
if s.is_empty() { None } else { Some(s) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normaliza una cadena ASCII con uno o varios tokens separados.
|
/// Normaliza una cadena ASCII con uno o varios tokens, exigiendo contenido no vacío.
|
||||||
///
|
///
|
||||||
/// Los *separadores* son caracteres `is_ascii_whitespace()` como `' '`, `'\t'`, `'\n'` o `'\r'`.
|
/// Los *separadores* de los tokens son caracteres `is_ascii_whitespace()` como `' '`, `'\t'`,
|
||||||
|
/// `'\n'` o `'\r'`.
|
||||||
///
|
///
|
||||||
/// Reglas:
|
/// Reglas:
|
||||||
///
|
///
|
||||||
|
|
@ -85,13 +87,20 @@ pub fn non_blank(s: &str) -> Option<&str> {
|
||||||
/// Intenta devolver siempre `Cow::Borrowed` para no reservar memoria, y `Cow::Owned` sólo si ha
|
/// Intenta devolver siempre `Cow::Borrowed` para no reservar memoria, y `Cow::Owned` sólo si ha
|
||||||
/// tenido que aplicar cambios para normalizar.
|
/// tenido que aplicar cambios para normalizar.
|
||||||
///
|
///
|
||||||
|
/// Normalmente se usará [`normalize_ascii()`], que trata una cadena en blanco como resultado
|
||||||
|
/// válido, en vez de como un error. Esta variante es para detectar también cuándo un resultado es
|
||||||
|
/// una cadena vacía para rechazarla de inmediato.
|
||||||
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::util;
|
/// # use pagetop::util;
|
||||||
/// assert_eq!(util::normalize_ascii(" Foo\tBAR CLi\r\n").unwrap().as_ref(), "foo bar cli");
|
/// assert_eq!(
|
||||||
|
/// util::normalize_ascii_non_blank(" Foo\tBAR CLi\r\n").unwrap().as_ref(),
|
||||||
|
/// "foo bar cli"
|
||||||
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
pub fn normalize_ascii(input: &str) -> Result<Cow<'_, str>, NormalizeAsciiError> {
|
pub fn normalize_ascii_non_blank(input: &str) -> Result<Cow<'_, str>, NormalizeAsciiError> {
|
||||||
let bytes = input.as_bytes();
|
let bytes = input.as_bytes();
|
||||||
if bytes.is_empty() {
|
if bytes.is_empty() {
|
||||||
return Err(NormalizeAsciiError::IsEmpty);
|
return Err(NormalizeAsciiError::IsEmpty);
|
||||||
|
|
@ -163,19 +172,35 @@ pub fn normalize_ascii(input: &str) -> Result<Cow<'_, str>, NormalizeAsciiError>
|
||||||
Ok(Cow::Owned(output))
|
Ok(Cow::Owned(output))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normaliza una cadena ASCII, opcionalmente vacía, con uno o varios tokens separados.
|
/// Normaliza una cadena ASCII con uno o varios tokens, aceptando cadenas vacías.
|
||||||
///
|
///
|
||||||
/// - Devuelve `Some(Cow)` si la entrada es válida ASCII (normalizada a minúsculas).
|
/// - Devuelve `Some(Cow)` normalizado (ver [`normalize_ascii_non_blank()`] para las reglas exactas
|
||||||
/// - Devuelve `Some(Cow::Borrowed(""))` si la entrada es `""` o queda vacía tras recortar.
|
/// de recorte, colapso de espacios y minúsculas) si la entrada es ASCII válida.
|
||||||
/// - Devuelve `None` si la entrada contiene bytes no ASCII; y emite un `trace::debug!` con el campo
|
/// - Devuelve `Some(Cow::Borrowed(""))` si la entrada es `""` o queda vacía tras recortar. Una
|
||||||
/// `target`.
|
/// entrada en blanco no es un error, es un resultado trivial.
|
||||||
|
/// - Devuelve `None` **sólo** si la entrada contiene bytes no ASCII; y emite un `trace::debug!` con
|
||||||
|
/// la ubicación exacta del llamador (vía `#[track_caller]`) para poder localizar el origen del
|
||||||
|
/// valor rechazado.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::util;
|
||||||
|
/// assert_eq!(
|
||||||
|
/// util::normalize_ascii(" Foo\tBAR CLi\r\n").unwrap().as_ref(),
|
||||||
|
/// "foo bar cli"
|
||||||
|
/// );
|
||||||
|
/// assert_eq!(util::normalize_ascii(" ").unwrap().as_ref(), "");
|
||||||
|
/// assert_eq!(util::normalize_ascii("ñoño"), None);
|
||||||
|
/// ```
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn normalize_ascii_or_empty<'a>(input: &'a str, target: &'static str) -> Option<Cow<'a, str>> {
|
#[track_caller]
|
||||||
match normalize_ascii(input) {
|
pub fn normalize_ascii(input: &str) -> Option<Cow<'_, str>> {
|
||||||
|
match normalize_ascii_non_blank(input) {
|
||||||
Ok(s) => Some(s),
|
Ok(s) => Some(s),
|
||||||
Err(NormalizeAsciiError::NonAscii) => {
|
Err(NormalizeAsciiError::NonAscii) => {
|
||||||
trace::debug!(
|
trace::debug!(
|
||||||
target = %target,
|
caller = %Location::caller(),
|
||||||
input = %input.escape_default(),
|
input = %input.escape_default(),
|
||||||
"Ignoring due to non-ASCII chars"
|
"Ignoring due to non-ASCII chars"
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ async fn setup() {
|
||||||
Application::new().await;
|
Application::new().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Testing normalize_ascii() >******************************************************************
|
// **< Testing normalize_ascii_non_blank() >********************************************************
|
||||||
|
|
||||||
fn assert_err(input: &str, expected: util::NormalizeAsciiError) {
|
fn assert_err(input: &str, expected: util::NormalizeAsciiError) {
|
||||||
let out = util::normalize_ascii(input);
|
let out = util::normalize_ascii_non_blank(input);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
out,
|
out,
|
||||||
Err(expected),
|
Err(expected),
|
||||||
|
|
@ -22,7 +22,7 @@ fn assert_err(input: &str, expected: util::NormalizeAsciiError) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_borrowed(input: &str, expected: &str) {
|
fn assert_borrowed(input: &str, expected: &str) {
|
||||||
let out = util::normalize_ascii(input).expect("Expected Ok(..)");
|
let out = util::normalize_ascii_non_blank(input).expect("Expected Ok(..)");
|
||||||
assert_eq!(out.as_ref(), expected, "Input {:?}", input);
|
assert_eq!(out.as_ref(), expected, "Input {:?}", input);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(out, Cow::Borrowed(_)),
|
matches!(out, Cow::Borrowed(_)),
|
||||||
|
|
@ -33,7 +33,7 @@ fn assert_borrowed(input: &str, expected: &str) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_owned(input: &str, expected: &str) {
|
fn assert_owned(input: &str, expected: &str) {
|
||||||
let out = util::normalize_ascii(input).expect("Expected Ok(..)");
|
let out = util::normalize_ascii_non_blank(input).expect("Expected Ok(..)");
|
||||||
assert_eq!(out.as_ref(), expected, "Input {:?}", input);
|
assert_eq!(out.as_ref(), expected, "Input {:?}", input);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(out, Cow::Owned(_)),
|
matches!(out, Cow::Owned(_)),
|
||||||
|
|
@ -248,8 +248,8 @@ async fn normalize_is_idempotent() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let first = util::normalize_ascii(input).unwrap();
|
let first = util::normalize_ascii_non_blank(input).unwrap();
|
||||||
let second = util::normalize_ascii(first.as_ref()).unwrap();
|
let second = util::normalize_ascii_non_blank(first.as_ref()).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
first.as_ref(),
|
first.as_ref(),
|
||||||
second.as_ref(),
|
second.as_ref(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue