Walidacja#
API Validation Vapora pomaga zwalidować treść (body) i parametry zapytania (query parameters) przychodzącego żądania przed użyciem API Content do dekodowania danych.
Wprowadzenie#
Głęboka integracja Vapora z typowo bezpiecznym protokołem Codable w Swifcie oznacza, że nie musisz przejmować się walidacją danych tak bardzo, jak w językach z dynamicznym typowaniem. Istnieje jednak kilka powodów, dla których warto skorzystać z jawnej walidacji przy użyciu API Validation.
Czytelne dla człowieka błędy#
Dekodowanie struktur za pomocą API Content zwróci błędy, jeśli którekolwiek z danych są nieprawidłowe. Jednak te komunikaty błędów mogą czasem nie być zbyt czytelne dla człowieka. Weźmy na przykład poniższy enum oparty na stringu:
enum Color: String, Codable {
case red, blue, green
}
Jeśli użytkownik spróbuje przekazać string "purple" do właściwości typu Color, otrzyma błąd podobny do poniższego:
Cannot initialize Color from invalid String value purple for key favoriteColor
Chociaż ten błąd jest technicznie poprawny i skutecznie zabezpieczył endpoint przed nieprawidłową wartością, mógłby lepiej informować użytkownika o pomyłce i dostępnych opcjach. Korzystając z API Validation, możesz generować błędy takie jak poniższy:
favoriteColor is not red, blue, or green
Co więcej, Codable przestaje próbować dekodować typ, gdy tylko napotka pierwszy błąd. Oznacza to, że nawet jeśli w żądaniu jest wiele nieprawidłowych właściwości, użytkownik zobaczy tylko pierwszy błąd. API Validation zgłosi wszystkie niepowodzenia walidacji w ramach jednego żądania.
Szczegółowa walidacja#
Codable dobrze radzi sobie z walidacją typów, ale czasami potrzebujesz czegoś więcej. Na przykład walidacji zawartości stringa albo walidacji zakresu liczby całkowitej. API Validation ma walidatory pomagające sprawdzać dane, takie jak adresy e-mail, zestawy znaków, zakresy liczb całkowitych i wiele innych.
Validatable#
Aby zwalidować żądanie, musisz wygenerować kolekcję Validations. Najczęściej robi się to, dostosowując istniejący typ do protokołu Validatable.
Przyjrzyjmy się, jak można dodać walidację do tego prostego endpointu POST /users. Ten przewodnik zakłada, że znasz już 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)
// Do something with user.
return user
}
Dodawanie walidacji#
Pierwszym krokiem jest dostosowanie typu, który dekodujesz, w tym przypadku CreateUser, do protokołu Validatable. Można to zrobić w rozszerzeniu (extension).
extension CreateUser: Validatable {
static func validations(_ validations: inout Validations) {
// Validations go here.
}
}
Statyczna metoda validations(_:) zostanie wywołana, gdy CreateUser będzie walidowany. Wszystkie walidacje, które chcesz przeprowadzić, powinny zostać dodane do dostarczonej kolekcji Validations. Przyjrzyjmy się dodaniu prostej walidacji wymagającej, aby adres e-mail użytkownika był prawidłowy.
validations.add("email", as: String.self, is: .email)
Pierwszy parametr to oczekiwany klucz wartości, w tym przypadku "email". Powinien on odpowiadać nazwie właściwości w walidowanym typie. Drugi parametr, as, to oczekiwany typ, w tym przypadku String. Typ zwykle odpowiada typowi właściwości, ale nie zawsze. Na koniec, po trzecim parametrze, is, można dodać jeden lub więcej walidatorów. W tym przypadku dodajemy pojedynczy walidator sprawdzający, czy wartość jest adresem e-mail.
Walidacja treści żądania#
Po dostosowaniu swojego typu do protokołu Validatable, statyczna funkcja validate(content:) może zostać użyta do walidacji treści żądania. Dodaj poniższą linię przed req.content.decode(CreateUser.self) w handlerze trasy.
try CreateUser.validate(content: req)
Teraz spróbuj wysłać poniższe żądanie zawierające nieprawidłowy adres e-mail:
POST /users HTTP/1.1
Content-Length: 67
Content-Type: application/json
{
"age": 4,
"email": "foo",
"favoriteColor": "green",
"name": "Foo",
"username": "foo"
}
Powinieneś zobaczyć zwrócony poniższy błąd:
email is not a valid email address
Walidacja zapytania żądania#
Typy dostosowane do protokołu Validatable mają również validate(query:), który może zostać użyty do walidacji ciągu zapytania (query string) żądania. Dodaj poniższe linie do handlera trasy.
try CreateUser.validate(query: req)
req.query.decode(CreateUser.self)
Teraz spróbuj wysłać poniższe żądanie zawierające nieprawidłowy adres e-mail w ciągu zapytania.
GET /users?age=4&email=foo&favoriteColor=green&name=Foo&username=foo HTTP/1.1
Powinieneś zobaczyć zwrócony poniższy błąd:
email is not a valid email address
Walidacja liczb całkowitych#
Świetnie, teraz spróbujmy dodać walidację dla age.
validations.add("age", as: Int.self, is: .range(13...))
Walidacja wieku wymaga, aby wiek był większy lub równy 13. Jeśli spróbujesz wysłać to samo żądanie co powyżej, powinieneś teraz zobaczyć nowy błąd:
age is less than minimum of 13, email is not a valid email address
Walidacja stringów#
Następnie dodajmy walidacje dla name i username.
validations.add("name", as: String.self, is: !.empty)
validations.add("username", as: String.self, is: .count(3...) && .alphanumeric)
Walidacja name używa operatora !, aby odwrócić walidator .empty. Będzie to wymagało, aby string nie był pusty.
Walidacja username łączy dwa walidatory za pomocą &&. Będzie to wymagało, aby string miał co najmniej 3 znaki długości i zawierał wyłącznie znaki alfanumeryczne.
Walidacja enumów#
Na koniec przyjrzyjmy się nieco bardziej zaawansowanej walidacji, sprawdzającej, czy podany favoriteColor jest prawidłowy.
validations.add(
"favoriteColor", as: String.self,
is: .in("red", "blue", "green"),
required: false
)
Ponieważ nie jest możliwe zdekodowanie Color z nieprawidłowej wartości, ta walidacja używa String jako typu bazowego. Wykorzystuje walidator .in, aby zweryfikować, że wartość jest jedną z prawidłowych opcji: red, blue lub green. Ponieważ ta wartość jest opcjonalna, required jest ustawione na false, aby zasygnalizować, że walidacja nie powinna zawieść, jeśli tego klucza brakuje w danych żądania.
Zauważ, że chociaż walidacja koloru ulubionego przejdzie, jeśli klucz jest nieobecny, nie przejdzie, jeśli podano null. Jeśli chcesz obsługiwać null, zmień typ walidacji na String? i użyj udogodnienia .nil || (czytane jako: “is nil or …”).
validations.add(
"favoriteColor", as: String?.self,
is: .nil || .in("red", "blue", "green"),
required: false
)
Niestandardowe błędy#
Możesz chcieć dodać niestandardowe, czytelne dla człowieka błędy do swoich Validations lub Validator. Aby to zrobić, po prostu podaj dodatkowy parametr customFailureDescription, który nadpisze domyślny błąd.
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!"
)
Walidatory#
Poniżej znajduje się lista aktualnie wspieranych walidatorów wraz z krótkim wyjaśnieniem, co robią.
| Walidacja | Opis |
|---|---|
.ascii |
Zawiera wyłącznie znaki ASCII. |
.alphanumeric |
Zawiera wyłącznie znaki alfanumeryczne. |
.characterSet(_:) |
Zawiera wyłącznie znaki z podanego CharacterSet. |
.count(_:) |
Liczba elementów kolekcji mieści się w podanych granicach. |
.email |
Zawiera prawidłowy adres e-mail. |
.empty |
Kolekcja jest pusta. |
.in(_:) |
Wartość znajduje się w podanej Collection. |
.nil |
Wartość ma wartość null. |
.range(_:) |
Wartość mieści się w podanym Range. |
.url |
Zawiera prawidłowy URL. |
.custom(_:, validationClosure: (value) -> Bool) |
Niestandardowa, jednorazowa walidacja. |
Walidatory mogą być również łączone w celu budowania złożonych walidacji za pomocą operatorów. Więcej informacji o walidatorze .custom znajdziesz w sekcji Niestandardowe walidatory.
| Operator | Pozycja | Opis |
|---|---|---|
! |
przedrostkowy (prefix) | Odwraca walidator, wymagając przeciwieństwa. |
&& |
infiksowy (infix) | Łączy dwa walidatory, wymaga obu. |
|| |
infiksowy (infix) | Łączy dwa walidatory, wymaga jednego. |
Niestandardowe walidatory#
Istnieją dwa sposoby tworzenia niestandardowych walidatorów.
Rozszerzanie API Validation#
Rozszerzanie API Validation najlepiej sprawdza się w przypadkach, gdy planujesz używać niestandardowego walidatora w więcej niż jednym obiekcie Content. W tej sekcji przeprowadzimy Cię przez kroki tworzenia niestandardowego walidatora do walidacji kodów pocztowych.
Najpierw stwórz nowy typ reprezentujący wyniki walidacji ZipCode. Ta struktura będzie odpowiedzialna za zgłaszanie, czy dany string jest prawidłowym kodem pocztowym.
extension ValidatorResults {
/// Represents the result of a validator that checks if a string is a valid zip code.
public struct ZipCode {
/// Indicates whether the input is a valid zip code.
public let isValidZipCode: Bool
}
}
Następnie dostosuj nowy typ do protokołu ValidatorResult, który definiuje zachowanie oczekiwane od niestandardowego walidatora.
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"
}
}
Na koniec zaimplementuj logikę walidacji kodów pocztowych. Użyj wyrażenia regularnego, aby sprawdzić, czy podany string pasuje do formatu amerykańskiego kodu pocztowego.
private let zipCodeRegex: String = "^\\d{5}(?:[-\\s]\\d{4})?$"
extension Validator where T == String {
/// Validates whether a `String` is a valid zip code.
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)
}
}
}
Teraz, gdy zdefiniowałeś niestandardowy walidator zipCode, możesz go użyć do walidacji kodów pocztowych w swojej aplikacji. Po prostu dodaj poniższą linię do swojego kodu walidacji:
validations.add("zipCode", as: String.self, is: .zipCode)
Walidator Custom#
Walidator Custom najlepiej sprawdza się w przypadkach, gdy chcesz zwalidować właściwość tylko w jednym obiekcie Content. Ta implementacja ma następujące dwie zalety w porównaniu do rozszerzania API Validation:
Prostszą implementację niestandardowej logiki walidacji.
Krótszą składnię.
W tej sekcji przeprowadzimy Cię przez kroki tworzenia niestandardowego walidatora sprawdzającego, czy pracownik jest częścią naszej firmy, patrząc na właściwość nameAndSurname.
let allCompanyEmployees: [String] = [
"Everett Erickson",
"Sabrina Manning",
"Seth Gates",
"Melina Hobbs",
"Brendan Wade",
"Evie Richardson",
]
struct Employee: Content {
var nameAndSurname: String
var email: String
var age: Int
var role: String
static func validations(_ validations: inout Validations) {
validations.add(
"nameAndSurname",
as: String.self,
is: .custom("Validates whether employee is part of XYZ company by looking at name and surname.") { nameAndSurname in
for employee in allCompanyEmployees {
if employee == nameAndSurname {
return true
}
}
return false
}
)
}
}