Skip to content
This repository has been archived by the owner on Sep 7, 2021. It is now read-only.

Feature/response modifier #10

Merged
merged 4 commits into from
Apr 8, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Sources/Corvus/Endpoints/Modifiers/ResponseModifier.swift
Original file line number Diff line number Diff line change
@@ -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<Response> {
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<R: CorvusResponse>(
with: R.Type
) -> ResponseModifier<Self, R> {
ResponseModifier(self)
}
}
14 changes: 14 additions & 0 deletions Sources/Corvus/Protocols/http/CorvusResponse.swift
Original file line number Diff line number Diff line change
@@ -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)
}
64 changes: 64 additions & 0 deletions Tests/CorvusTests/ApplicationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Model: AnyModel & Equatable>: CorvusResponse, Equatable {
let success = true
let payload: [Model]

init(item: [Model]) {
payload = item
}
}

final class ResponseModifierTest: RestApi {

let testParameter = Parameter<Account>()

var content: Endpoint {
Group("api", "accounts") {
Create<Account>().respond(with: CreateResponse.self)
ReadAll<Account>().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 {
Expand Down