diff --git a/Sources/Corvus/Endpoints/Modifiers/ResponseModifier.swift b/Sources/Corvus/Endpoints/Modifiers/ResponseModifier.swift new file mode 100644 index 0000000..d066678 --- /dev/null +++ b/Sources/Corvus/Endpoints/Modifiers/ResponseModifier.swift @@ -0,0 +1,57 @@ +import Vapor +import Fluent + +/// A class that wraps a component which utilizes a `.respond(with:)` modifier. That +/// allows Corvus to chain modifiers, as it gets treated as any other struct +/// conforming to `RestEndpoint`. +public final class ResponseModifier< + Q: RestEndpoint, + R: CorvusResponse>: +RestEndpoint where Q.Element == R.Item { + + /// The `Response` of this modifier. + public typealias Response = R + + /// The `RestEndpoint` the `.respond(with:)` modifier is attached to. + public let restEndpoint: Q + + /// The HTTP operation type of the component. + public let operationType: OperationType + + /// Initializes the modifier with its underlying `RestEndpoint`. + /// + /// - Parameters: + /// - queryEndpoint: The `QueryEndpoint` which the modifer is attached + /// to. + public init(_ restEndpoint: Q) { + self.restEndpoint = restEndpoint + self.operationType = restEndpoint.operationType + } + + /// A method which transform the restEndpoints's handler return value to a + /// `Response`. + /// + /// - Parameter req: An incoming `Request`. + /// - Returns: An `EventLoopFuture` containing the + /// `ResponseModifier`'s `Response`. + public func handler(_ req: Request) + throws -> EventLoopFuture { + try restEndpoint.handler(req).map(Response.init) + } + +} + +/// An extension that adds a `.respond(with:)` modifier to `RestEndpoint`. +extension RestEndpoint { + + /// A modifier used to transform the values returned by a component using a + /// `CorvusResponse`. + /// + /// - Parameter as: A type conforming to `CorvusResponse`. + /// - Returns: An instance of a `ResponseModifier` with the supplied `CorvusResponse`. + public func respond( + with: R.Type + ) -> ResponseModifier { + ResponseModifier(self) + } +} diff --git a/Sources/Corvus/Protocols/http/CorvusResponse.swift b/Sources/Corvus/Protocols/http/CorvusResponse.swift new file mode 100644 index 0000000..c9c7c46 --- /dev/null +++ b/Sources/Corvus/Protocols/http/CorvusResponse.swift @@ -0,0 +1,14 @@ +import Vapor + +/// `CorvusResponse` is a wrapper type for the result of`QueryEndpoint`s. Can +/// be used to add metadata to a response. +/// +public protocol CorvusResponse: Content { + + /// The item is equivalent to the `QueryEndpoint`'s `QuerySubject`. + associatedtype Item + + /// Initialises a `CorvusResponse` with a given item. + /// Normally this is the result of the `QueryEndpoints`'s handler function. + init(item: Item) +} diff --git a/Tests/CorvusTests/ApplicationTests.swift b/Tests/CorvusTests/ApplicationTests.swift index e8c7422..3af1b49 100644 --- a/Tests/CorvusTests/ApplicationTests.swift +++ b/Tests/CorvusTests/ApplicationTests.swift @@ -675,6 +675,70 @@ final class ApplicationTests: XCTestCase { XCTAssertEqual(res.status, .notFound) } } + + func testResponseModifier() throws { + + // This is a basic response. + struct CreateResponse: CorvusResponse, Equatable { + let created = true + let name: String + + init(item: Account) { + self.name = item.name + } + } + + // This response is a more complex example using generics. + // This allows for responses which work with any number of models. + struct ReadResponse: CorvusResponse, Equatable { + let success = true + let payload: [Model] + + init(item: [Model]) { + payload = item + } + } + + final class ResponseModifierTest: RestApi { + + let testParameter = Parameter() + + var content: Endpoint { + Group("api", "accounts") { + Create().respond(with: CreateResponse.self) + ReadAll().respond(with: ReadResponse.self) + } + } + } + + let app = Application(.testing) + defer { app.shutdown() } + let readOneTest = ResponseModifierTest() + + app.databases.use(.sqlite(.memory), as: .test, isDefault: true) + app.migrations.add(CreateAccount()) + + try app.autoMigrate().wait() + + try app.register(collection: readOneTest) + + let account = Account(name: "Berzan") + let createRes = CreateResponse(item: account) + let readRes = ReadResponse(item: [account]) + + try app.testable().test( + .POST, + "/api/accounts", + headers: ["content-type": "application/json"], + body: account.encode(), + afterResponse: { res in + XCTAssertEqualJSON(res.body.string, createRes) + } + ).test(.GET, "/api/accounts/") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqualJSON(res.body.string, readRes) + } + } } extension DatabaseID {