Skip to content

Instantly share code, notes, and snippets.

@jdmcd
Last active April 28, 2020 16:44
Show Gist options
  • Save jdmcd/e2e377f7d367fd8e280809caf346348d to your computer and use it in GitHub Desktop.
Save jdmcd/e2e377f7d367fd8e280809caf346348d to your computer and use it in GitHub Desktop.
A collection of helpers for Vapor 4
// An extension to replicate the same functionality from Vapor 3 with automatic content decoding:
import Foundation
import Vapor
/// A basic, closure-based `Responder`.
struct ContentBasicResponder<C: Content>: Responder {
/// The stored responder closure.
private let closure: (Request, C) throws -> EventLoopFuture<Response>
/// Create a new `BasicResponder`.
///
/// let notFound: Responder = BasicResponder { req in
/// let res = req.response(http: .init(status: .notFound))
/// return req.eventLoop.newSucceededFuture(result: res)
/// }
///
/// - parameters:
/// - closure: Responder closure.
public init(
closure: @escaping (Request, C) throws -> EventLoopFuture<Response>
) {
self.closure = closure
}
/// See `Responder`.
public func respond(to request: Request) -> EventLoopFuture<Response> {
do {
let content = try request.content.decode(C.self)
return try closure(request, content)
} catch {
return request.eventLoop.makeFailedFuture(error)
}
}
}
extension RoutesBuilder {
@discardableResult
public func post<Response, C>(
_ content: C.Type,
at path: PathComponent...,
use closure: @escaping (Request, C) throws -> Response
) -> Route
where Response: ResponseEncodable, C: Content
{
return self.onContent(content, .POST, path, use: closure)
}
@discardableResult
public func patch<Response, C>(
_ content: C.Type,
at path: PathComponent...,
use closure: @escaping (Request, C) throws -> Response
) -> Route
where Response: ResponseEncodable, C: Content
{
return self.onContent(content, .PATCH, path, use: closure)
}
@discardableResult
public func onContent<Response, C>(
_ contentType: C.Type,
_ method: HTTPMethod,
_ path: [PathComponent],
body: HTTPBodyStreamStrategy = .collect,
use closure: @escaping (Request, C) throws -> Response
) -> Route
where Response: ResponseEncodable, C: Content
{
let responder = ContentBasicResponder<C> { request, content in
if case .collect(let max) = body, request.body.data == nil {
return request.body.collect(max: max).flatMapThrowing { _ in
return try closure(request, content)
}.encodeResponse(for: request)
} else {
return try closure(request, content)
.encodeResponse(for: request)
}
}
let route = Route(
method: method,
path: path,
responder: responder,
requestType: Request.self,
responseType: Response.self
)
self.add(route)
return route
}
}
// Then, in your controller:
routes.post(LoginRequest.self, at: "login", use: login)
func login(req: Request, content: LoginRequest) throws -> Future<LoginResponse> {
// Work
}
// I wanted the ability to use the default Swift-Log format:
import Foundation
import Vapor
import Logging
extension LoggingSystem {
public static func customBootstrap(from environment: inout Environment) throws {
struct LogSignature: CommandSignature {
@Option(name: "log", help: "Change log level")
var level: Logger.Level?
init() { }
}
// Determine log level from environment.
let level = try LogSignature(from: &environment.commandInput).level
?? Environment.process.LOG_LEVEL
?? (environment == .production ? .notice: .info)
// Disable stack traces if log level > debug.
if level > .debug {
StackTrace.isCaptureEnabled = false
}
// Bootstrap logger to use Terminal.
return LoggingSystem.customBootstrap(console: Terminal(), level: level)
}
public static func customBootstrap(console: Console, level: Logger.Level = .info) {
self.bootstrap { label in
var logHandler = StreamLogHandler.standardOutput(label: label)
logHandler.logLevel = level
return logHandler
}
}
}
// And then in main.swift:
try LoggingSystem.customBootstrap(from: &env)
// Helpers for auto incrementing int ids and timestamps:
extension SchemaBuilder {
public func autoIncrementingId() -> Self {
self.field("id", .int, .identifier(auto: true))
}
public func timestampFields() -> Self {
self
.field("createdAt", .datetime)
.field("updatedAt", .datetime)
.field("deletedAt", .datetime)
}
}
// Then, in your migration:
return database.schema(User.schema)
.autoIncrementingId()
.timestampFields()
// Restores some of the type-safe functionality of parameters (parts of this based off of @0xtim's work)
// A few gotchas: the IDValue on the Model must be an Int and this doesn't work with multiple parameters of the same type in the path
import Vapor
import Fluent
protocol ParameterModel {
static var parameterKey: String { get }
static var parameter: PathComponent { get }
}
extension ParameterModel where Self: Model {
static var parameterKey: String {
return Self.schema
}
static var parameter: PathComponent {
return PathComponent(stringLiteral: ":\(Self.parameterKey)")
}
}
extension Request {
func next<M: ParameterModel & Model>(_ type: M.Type) -> Future<M> where M.IDValue == Int {
guard let stringValue = self.parameters.get(M.parameterKey) else {
return future(error: Abort(.badRequest, reason: "Could not find \(M.parameterKey) parameter"))
}
guard let intValue = Int(stringValue) else {
return future(error: Abort(.badRequest, reason: "Could not convert \(M.parameterKey) parameter to int"))
}
return M
.find(intValue, on: db)
.unwrap(or: Abort(.badRequest, reason: "Could not find a \(M.self) with ID of \(intValue)"))
}
}
// In the model:
extension User: ParameterModel { }
// In the router:
routes.get(User.parameter, use: getUser)
// In the route:
let userQuery = req.next(User.self)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment