feat: Initial commit

This commit is contained in:
2025-02-25 12:01:47 -05:00
parent 3c0c100e50
commit a289075e75
2557 changed files with 379222 additions and 47 deletions

View 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

View 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))
)
}
}

View 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)
})
)
}
}

View 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)
}
}
}
}

View 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
// }
// )

View File

@@ -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)
}
}

View File

@@ -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!"
}
}