feat: Initial commit
This commit is contained in:
17
Sources/App/BrowserSync.swift
Normal file
17
Sources/App/BrowserSync.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
import Vapor
|
||||
|
||||
#if DEBUG
|
||||
struct BrowserSyncHandler: LifecycleHandler {
|
||||
func didBoot(_ application: Application) throws {
|
||||
let process = Process()
|
||||
process.executableURL = URL(filePath: "/bin/sh")
|
||||
process.arguments = ["-c", "browser-sync reload"]
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
print("Could not auto-reload: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
35
Sources/App/Extensions/ApiController+respond.swift
Normal file
35
Sources/App/Extensions/ApiController+respond.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
import ApiController
|
||||
import Routes
|
||||
import Vapor
|
||||
|
||||
extension ApiController {
|
||||
|
||||
func respond(_ route: SiteRoute.Api, request: Vapor.Request) async throws -> any AsyncResponseEncodable {
|
||||
guard let encodable = try await json(route, request.logger) else {
|
||||
return HTTPStatus.ok
|
||||
}
|
||||
return AnyJSONResponse(value: encodable)
|
||||
}
|
||||
}
|
||||
|
||||
struct AnyJSONResponse: AsyncResponseEncodable {
|
||||
public var headers: HTTPHeaders = ["Content-Type": "application/json"]
|
||||
let value: any Encodable
|
||||
|
||||
init(additionalHeaders: HTTPHeaders = [:], value: any Encodable) {
|
||||
if additionalHeaders.contains(name: .contentType) {
|
||||
self.headers = additionalHeaders
|
||||
} else {
|
||||
headers.add(contentsOf: additionalHeaders)
|
||||
}
|
||||
self.value = value
|
||||
}
|
||||
|
||||
func encodeResponse(for request: Request) async throws -> Response {
|
||||
try Response(
|
||||
status: .ok,
|
||||
headers: headers,
|
||||
body: .init(data: JSONEncoder().encode(value))
|
||||
)
|
||||
}
|
||||
}
|
||||
57
Sources/App/Extensions/ViewController+respond.swift
Normal file
57
Sources/App/Extensions/ViewController+respond.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
import Elementary
|
||||
import Routes
|
||||
import Vapor
|
||||
import VaporElementary
|
||||
import ViewController
|
||||
|
||||
extension ViewController {
|
||||
func respond(route: SiteRoute.View, request: Vapor.Request) async throws -> any AsyncResponseEncodable {
|
||||
// let html = try await view(
|
||||
// for: route,
|
||||
// isHtmxRequest: request.isHtmxRequest,
|
||||
// logger: request.logger,
|
||||
// authenticate: { request.session.authenticate($0) },
|
||||
// currentUser: {
|
||||
// try request.auth.require(User.self)
|
||||
// }
|
||||
// )
|
||||
let html = try await view(route)
|
||||
return AnyHTMLResponse(value: html)
|
||||
}
|
||||
}
|
||||
|
||||
// Re-adapted from `HTMLResponse` in the VaporElementary package to work with any html types
|
||||
// returned from the view controller.
|
||||
struct AnyHTMLResponse: AsyncResponseEncodable {
|
||||
|
||||
public var chunkSize: Int
|
||||
public var headers: HTTPHeaders = ["Content-Type": "text/html; charset=utf-8"]
|
||||
var value: _SendableAnyHTMLBox
|
||||
|
||||
init(chunkSize: Int = 1024, additionalHeaders: HTTPHeaders = [:], value: AnySendableHTML) {
|
||||
self.chunkSize = chunkSize
|
||||
if additionalHeaders.contains(name: .contentType) {
|
||||
self.headers = additionalHeaders
|
||||
} else {
|
||||
headers.add(contentsOf: additionalHeaders)
|
||||
}
|
||||
self.value = .init(value)
|
||||
}
|
||||
|
||||
func encodeResponse(for request: Request) async throws -> Response {
|
||||
Response(
|
||||
status: .ok,
|
||||
headers: headers,
|
||||
body: .init(asyncStream: { [value, chunkSize] writer in
|
||||
guard let html = value.tryTake() else {
|
||||
assertionFailure("Non-sendable HTML value consumed more than once")
|
||||
request.logger.error("Non-sendable HTML value consumed more than once")
|
||||
throw Abort(.internalServerError)
|
||||
}
|
||||
try await writer.writeHTML(html, chunkSize: chunkSize)
|
||||
try await writer.write(.end)
|
||||
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
44
Sources/App/Middleware/DependenciesMiddleware.swift
Normal file
44
Sources/App/Middleware/DependenciesMiddleware.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
import ApiControllerLive
|
||||
import Dependencies
|
||||
@preconcurrency import PsychrometricClientLive
|
||||
import Vapor
|
||||
import ViewControllerLive
|
||||
|
||||
// Taken from discussions page on `swift-dependencies`.
|
||||
|
||||
struct DependenciesMiddleware: AsyncMiddleware {
|
||||
|
||||
private let values: DependencyValues.Continuation
|
||||
private let apiController: ApiController
|
||||
private let psychrometricClient: PsychrometricClient
|
||||
// private let database: DatabaseClient
|
||||
private let viewController: ViewController
|
||||
|
||||
init(
|
||||
// database: DatabaseClient,
|
||||
apiController: ApiController = .liveValue,
|
||||
psychrometricClient: PsychrometricClient = .liveValue,
|
||||
viewController: ViewController = .liveValue
|
||||
) {
|
||||
self.values = withEscapedDependencies { $0 }
|
||||
self.apiController = apiController
|
||||
// self.database = database
|
||||
self.psychrometricClient = psychrometricClient
|
||||
self.viewController = viewController
|
||||
}
|
||||
|
||||
func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
|
||||
try await values.yield {
|
||||
try await withDependencies {
|
||||
$0.apiController = apiController
|
||||
// $0.database = database
|
||||
// $0.dateFormatter = .liveValue
|
||||
$0.psychrometricClient = psychrometricClient
|
||||
$0.viewController = viewController
|
||||
} operation: {
|
||||
try await next.respond(to: request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
96
Sources/App/Middleware/URLRoutingMiddleware.swift
Normal file
96
Sources/App/Middleware/URLRoutingMiddleware.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
import URLRouting
|
||||
import Vapor
|
||||
import VaporRouting
|
||||
|
||||
// Taken from github.com/nevillco/vapor-routing
|
||||
|
||||
public extension Application {
|
||||
/// Mounts a router to the Vapor application.
|
||||
///
|
||||
/// See ``VaporRouting`` for more information on usage.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - router: A parser-printer that works on inputs of `URLRequestData`.
|
||||
/// - middleware: A closure for providing any per-route migrations to be run before processing the request.
|
||||
/// - closure: A closure that takes a `Request` and the router's output as arguments.
|
||||
func mount<R: Parser>(
|
||||
_ router: R,
|
||||
middleware: @escaping @Sendable (R.Output) -> [any Middleware]? = { _ in nil },
|
||||
use closure: @escaping @Sendable (Request, R.Output) async throws -> any AsyncResponseEncodable
|
||||
) where R.Input == URLRequestData, R: Sendable, R.Output: Sendable {
|
||||
self.middleware.use(AsyncRoutingMiddleware(router: router, middleware: middleware, respond: closure))
|
||||
}
|
||||
}
|
||||
|
||||
/// Serves requests using a router and response handler.
|
||||
///
|
||||
/// You will not typically need to interact with this type directly. Instead you should use the
|
||||
/// `mount` method on your Vapor application.
|
||||
///
|
||||
/// See ``VaporRouting`` for more information on usage.
|
||||
public struct AsyncRoutingMiddleware<Router: Parser>: AsyncMiddleware
|
||||
where Router.Input == URLRequestData,
|
||||
Router: Sendable,
|
||||
Router.Output: Sendable
|
||||
{
|
||||
let router: Router
|
||||
let middleware: @Sendable (Router.Output) -> [any Middleware]?
|
||||
let respond: @Sendable (Request, Router.Output) async throws -> any AsyncResponseEncodable
|
||||
|
||||
public func respond(
|
||||
to request: Request,
|
||||
chainingTo next: any AsyncResponder
|
||||
) async throws -> Response {
|
||||
if request.body.data == nil {
|
||||
try await _ = request.body.collect(max: request.application.routes.defaultMaxBodySize.value)
|
||||
.get()
|
||||
}
|
||||
|
||||
guard let requestData = URLRequestData(request: request)
|
||||
else { return try await next.respond(to: request) }
|
||||
|
||||
let route: Router.Output
|
||||
do {
|
||||
route = try router.parse(requestData)
|
||||
} catch let routingError {
|
||||
do {
|
||||
return try await next.respond(to: request)
|
||||
} catch {
|
||||
request.logger.info("\(routingError)")
|
||||
|
||||
guard request.application.environment == .development
|
||||
else { throw error }
|
||||
|
||||
return Response(status: .notFound, body: .init(string: "Routing \(routingError)"))
|
||||
}
|
||||
}
|
||||
|
||||
if let middleware = middleware(route) {
|
||||
return try await middleware.makeResponder(chainingTo: AsyncBasicResponder { request in
|
||||
try await self.respond(request, route).encodeResponse(for: request)
|
||||
}).respond(to: request).get()
|
||||
|
||||
// return try await middleware.respond(
|
||||
// to: request,
|
||||
// chainingTo: AsyncBasicResponder { request in
|
||||
// try await self.respond(request, route).encodeResponse(for: request)
|
||||
// }
|
||||
// ).get()
|
||||
} else {
|
||||
return try await respond(request, route).encodeResponse(for: request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
// app.mount(
|
||||
// router,
|
||||
// middleware: { route in
|
||||
// case .onboarding: return nil
|
||||
// case .signIn: return BasicAuthMiddleware()
|
||||
// default: return BearerAuthMiddleware()
|
||||
// },
|
||||
// use: { request, route in
|
||||
// // route handline
|
||||
// }
|
||||
// )
|
||||
@@ -1,9 +1,69 @@
|
||||
import ApiController
|
||||
import Dependencies
|
||||
import Routes
|
||||
import Vapor
|
||||
import VaporElementary
|
||||
@preconcurrency import VaporRouting
|
||||
import ViewControllerLive
|
||||
|
||||
// configures your application
|
||||
public func configure(_ app: Application) async throws {
|
||||
// uncomment to serve files from /Public folder
|
||||
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||
// register routes
|
||||
try routes(app)
|
||||
addMiddleware(to: app)
|
||||
|
||||
#if DEBUG
|
||||
// Live reload of the application for development when launched with the `./swift-dev` command
|
||||
app.lifecycle.use(BrowserSyncHandler())
|
||||
#endif
|
||||
|
||||
// register routes
|
||||
addRoutes(to: app)
|
||||
}
|
||||
|
||||
private func addMiddleware(to app: Application) {
|
||||
// cors middleware should come before default error middleware using `at: .beginning`
|
||||
let corsConfiguration = CORSMiddleware.Configuration(
|
||||
allowedOrigin: .all,
|
||||
allowedMethods: [.GET, .POST, .PUT, .OPTIONS, .DELETE, .PATCH],
|
||||
allowedHeaders: [.accept, .authorization, .contentType, .origin,
|
||||
.xRequestedWith, .userAgent, .accessControlAllowOrigin]
|
||||
)
|
||||
let cors = CORSMiddleware(configuration: corsConfiguration)
|
||||
app.middleware.use(cors, at: .beginning)
|
||||
|
||||
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||
// app.middleware.use(app.sessions.middleware)
|
||||
app.middleware.use(DependenciesMiddleware())
|
||||
}
|
||||
|
||||
private func addRoutes(to app: Application) {
|
||||
app.mount(
|
||||
SiteRoute.router,
|
||||
middleware: { _ in
|
||||
nil
|
||||
// if app.environment == .testing {
|
||||
// return nil
|
||||
// } else {
|
||||
// return $0.middleware()
|
||||
// }
|
||||
},
|
||||
use: siteHandler
|
||||
)
|
||||
}
|
||||
|
||||
@Sendable
|
||||
private func siteHandler(
|
||||
request: Request,
|
||||
route: SiteRoute
|
||||
) async throws -> any AsyncResponseEncodable {
|
||||
@Dependency(\.apiController) var apiController
|
||||
@Dependency(\.viewController) var viewController
|
||||
|
||||
switch route {
|
||||
case let .api(route):
|
||||
return try await apiController.respond(route, request: request)
|
||||
case .health:
|
||||
return HTTPStatus.ok
|
||||
case let .view(route):
|
||||
return try await viewController.respond(route: route, request: request)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import Vapor
|
||||
|
||||
func routes(_ app: Application) throws {
|
||||
app.get { req async in
|
||||
"It works!"
|
||||
}
|
||||
|
||||
app.get("hello") { req async -> String in
|
||||
"Hello, world!"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user