Validación¶
La API Validation de Vapor te ayuda a validar peticiones entrantes antes de usar la API Content para decodificar datos.
Introducción¶
La profunda integración del protocolo de tipado seguro Codable
de Swift en Vapor hace que no tengas que preocuparte tanto por la validación de datos como lo harías en un lenguaje de tipado dinámico. Sin embargo, existen varias razones por las que puedas querer validación explícita usando la API Validation.
Errores legibles por humanos¶
Decodificar struct usando la API Content dará errores si alguno de los datos no es válido. Sin embargo, a veces estos mensajes de error pueden carecer de legibilidad para un humano. Por ejemplo, teniendo el siguiente enum respaldado por cadenas:
enum Color: String, Codable {
case red, blue, green
}
Si un usuario trata de pasar la cadena "purple"
a una propiedad de tipo Color
, recibirán un error similar al siguiente:
Cannot initialize Color from invalid String value purple for key favoriteColor
Aunque este error es técnicamente correcto y ha protegido con éxito el endpoint ante un valor no válido, podría informar de una mejor forma al usuario acerca del error, indicándole las opciones disponibles. Usando la API de validación (Validation), puedes generar errores como el siguiente:
favoriteColor is not red, blue, or green
Es más, Codable
interrumpirá la decodificación de un tipo en cuanto reciba el primer error. Esto implica que, aunque varias de las propiedades de la petición sean inválidas, el usuario solo verá el primer error. La API Validation informará sobre todos los errores de validación en una única petición.
Validación específica¶
Codable
controla bien la validación de tipos, pero a veces puedes querer más que eso. Por ejemplo, validar el contenido de una cadena o el tamaño de un número entero. La API Validation posee validadores para ayudar en la validación de datos como emails, conjuntos de caracteres, rangos de números enteros y más.
Validatable¶
Para validar una petición, deberás generar una colección de Validations
. La manera más común de hacerlo es conformar un tipo existente a Validatable
.
Echemos un vistazo a cómo podrías añadir validación a este simple endpoint de POST /users
. Esta guía asume que ya estás familiarizado con la API Content.
enum Color: String, Codable {
case red, blue, green
}
struct CreateUser: Content {
var name: String
var username: String
var age: Int
var email: String
var favoriteColor: Color?
}
app.post("users") { req -> CreateUser in
let user = try req.content.decode(CreateUser.self)
// Haz algo con user.
return user
}
Añadiendo Validaciones¶
El primer paso es conformar el tipo que estás decodificando, en este caso CreateUser
, a Validatable
. Esto puede hacerse en una extensión.
extension CreateUser: Validatable {
static func validations(_ validations: inout Validations) {
// Aquí van las validaciones.
}
}
El método estático validations(_:)
se llamará cuando CreateUser
sea validado. Cualquier validación que quieras llevar a cabo deberá ser añadida a la colección Validations
proporcionada. Veamos como añadir una simple validación que requiera que el email del usuario sea válido.
validations.add("email", as: String.self, is: .email)
El primer parámetro es la clave esperada del valor, en este caso, "email"
. Esta clave debería ser igual al nombre de la propiedad del tipo que se está validando. El segundo parámetro, as
, es el tipo esperado, en este caso, String
. Suele coincidir con el tipo de la propiedad, pero no siempre. Finalmente, pueden añadirse uno o más validadores después en el tercer parámetro, is
. En este caso estamos añadiendo un único validador que comprueba si el valor es una dirección de email.
Validando contenido de peticiones¶
Una vez hayas conformado tu tipo a Validatable
, la función estática validate(content:)
puede usarse para validar el contenido de una petición. Antes de req.content.decode(CreateUser.self)
, añade la línea a continuación en el controlador de ruta:
try CreateUser.validate(content: req)
Ahora, prueba a enviar la siguiente petición con un email no válido:
POST /users HTTP/1.1
Content-Length: 67
Content-Type: application/json
{
"age": 4,
"email": "foo",
"favoriteColor": "green",
"name": "Foo",
"username": "foo"
}
Deberías recibir el siguiente error:
email is not a valid email address
Validando la consulta (query) de una petición¶
Los tipos conformados con Validatable
también disponen de validate(query:)
, que puede usarse para validar la cadena de consulta (query string) de una petición. Añade las siguientes líneas al controlador de ruta:
try CreateUser.validate(query: req)
req.query.decode(CreateUser.self)
Ahora, prueba a enviar la siguiente petición con un email no válido en la cadena de consulta.
GET /users?age=4&email=foo&favoriteColor=green&name=Foo&username=foo HTTP/1.1
Deberías recibir el siguiente error:
email is not a valid email address
Validación de números enteros¶
Genial, ahora probemos a añadir una validación para age
.
validations.add("age", as: Int.self, is: .range(13...))
La validación de edad requiere que age
sea igual o superior a 13
. Si pruebas la petición mencionada arriba, deberías ver un nuevo error:
age is less than minimum of 13, email is not a valid email address
Validación de cadenas (string)¶
A continuación, añadiremos validaciones para name
y username
.
validations.add("name", as: String.self, is: !.empty)
validations.add("username", as: String.self, is: .count(3...) && .alphanumeric)
La validación de name
usa el operador !
para invertir la validación de .empty
, requiriendo que la cadena no sea vacía.
La validación de username
combina dos validadores mediante el uso de &&
, requiriendo que la cadena tenga una longitud de, al menos, 3 caracteres y que contenga únicamente caracteres alfanuméricos.
Validación de Enums¶
Finalmente, veremos una validación ligeramente más avanzada para comprobar que el favoriteColor
proporcionado es válido.
validations.add(
"favoriteColor", as: String.self,
is: .in("red", "blue", "green"),
required: false
)
Como no es posible decodificar un Color
desde un valor no válido, esta validación usa String
como tipo base. Utiliza en validador .in
para verificar que el valor es una opción válida: red, blue o green. Como este valor es opcional, required
se establece a false
para indicar que la validación no debería fallar si esta clave no se encuentra en los datos de la petición.
Ten en cuenta que, aunque la validación de favoriteColor
pasará si la clave falta, no pasará si se proporciona null
(nulo). Si quieres soportar null
, cambia el tipo de la validación a String?
y usa el operador de conveniencia .nil ||
(leer como: "es nil (nulo) o ...").
validations.add(
"favoriteColor", as: String?.self,
is: .nil || .in("red", "blue", "green"),
required: false
)
Errores Personalizados¶
Puede que quieras añadir errores personalizados que sean legibles por humanos a tus Validations
o Validator
. Para hacerlo, simplemente proporciona el parámetro adicional customFailureDescription
, que sobrescribirá el error por defecto.
validations.add(
"name",
as: String.self,
is: !.empty,
customFailureDescription: "Provided name is empty!"
)
validations.add(
"username",
as: String.self,
is: .count(3...) && .alphanumeric,
customFailureDescription: "Provided username is invalid!"
)
Validadores (Validators)¶
Debajo tienes una lista de los validadores soportados actualmente y una breve explicación de lo que hacen.
Validación | Descripción |
---|---|
.ascii |
Contiene únicamente caracteres ASCII. |
.alphanumeric |
Contiene únicamente caracteres alfanuméricos. |
.characterSet(_:) |
Contiene únicamente caracteres del CharacterSet (conjunto de caracteres) proporcionado. |
.count(_:) |
El número de elementos (count) de una colección está entre los límites proporcionados. |
.email |
Contiene un email válido. |
.empty |
La colección está vacía. |
.in(_:) |
El valor se encuentra en la Collection (colección) proporcionada. |
.nil |
El valor es null (nulo). |
.range(_:) |
El valor se encuentra en el Range (rango) proporcionado. |
.url |
Contiene una URL válida. |
Los validadores también pueden combinarse mediante operadores para construir validaciones complejas.
Operador | Posición | Descripción |
---|---|---|
! |
prefijo | Invierte un validador, requiriendo lo opuesto. |
&& |
infijo | Combina dos validadores, requiere ambos. |
|| |
infijo | Combina dos validadores, requiere al menos uno. |
Validadores Personalizados¶
Crear un validador personalizado para códigos postales te permite extender la funcionalidad del marco de validación. En esta sección, te guiaremos a través de los pasos para crear un validador personalizado que valide códigos postales.
Primero, crea un nuevo tipo para representar los resultados de la validación de ZipCode
. Esta estructura será responsable de informar si una cadena dada es un código postal válido.
extension ValidatorResults {
/// Representa el resultado de un validador que verifica si una cadena es un código postal válido.
public struct ZipCode {
/// Indica si la entrada es un código postal válido.
public let isValidZipCode: Bool
}
}
A continuación, haz que el nuevo tipo cumpla con ValidatorResult
, que define el comportamiento esperado de un validador personalizado.
extension ValidatorResults.ZipCode: ValidatorResult {
public var isFailure: Bool {
!self.isValidZipCode
}
public var successDescription: String? {
"is a valid zip code"
}
public var failureDescription: String? {
"is not a valid zip code"
}
}
Finalmente, implementa la lógica de validación para los códigos postales. Usa una expresión regular para verificar si la cadena de entrada coincide con el formato de un código postal de EE.UU.
private let zipCodeRegex: String = "^\\d{5}(?:[-\\s]\\d{4})?$"
extension Validator where T == String {
/// Valida si una cadena es un código postal válido.
public static var zipCode: Validator<T> {
.init { input in
guard let range = input.range(of: zipCodeRegex, options: [.regularExpression]),
range.lowerBound == input.startIndex && range.upperBound == input.endIndex
else {
return ValidatorResults.ZipCode(isValidZipCode: false)
}
return ValidatorResults.ZipCode(isValidZipCode: true)
}
}
}
Ahora que has definido el validador personalizado zipCode
, puedes usarlo para validar códigos postales en tu aplicación. Simplemente agrega la siguiente línea a tu código de validación:
validations.add("zipCode", as: String.self, is: .zipCode)