Content¶
La API de contenido de Vapor nos permite codificar / decodificar fácilmente estructuras Codable en / desde mensajes HTTP. La codificación JSON se usa por defecto con soporte preparado para Formulario URL-Encoded y Multipart. La API también se puede configurar, permitiéndote agregar, modificar o reemplazar estrategias de codificación para ciertos tipos de contenido HTTP.
Presentación¶
Para comprender cómo funciona la API de contenido de Vapor, primero debes comprender algunos conceptos básicos sobre los mensajes HTTP. Presta atención a la siguiente solicitud de ejemplo.
POST /greeting HTTP/1.1
content-type: application/json
content-length: 18
{"hello": "world"}
Esta petición indica que contiene datos codificados en JSON utilizando la cabecera (header) content-type
y el tipo de contenido (media type) application/json
. A continuación, algunos datos JSON se hayan en el cuerpo (body) de la petición, después de las cabeceras.
Estructura del Contenido¶
El primer paso para decodificar este mensaje HTTP es crear un tipo Codable que coincida con la estructura esperada.
struct Greeting: Content {
var hello: String
}
Conformar el tipo con Content
agregará automáticamente la conformidad con Codable
, junto con utilidades adicionales para trabajar con la API de contenido.
Una vez que tengas la estructura del contenido, puedes decodificarlo desde la solicitud entrante usando req.content
.
app.post("greeting") { req in
let greeting = try req.content.decode(Greeting.self)
print(greeting.hello) // "world"
return HTTPStatus.ok
}
El método de decodificación decode
utiliza el tipo de contenido de la solicitud para encontrar un decodificador apropiado. Si no se encuentra un decodificador, o la solicitud no contiene el header del tipo de contenido, se lanzará un error 415
.
Eso significa que esta ruta acepta automáticamente todos los demás tipos de contenido admitidos, como el formulario url-encoded:
POST /greeting HTTP/1.1
content-type: application/x-www-form-urlencoded
content-length: 11
hello=world
En el caso de subidas de archivos, la propiedad de contenido debe ser del tipo Data
struct Profile: Content {
var name: String
var email: String
var image: Data
}
Tipos de Contenido Soportados¶
A continuación se muestran los tipos de contenido (media types) que la API admite por defecto.
nombre | valor de header | media type |
---|---|---|
JSON | application/json | .json |
Multipart | multipart/form-data | .formData |
URL-Encoded Form | application/x-www-form-urlencoded | .urlEncodedForm |
Plaintext | text/plain | .plainText |
HTML | text/html | .html |
No todos los tipos de contenido son compatibles con todas las funciones Codable
. Por ejemplo, JSON no admite fragmentos de nivel superior (top-level) y Plaintext no admite datos anidados.
Consultas (Query)¶
Las API de contenido de Vapor admiten el manejo de datos URL codificados en la cadena de consulta.
Decodificación¶
Para comprender cómo funciona la decodificación de una cadena de consulta de URL, echa un vistazo a la siguiente solicitud de ejemplo.
GET /hello?name=Vapor HTTP/1.1
content-length: 0
Al igual que las APIs para manejar el contenido del body del mensaje HTTP, el primer paso para analizar cadenas de consulta de URL es crear un struct
que coincida con la estructura esperada.
struct Hello: Content {
var name: String?
}
Ten en cuenta que name
es una String
opcional, ya que las cadenas de consulta de URL siempre deben ser opcionales. Si deseas solicitar un parámetro, utiliza un parámetro de ruta en su lugar.
Ahora que tienes un struct Content
para la cadena de consulta esperada de esta ruta, puedes decodificarla.
app.get("hello") { req -> String in
let hello = try req.query.decode(Hello.self)
return "Hello, \(hello.name ?? "Anonymous")"
}
Esta ruta daría como resultado la siguiente respuesta a la solicitud de ejemplo anterior:
HTTP/1.1 200 OK
content-length: 12
Hello, Vapor
Si se omitiera la cadena de consulta, como en la siguiente solicitud, se usaría en su lugar el nombre "Anonymous".
GET /hello HTTP/1.1
content-length: 0
Valores Simples¶
Además de decodificar a un struct Content
, Vapor también soporta la obtención de valores individuales de la cadena de consulta mediante subíndices.
let name: String? = req.query["name"]
Hooks¶
Vapor llamará automáticamente a beforeEncode
y afterDecode
en un tipo Content
. Se proporcionan implementaciones predeterminadas que no hacen nada, pero puedes usar estos métodos para ejecutar una lógica personalizada.
// Se ejecuta después de decodificar este Content. `mutating` solo se requiere para structs, no para clases.
mutating func afterDecode() throws {
// Es posible que no se pase name, pero si lo hace, entonces no puede ser una cadena vacía.
self.name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines)
if let name = self.name, name.isEmpty {
throw Abort(.badRequest, reason: "Name must not be empty.")
}
}
// Se ejecuta antes de que se codifique este Content. `mutating` solo se requiere para structs, no para clases.
mutating func beforeEncode() throws {
// *Siempre* tiene que devolver un name, y no puede ser una cadena vacía.
guard
let name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines),
!name.isEmpty
else {
throw Abort(.badRequest, reason: "Name must not be empty.")
}
self.name = name
}
Sobreescribiendo Valores Predeterminados¶
Los codificadores y decodificadores predeterminados utilizados por las APIs de Content de Vapor se pueden configurar.
Global¶
ContentConfiguration.global
te permite cambiar los codificadores y decodificadores que usa Vapor por defecto. Esto es útil para cambiar la forma en que toda la aplicación analiza y serializa los datos.
// crea un nuevo JSON encoder que use fechas de marca de tiempo de Unix
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .secondsSince1970
// sobreescriba el codificador global utilizado para el media type `.json`
ContentConfiguration.global.use(encoder: encoder, for: .json)
La mutación de ContentConfiguration
generalmente se realiza en configure.swift
.
Usos Únicos (One-Off)¶
Las llamadas a métodos de codificación y decodificación como req.content.decode
admiten el paso de codificadores personalizados para usos únicos.
// crea un nuevo JSON decoder que use fechas de marca de tiempo de Unix
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
// decodifica el struct Hello usando un decodificador personalizado
let hello = try req.content.decode(Hello.self, using: decoder)
Codificadores Personalizados¶
Las aplicaciones y paquetes de terceros pueden agregar soporte para tipos de contenido que Vapor no admite de forma predeterminada mediante la creación de codificadores personalizados.
Content¶
Vapor especifica dos protocolos para codificadores capaces de manejar contenido en el body de mensajes HTTP: ContentDecoder
y ContentEncoder
.
public protocol ContentEncoder {
func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws
where E: Encodable
}
public protocol ContentDecoder {
func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D
where D: Decodable
}
Conformar con estos protocolos permite que sus codificadores personalizados se registren en ContentConfiguration
como se especificó anteriormente.
URL Query¶
Vapor especifica dos protocolos para codificadores capaces de manejar contenido en cadenas de consulta de URL: URLQueryDecoder
y URLQueryEncoder
.
public protocol URLQueryDecoder {
func decode<D>(_ decodable: D.Type, from url: URI) throws -> D
where D: Decodable
}
public protocol URLQueryEncoder {
func encode<E>(_ encodable: E, to url: inout URI) throws
where E: Encodable
}
Conformar con estos protocolos permite que sus codificadores personalizados se registren en ContentConfiguration
para manejar cadenas de consulta de URL usando los métodos use(urlEncoder:)
y use(urlDecoder:)
.
ResponseEncodable
Personalizado¶
Otro enfoque consiste en implementar ResponseEncodable
en sus tipos. Considera este tipo de wrapper HTML
trivial:
struct HTML {
let value: String
}
Luego su implementación con ResponseEncodable
se vería así:
extension HTML: ResponseEncodable {
public func encodeResponse(for request: Request) -> EventLoopFuture<Response> {
var headers = HTTPHeaders()
headers.add(name: .contentType, value: "text/html")
return request.eventLoop.makeSucceededFuture(.init(
status: .ok, headers: headers, body: .init(string: value)
))
}
}
Si estás usando async
/await
, puedes usar AsyncResponseEncodable
:
extension HTML: AsyncResponseEncodable {
public func encodeResponse(for request: Request) async throws -> Response {
var headers = HTTPHeaders()
headers.add(name: .contentType, value: "text/html")
return .init(status: .ok, headers: headers, body: .init(string: value))
}
}
Ten en cuenta que esto permite personalizar el header Content-Type
. Consulta la referencia de HTTPHeaders
para obtener más detalles.
Luego puede usar HTML
como tipo de respuesta en tus rutas:
app.get { _ in
HTML(value: """
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>
""")
}