Skip to content

Commit

Permalink
Add Content hooks (vapor#2267)
Browse files Browse the repository at this point in the history
* Adds automatic calls to beforeEncode() and afterDecode() on Content

* Added doc comment
  • Loading branch information
grosch authored and pull[bot] committed Apr 15, 2020
1 parent ecf48f5 commit 657613e
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 34 deletions.
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


/// 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"
}
}

0 comments on commit 657613e

Please sign in to comment.