feat: Begins vapor application, begins view controller.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "f8ca659e4ec9041ea590b94c8dc267718be8941a4594eb74964b6396e987ca95",
|
"originHash" : "5d6dad57209ac74e3c47d8e8eb162768b81c9e63e15df87d29019d46a13cfec2",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "async-http-client",
|
"identity" : "async-http-client",
|
||||||
@@ -37,6 +37,24 @@
|
|||||||
"version" : "4.15.2"
|
"version" : "4.15.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "elementary",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/elementary-swift/elementary.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "65803c4770b6c8b6452c6ce83ab570c1b7042528",
|
||||||
|
"version" : "0.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "elementary-htmx",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/elementary-swift/elementary-htmx.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "f7ba147f2a076142a90a4f8300a89584164dba6d",
|
||||||
|
"version" : "0.5.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "fluent",
|
"identity" : "fluent",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -379,6 +397,24 @@
|
|||||||
"version" : "4.120.0"
|
"version" : "4.120.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "vapor-elementary",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor-community/vapor-elementary.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "e1326d2439a9d4bd271c24b9765c195f7f793e77",
|
||||||
|
"version" : "0.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "vapor-routing",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/vapor-routing.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "ae1db2ec96fad88b00173a265313de2c447a9945",
|
||||||
|
"version" : "0.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "websocket-kit",
|
"identity" : "websocket-kit",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -5,23 +5,41 @@ import PackageDescription
|
|||||||
let package = Package(
|
let package = Package(
|
||||||
name: "swift-manual-d",
|
name: "swift-manual-d",
|
||||||
products: [
|
products: [
|
||||||
|
.executable(name: "App", targets: ["App"]),
|
||||||
|
.library(name: "DatabaseClient", targets: ["DatabaseClient"]),
|
||||||
.library(name: "ManualDCore", targets: ["ManualDCore"]),
|
.library(name: "ManualDCore", targets: ["ManualDCore"]),
|
||||||
.library(name: "ManualDClient", targets: ["ManualDClient"]),
|
.library(name: "ManualDClient", targets: ["ManualDClient"]),
|
||||||
|
.library(name: "ViewController", targets: ["ViewController"]),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
// 💧 A server-side Swift web framework.
|
|
||||||
.package(url: "https://github.com/vapor/vapor.git", from: "4.110.1"),
|
.package(url: "https://github.com/vapor/vapor.git", from: "4.110.1"),
|
||||||
// 🗄 An ORM for SQL and NoSQL databases.
|
|
||||||
.package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"),
|
.package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"),
|
||||||
// 🪶 Fluent driver for SQLite.
|
|
||||||
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"),
|
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"),
|
||||||
// 🔵 Non-blocking, event-driven networking Swift. Used for, custom executors
|
|
||||||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
|
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
|
||||||
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"),
|
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"),
|
||||||
.package(url: "https://github.com/pointfreeco/swift-url-routing.git", from: "0.6.2"),
|
.package(url: "https://github.com/pointfreeco/swift-url-routing.git", from: "0.6.2"),
|
||||||
|
.package(url: "https://github.com/pointfreeco/vapor-routing.git", from: "0.1.3"),
|
||||||
.package(url: "https://github.com/pointfreeco/swift-case-paths.git", from: "1.6.0"),
|
.package(url: "https://github.com/pointfreeco/swift-case-paths.git", from: "1.6.0"),
|
||||||
|
.package(url: "https://github.com/elementary-swift/elementary.git", from: "0.6.0"),
|
||||||
|
.package(url: "https://github.com/elementary-swift/elementary-htmx.git", from: "0.5.0"),
|
||||||
|
.package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.1.0"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
|
.executableTarget(
|
||||||
|
name: "App",
|
||||||
|
dependencies: [
|
||||||
|
.target(name: "DatabaseClient"),
|
||||||
|
.target(name: "ViewController"),
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "Fluent", package: "fluent"),
|
||||||
|
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
|
||||||
|
.product(name: "Vapor", package: "vapor"),
|
||||||
|
.product(name: "NIOCore", package: "swift-nio"),
|
||||||
|
.product(name: "NIOPosix", package: "swift-nio"),
|
||||||
|
.product(name: "VaporElementary", package: "vapor-elementary"),
|
||||||
|
.product(name: "VaporRouting", package: "vapor-routing"),
|
||||||
|
]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "DatabaseClient",
|
name: "DatabaseClient",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
@@ -40,6 +58,12 @@ let package = Package(
|
|||||||
.product(name: "CasePaths", package: "swift-case-paths"),
|
.product(name: "CasePaths", package: "swift-case-paths"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "ApiRouteTests",
|
||||||
|
dependencies: [
|
||||||
|
.target(name: "ManualDCore")
|
||||||
|
]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "ManualDClient",
|
name: "ManualDClient",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
@@ -55,10 +79,15 @@ let package = Package(
|
|||||||
.product(name: "DependenciesTestSupport", package: "swift-dependencies"),
|
.product(name: "DependenciesTestSupport", package: "swift-dependencies"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
.target(
|
||||||
name: "ApiRouteTests",
|
name: "ViewController",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.target(name: "ManualDCore")
|
.target(name: "ManualDCore"),
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||||
|
.product(name: "Elementary", package: "elementary"),
|
||||||
|
.product(name: "ElementaryHTMX", package: "elementary-htmx"),
|
||||||
|
.product(name: "Vapor", package: "vapor"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
0
Public/css/main.css
Normal file
0
Public/css/main.css
Normal file
0
Public/js/main.js
Normal file
0
Public/js/main.js
Normal file
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
|
||||||
7
Sources/App/Extensions/Request+extensions.swift
Normal file
7
Sources/App/Extensions/Request+extensions.swift
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Vapor
|
||||||
|
|
||||||
|
extension Request {
|
||||||
|
var isHtmxRequest: Bool {
|
||||||
|
headers.contains(name: "hx-request")
|
||||||
|
}
|
||||||
|
}
|
||||||
60
Sources/App/Extensions/ViewController+respond.swift
Normal file
60
Sources/App/Extensions/ViewController+respond.swift
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import Elementary
|
||||||
|
import ManualDCore
|
||||||
|
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(
|
||||||
|
.init(
|
||||||
|
route: route,
|
||||||
|
isHtmxRequest: request.isHtmxRequest,
|
||||||
|
logger: request.logger,
|
||||||
|
// authenticate: { request.session.authenticate($0) },
|
||||||
|
// currentUser: {
|
||||||
|
// try request.auth.require(User.self)
|
||||||
|
// }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Sources/App/Middleware/DependenciesMiddleware.swift
Normal file
41
Sources/App/Middleware/DependenciesMiddleware.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// import ApiControllerLive
|
||||||
|
import DatabaseClient
|
||||||
|
import Dependencies
|
||||||
|
import Vapor
|
||||||
|
import ViewController
|
||||||
|
|
||||||
|
// Taken from discussions page on `swift-dependencies`.
|
||||||
|
|
||||||
|
// FIX: Use live view controller.
|
||||||
|
struct DependenciesMiddleware: AsyncMiddleware {
|
||||||
|
|
||||||
|
private let values: DependencyValues.Continuation
|
||||||
|
// private let apiController: ApiController
|
||||||
|
private let database: DatabaseClient
|
||||||
|
private let viewController: ViewController
|
||||||
|
|
||||||
|
init(
|
||||||
|
database: DatabaseClient,
|
||||||
|
// apiController: ApiController = .liveValue,
|
||||||
|
viewController: ViewController = .testValue
|
||||||
|
) {
|
||||||
|
self.values = withEscapedDependencies { $0 }
|
||||||
|
// self.apiController = apiController
|
||||||
|
self.database = database
|
||||||
|
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.viewController = viewController
|
||||||
|
} operation: {
|
||||||
|
try await next.respond(to: request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
100
Sources/App/Middleware/URLRoutingMiddleware.swift
Normal file
100
Sources/App/Middleware/URLRoutingMiddleware.swift
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import URLRouting
|
||||||
|
import Vapor
|
||||||
|
import VaporRouting
|
||||||
|
|
||||||
|
// Taken from github.com/nevillco/vapor-routing
|
||||||
|
|
||||||
|
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.
|
||||||
|
public 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
|
||||||
|
// }
|
||||||
|
// )
|
||||||
132
Sources/App/configure.swift
Normal file
132
Sources/App/configure.swift
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import DatabaseClient
|
||||||
|
import Dependencies
|
||||||
|
import Elementary
|
||||||
|
import Fluent
|
||||||
|
import FluentSQLiteDriver
|
||||||
|
import ManualDCore
|
||||||
|
import NIOSSL
|
||||||
|
import Vapor
|
||||||
|
import VaporElementary
|
||||||
|
@preconcurrency import VaporRouting
|
||||||
|
import ViewController
|
||||||
|
|
||||||
|
// configures your application
|
||||||
|
public func configure(
|
||||||
|
_ app: Application,
|
||||||
|
makeDatabaseClient: @escaping (any Database) -> DatabaseClient = { .live(database: $0) }
|
||||||
|
) async throws {
|
||||||
|
// Setup the database client.
|
||||||
|
let databaseClient = try await setupDatabase(on: app, factory: makeDatabaseClient)
|
||||||
|
// Add the global middlewares.
|
||||||
|
addMiddleware(to: app, database: databaseClient)
|
||||||
|
#if DEBUG
|
||||||
|
// Live reload of the application for development when launched with the `./swift-dev` command
|
||||||
|
app.lifecycle.use(BrowserSyncHandler())
|
||||||
|
#endif
|
||||||
|
// Add our route handlers.
|
||||||
|
addRoutes(to: app)
|
||||||
|
if app.environment != .testing {
|
||||||
|
try await app.autoMigrate()
|
||||||
|
}
|
||||||
|
// Add our custom cli-commands to the application.
|
||||||
|
addCommands(to: app)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addMiddleware(to app: Application, database databaseClient: DatabaseClient) {
|
||||||
|
// 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(database: databaseClient))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupDatabase(
|
||||||
|
on app: Application,
|
||||||
|
factory makeDatabaseClient: @escaping (any Database) -> DatabaseClient
|
||||||
|
) async throws -> DatabaseClient {
|
||||||
|
switch app.environment {
|
||||||
|
case .production, .development:
|
||||||
|
let dbFileName = Environment.get("SQLITE_FILENAME") ?? "db.sqlite"
|
||||||
|
app.databases.use(DatabaseConfigurationFactory.sqlite(.file(dbFileName)), as: .sqlite)
|
||||||
|
default:
|
||||||
|
app.databases.use(DatabaseConfigurationFactory.sqlite(.memory), as: .sqlite)
|
||||||
|
}
|
||||||
|
|
||||||
|
let databaseClient = makeDatabaseClient(app.db)
|
||||||
|
|
||||||
|
if app.environment != .testing {
|
||||||
|
try await app.migrations.add(databaseClient.migrations.run())
|
||||||
|
}
|
||||||
|
|
||||||
|
return databaseClient
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addRoutes(to app: Application) {
|
||||||
|
// Redirect the index path to purchase order route.
|
||||||
|
// app.get { req in
|
||||||
|
// req.redirect(to: SiteRoute.View.router.path(for: .purchaseOrder(.index)))
|
||||||
|
// }
|
||||||
|
|
||||||
|
app.mount(
|
||||||
|
SiteRoute.router,
|
||||||
|
middleware: {
|
||||||
|
if app.environment == .testing {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return $0.middleware()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
use: siteHandler
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addCommands(to app: Application) {
|
||||||
|
// #if DEBUG
|
||||||
|
// app.asyncCommands.use(SeedCommand(), as: "seed")
|
||||||
|
// #endif
|
||||||
|
// app.asyncCommands.use(GenerateAdminUserCommand(), as: "generate-admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SiteRoute {
|
||||||
|
|
||||||
|
fileprivate func middleware() -> [any Middleware]? {
|
||||||
|
return nil
|
||||||
|
// switch self {
|
||||||
|
// case .api(let route):
|
||||||
|
// return route.middleware
|
||||||
|
// // case .health:
|
||||||
|
// // return nil
|
||||||
|
// case .view(let route):
|
||||||
|
// return route.middleware
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Sendable
|
||||||
|
private func siteHandler(
|
||||||
|
request: Request,
|
||||||
|
route: SiteRoute
|
||||||
|
) async throws -> any AsyncResponseEncodable {
|
||||||
|
// @Dependency(\.apiController) var apiController
|
||||||
|
@Dependency(\.viewController) var viewController
|
||||||
|
|
||||||
|
switch route {
|
||||||
|
case .api(let route):
|
||||||
|
return HTTPStatus.ok
|
||||||
|
// return try await apiController.respond(route, request: request)
|
||||||
|
case .health:
|
||||||
|
return HTTPStatus.ok
|
||||||
|
case .view(let route):
|
||||||
|
return try await viewController.respond(route: route, request: request)
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Sources/App/entrypoint.swift
Normal file
34
Sources/App/entrypoint.swift
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import DatabaseClient
|
||||||
|
import Dependencies
|
||||||
|
import Logging
|
||||||
|
import NIOCore
|
||||||
|
import NIOPosix
|
||||||
|
import Vapor
|
||||||
|
|
||||||
|
@main
|
||||||
|
enum Entrypoint {
|
||||||
|
static func main() async throws {
|
||||||
|
var env = try Environment.detect()
|
||||||
|
try LoggingSystem.bootstrap(from: &env)
|
||||||
|
|
||||||
|
let app = try await Application.make(env)
|
||||||
|
|
||||||
|
// This attempts to install NIO as the Swift Concurrency global executor.
|
||||||
|
// You can enable it if you'd like to reduce the amount of context switching between NIO and Swift Concurrency.
|
||||||
|
// Note: this has caused issues with some libraries that use `.wait()` and cleanly shutting down.
|
||||||
|
// If enabled, you should be careful about calling async functions before this point as it can cause assertion failures.
|
||||||
|
// let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
|
||||||
|
// app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)])
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await configure(app)
|
||||||
|
} catch {
|
||||||
|
app.logger.report(error: error)
|
||||||
|
try? await app.asyncShutdown()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
try await app.execute()
|
||||||
|
try await app.asyncShutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,19 @@ import Foundation
|
|||||||
public enum SiteRoute: Equatable, Sendable {
|
public enum SiteRoute: Equatable, Sendable {
|
||||||
|
|
||||||
case api(Self.Api)
|
case api(Self.Api)
|
||||||
|
case health
|
||||||
|
case view(Self.View)
|
||||||
|
|
||||||
public static let router = OneOf {
|
public static let router = OneOf {
|
||||||
Route(.case(Self.api)) {
|
Route(.case(Self.api)) {
|
||||||
SiteRoute.Api.router
|
SiteRoute.Api.router
|
||||||
}
|
}
|
||||||
|
Route(.case(Self.health)) {
|
||||||
|
Path { "health" }
|
||||||
|
Method.get
|
||||||
|
}
|
||||||
|
Route(.case(Self.view)) {
|
||||||
|
SiteRoute.View.router
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,51 @@ extension SiteRoute {
|
|||||||
/// Represents view routes.
|
/// Represents view routes.
|
||||||
///
|
///
|
||||||
/// The routes return html.
|
/// The routes return html.
|
||||||
public enum View {
|
public enum View: Equatable, Sendable {
|
||||||
|
case project(ProjectRoute)
|
||||||
|
|
||||||
|
static let router = OneOf {
|
||||||
|
Route(.case(Self.project)) {
|
||||||
|
SiteRoute.View.ProjectRoute.router
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SiteRoute.View {
|
||||||
|
public enum ProjectRoute: Equatable, Sendable {
|
||||||
|
case create(Project.Create)
|
||||||
|
case form
|
||||||
|
case index
|
||||||
|
|
||||||
|
static let rootPath = "projects"
|
||||||
|
|
||||||
|
public static let router = OneOf {
|
||||||
|
Route(.case(Self.create)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.post
|
||||||
|
Body {
|
||||||
|
FormData {
|
||||||
|
Field("name", .string)
|
||||||
|
Field("streetAddress", .string)
|
||||||
|
Field("city", .string)
|
||||||
|
Field("state", .string)
|
||||||
|
Field("zipCode", .string)
|
||||||
|
}
|
||||||
|
.map(.memberwise(Project.Create.init))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Route(.case(Self.form)) {
|
||||||
|
Path {
|
||||||
|
rootPath
|
||||||
|
"create"
|
||||||
|
}
|
||||||
|
Method.get
|
||||||
|
}
|
||||||
|
Route(.case(Self.index)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.get
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
Sources/ViewController/Interface.swift
Normal file
43
Sources/ViewController/Interface.swift
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import Elementary
|
||||||
|
import Logging
|
||||||
|
import ManualDCore
|
||||||
|
|
||||||
|
extension DependencyValues {
|
||||||
|
public var viewController: ViewController {
|
||||||
|
get { self[ViewController.self] }
|
||||||
|
set { self[ViewController.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public typealias AnySendableHTML = (any HTML & Sendable)
|
||||||
|
|
||||||
|
@DependencyClient
|
||||||
|
public struct ViewController: Sendable {
|
||||||
|
public var view: @Sendable (Request) async throws -> AnySendableHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ViewController {
|
||||||
|
|
||||||
|
public struct Request: Sendable {
|
||||||
|
|
||||||
|
public let route: SiteRoute.View
|
||||||
|
public let isHtmxRequest: Bool
|
||||||
|
public let logger: Logger
|
||||||
|
|
||||||
|
public init(
|
||||||
|
route: SiteRoute.View,
|
||||||
|
isHtmxRequest: Bool,
|
||||||
|
logger: Logger
|
||||||
|
) {
|
||||||
|
self.route = route
|
||||||
|
self.isHtmxRequest = isHtmxRequest
|
||||||
|
self.logger = logger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ViewController: TestDependencyKey {
|
||||||
|
public static let testValue = Self()
|
||||||
|
}
|
||||||
25
Sources/ViewController/Views/MainPage.swift
Normal file
25
Sources/ViewController/Views/MainPage.swift
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Elementary
|
||||||
|
|
||||||
|
public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable {
|
||||||
|
public var title: String { "Manual-D" }
|
||||||
|
public var lang: String { "en" }
|
||||||
|
let inner: Inner
|
||||||
|
|
||||||
|
init(_ inner: () -> Inner) {
|
||||||
|
self.inner = inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var head: some HTML {
|
||||||
|
meta(.charset(.utf8))
|
||||||
|
script(.src("https://unpkg.com/htmx.org@2.0.8")) {}
|
||||||
|
script(.src("/js/main.js")) {}
|
||||||
|
link(.rel(.stylesheet), .href("/css/main.css"))
|
||||||
|
link(.rel(.icon), .href("/images/favicon.ico"), .custom(name: "type", value: "image/x-icon"))
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some HTML {
|
||||||
|
inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol SendableHTMLDocument: HTMLDocument, Sendable {}
|
||||||
Reference in New Issue
Block a user