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

Commit

Permalink
Feature/response modifier (#10)
Browse files Browse the repository at this point in the history
* Implementation ResponseModifier

* Adds documentation

* Adds ResponseModifier test

* Fixes Swiftlint errors
  • Loading branch information
jhoogstraat authored Apr 8, 2020
1 parent 3955621 commit f87f948
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 0 deletions.
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

0 comments on commit f87f948

Please sign in to comment.