Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Content hooks #2267

Merged
merged 2 commits into from Apr 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions Sources/Vapor/Client/ClientRequest.swift
Expand Up @@ -63,6 +63,23 @@ extension ClientRequest {
}
return try decoder.decode(D.self, from: body, headers: self.headers)
}

mutating func encode<C>(_ content: C, using encoder: ContentEncoder) throws where C : Content {
var content = content
try content.beforeEncode()
var body = ByteBufferAllocator().buffer(capacity: 0)
try encoder.encode(content, to: &body, headers: &self.headers)
self.body = body
}

func decode<C>(_ content: C.Type, using decoder: ContentDecoder) throws -> C where C : Content {
guard let body = self.body else {
throw Abort(.lengthRequired)
}
var decoded = try decoder.decode(C.self, from: body, headers: self.headers)
try decoded.afterDecode()
return decoded
}
}

public var content: ContentContainer {
Expand Down
17 changes: 17 additions & 0 deletions Sources/Vapor/Client/ClientResponse.swift
Expand Up @@ -31,6 +31,23 @@ extension ClientResponse {
}
return try decoder.decode(D.self, from: body, headers: self.headers)
}

mutating func encode<C>(_ content: C, using encoder: ContentEncoder) throws where C : Content {
var body = ByteBufferAllocator().buffer(capacity: 0)
var content = content
try content.beforeEncode()
try encoder.encode(content, to: &body, headers: &self.headers)
self.body = body
}

func decode<C>(_ content: C.Type, using decoder: ContentDecoder) throws -> C where C : Content {
guard let body = self.body else {
throw Abort(.lengthRequired)
}
var decoded = try decoder.decode(C.self, from: body, headers: self.headers)
try decoded.afterDecode()
return decoded
}
}

public var content: ContentContainer {
Expand Down
20 changes: 20 additions & 0 deletions Sources/Vapor/Content/Content.swift
Expand Up @@ -37,6 +37,23 @@ public protocol Content: Codable, RequestDecodable, ResponseEncodable {
/// }
///
static var defaultContentType: HTTPMediaType { get }

/// Called before this `Content` is encoded, generally for a `Response` object.
///
/// You should use this method to perform any "sanitizing" which you need on the data.
/// For example, you may wish to replace empty strings with a `nil`, `trim()` your
/// strings or replace empty arrays with `nil`. You can also use this method to abort
/// the encoding if something isn't valid. An empty array may indicate an error, for example.
mutating func beforeEncode() throws
grosch marked this conversation as resolved.
Show resolved Hide resolved


/// Called after this `Content` is decoded, generally from a `Request` object.
///
/// You should use this method to perform any "sanitizing" which you need on the data.
/// For example, you may wish to replace empty strings with a `nil`, `trim()` your
/// strings or replace empty arrays with `nil`. You can also use this method to abort
/// the decoding if something isn't valid. An empty string may indicate an error, for example.
mutating func afterDecode() throws
}

/// MARK: Default Implementations
Expand Down Expand Up @@ -64,6 +81,9 @@ extension Content {
}
return request.eventLoop.makeSucceededFuture(response)
}

public mutating func beforeEncode() throws { }
public mutating func afterDecode() throws { }
}

// MARK: Default Conformances
Expand Down
27 changes: 26 additions & 1 deletion Sources/Vapor/Content/ContentContainer.swift
Expand Up @@ -15,6 +15,13 @@ extension ContentContainer {
return try self.decode(D.self, using: self.configuredDecoder())
}

public func decode<C>(_ decodable: C.Type) throws -> C where C: Content {
var content = try self.decode(C.self, using: self.configuredDecoder())
try content.afterDecode()

return content
}

// MARK: Encode

/// Serializes an `Encodable` object to this message using specific `HTTPMessageEncoder`.
Expand All @@ -27,6 +34,8 @@ extension ContentContainer {
public mutating func encode<C>(_ encodable: C) throws
where C: Content
{
var encodable = encodable
try encodable.beforeEncode()
try self.encode(encodable, as: C.defaultContentType)
}

Expand All @@ -44,7 +53,23 @@ extension ContentContainer {
{
try self.encode(encodable, using: self.configuredEncoder(for: contentType))
}


/// Serializes a `Content` object to this message using specific `HTTPMessageEncoder`.
///
/// try req.content.encode(user, using: JSONEncoder())
///
/// - parameters:
/// - content: Instance of generic `Content` to serialize to this HTTP message.
/// - encoder: Specific `HTTPMessageEncoder` to use.
/// - throws: Errors during serialization.
public mutating func encode<C>(_ content: C, as contentType: HTTPMediaType) throws
where C: Content
{
var content = content
try content.beforeEncode()
try self.encode(content, using: self.configuredEncoder(for: contentType))
}

// MARK: Single Value

/// Fetches a single `Decodable` value at the supplied key-path from this HTTP request's query string.
Expand Down
18 changes: 18 additions & 0 deletions Sources/Vapor/Request/Request.swift
Expand Up @@ -78,6 +78,24 @@ public final class Request: CustomStringConvertible {
}
return try decoder.decode(D.self, from: body, headers: self.request.headers)
}

func encode<C>(_ content: C, using encoder: ContentEncoder) throws where C : Content {
var content = content
try content.beforeEncode()
var body = ByteBufferAllocator().buffer(capacity: 0)
try encoder.encode(content, to: &body, headers: &self.request.headers)
self.request.bodyStorage = .collected(body)
}

func decode<C>(_ content: C.Type, using decoder: ContentDecoder) throws -> C where C : Content {
guard let body = self.request.body.data else {
self.request.logger.error("Decoding streaming bodies not supported")
throw Abort(.unprocessableEntity)
}
var decoded = try decoder.decode(C.self, from: body, headers: self.request.headers)
try decoded.afterDecode()
return decoded
}
}

public var content: ContentContainer {
Expand Down
17 changes: 17 additions & 0 deletions Sources/Vapor/Response/Response.swift
Expand Up @@ -84,6 +84,23 @@ public final class Response: CustomStringConvertible {
}
return try decoder.decode(D.self, from: body, headers: self.response.headers)
}

func encode<C>(_ content: C, using encoder: ContentEncoder) throws where C : Content {
var content = content
try content.beforeEncode()
var body = ByteBufferAllocator().buffer(capacity: 0)
try encoder.encode(content, to: &body, headers: &self.response.headers)
self.response.body = .init(buffer: body)
}

func decode<C>(_ content: C.Type, using decoder: ContentDecoder) throws -> C where C : Content {
guard let body = self.response.body.buffer else {
throw Abort(.unprocessableEntity)
}
var decoded = try decoder.decode(C.self, from: body, headers: self.response.headers)
try decoded.afterDecode()
return decoded
}
}

public var content: ContentContainer {
Expand Down
6 changes: 6 additions & 0 deletions Sources/XCTVapor/XCTHTTPRequest.swift
Expand Up @@ -19,6 +19,12 @@ public struct XCTHTTPRequest {
func decode<D>(_ decodable: D.Type, using decoder: ContentDecoder) throws -> D where D : Decodable {
fatalError("Decoding from test request is not supported.")
}

mutating func encode<C>(_ content: C, using encoder: ContentEncoder) throws where C : Content {
var content = content
try content.beforeEncode()
try encoder.encode(content, to: &self.body, headers: &self.headers)
}
}

public var content: ContentContainer {
Expand Down
6 changes: 6 additions & 0 deletions Sources/XCTVapor/XCTHTTPResponse.swift
Expand Up @@ -20,6 +20,12 @@ extension XCTHTTPResponse {
func decode<D>(_ decodable: D.Type, using decoder: ContentDecoder) throws -> D where D : Decodable {
try decoder.decode(D.self, from: self.body, headers: self.headers)
}

func decode<C>(_ content: C.Type, using decoder: ContentDecoder) throws -> C where C : Content {
var decoded = try decoder.decode(C.self, from: self.body, headers: self.headers)
try decoded.afterDecode()
return decoded
}
}

public var content: ContentContainer {
Expand Down
107 changes: 74 additions & 33 deletions Tests/VaporTests/ContentTests.swift
Expand Up @@ -232,38 +232,79 @@ final class ContentTests: XCTestCase {
}

func testJSONPreservesHTTPHeaders() throws {
let app = Application(.testing)
defer { app.shutdown() }

app.get("check") { (req: Request) -> String in
return "\(req.headers.first(name: .init("X-Test-Value")) ?? "MISSING").\(req.headers.first(name: .contentType) ?? "?")"
}

try app.test(.GET, "/check", headers: ["X-Test-Value": "PRESENT"], beforeRequest: { req in
try req.content.encode(["foo": "bar"], as: .json)
}) { res in
XCTAssertEqual(res.body.string, "PRESENT.application/json; charset=utf-8")
}
}

func testJSONAllowsContentTypeOverride() throws {
let app = Application(.testing)
defer { app.shutdown() }

app.get("check") { (req: Request) -> String in
return "\(req.headers.first(name: .init("X-Test-Value")) ?? "MISSING").\(req.headers.first(name: .contentType) ?? "?")"
}
// Me and my sadistic sense of humor.
ContentConfiguration.global.use(decoder: try! ContentConfiguration.global.requireDecoder(for: .json), for: .xml)

try app.testable().test(.GET, "/check", headers: [
"X-Test-Value": "PRESENT"
], beforeRequest: { req in
try req.content.encode(["foo": "bar"], as: .json)
req.headers.contentType = .xml
}) { res in
XCTAssertEqual(res.body.string, "PRESENT.application/xml; charset=utf-8")
}
}
let app = Application(.testing)
defer { app.shutdown() }

app.get("check") { (req: Request) -> String in
return "\(req.headers.first(name: .init("X-Test-Value")) ?? "MISSING").\(req.headers.first(name: .contentType) ?? "?")"
}

try app.test(.GET, "/check", headers: ["X-Test-Value": "PRESENT"], beforeRequest: { req in
try req.content.encode(["foo": "bar"], as: .json)
}) { res in
XCTAssertEqual(res.body.string, "PRESENT.application/json; charset=utf-8")
}
}

func testJSONAllowsContentTypeOverride() throws {
let app = Application(.testing)
defer { app.shutdown() }

app.get("check") { (req: Request) -> String in
return "\(req.headers.first(name: .init("X-Test-Value")) ?? "MISSING").\(req.headers.first(name: .contentType) ?? "?")"
}
// Me and my sadistic sense of humor.
ContentConfiguration.global.use(decoder: try! ContentConfiguration.global.requireDecoder(for: .json), for: .xml)

try app.testable().test(.GET, "/check", headers: [
"X-Test-Value": "PRESENT"
], beforeRequest: { req in
try req.content.encode(["foo": "bar"], as: .json)
req.headers.contentType = .xml
}) { res in
XCTAssertEqual(res.body.string, "PRESENT.application/xml; charset=utf-8")
}
}

func testBeforeEncodeContent() throws {
let content = SampleContent()
XCTAssertEqual(content.name, "old name")

let response = Response(status: .ok)
try response.content.encode(content)

let body = try XCTUnwrap(response.body.string)
XCTAssertEqual(body, #"{"name":"new name"}"#)
}

func testAfterContentEncode() throws {
let app = Application()
defer { app.shutdown() }

var body = ByteBufferAllocator().buffer(capacity: 0)
body.writeString(#"{"name": "before decode"}"#)

let request = Request(
application: app,
collectedBody: body,
on: EmbeddedEventLoop()
)

request.headers.contentType = .json

let content = try request.content.decode(SampleContent.self)
XCTAssertEqual(content.name, "new name after decode")
}
}

private struct SampleContent: Content {
var name = "old name"

mutating func beforeEncode() throws {
name = "new name"
}

mutating func afterDecode() throws {
name = "new name after decode"
}
}