Last active
April 28, 2020 16:44
-
-
Save jdmcd/e2e377f7d367fd8e280809caf346348d to your computer and use it in GitHub Desktop.
A collection of helpers for Vapor 4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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