feat: Begins vapor application, begins view controller.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "f8ca659e4ec9041ea590b94c8dc267718be8941a4594eb74964b6396e987ca95",
|
||||
"originHash" : "5d6dad57209ac74e3c47d8e8eb162768b81c9e63e15df87d29019d46a13cfec2",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "async-http-client",
|
||||
@@ -37,6 +37,24 @@
|
||||
"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",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -379,6 +397,24 @@
|
||||
"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",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -5,23 +5,41 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "swift-manual-d",
|
||||
products: [
|
||||
.executable(name: "App", targets: ["App"]),
|
||||
.library(name: "DatabaseClient", targets: ["DatabaseClient"]),
|
||||
.library(name: "ManualDCore", targets: ["ManualDCore"]),
|
||||
.library(name: "ManualDClient", targets: ["ManualDClient"]),
|
||||
.library(name: "ViewController", targets: ["ViewController"]),
|
||||
],
|
||||
dependencies: [
|
||||
// 💧 A server-side Swift web framework.
|
||||
.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"),
|
||||
// 🪶 Fluent driver for SQLite.
|
||||
.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/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/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/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: [
|
||||
.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(
|
||||
name: "DatabaseClient",
|
||||
dependencies: [
|
||||
@@ -40,6 +58,12 @@ let package = Package(
|
||||
.product(name: "CasePaths", package: "swift-case-paths"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "ApiRouteTests",
|
||||
dependencies: [
|
||||
.target(name: "ManualDCore")
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "ManualDClient",
|
||||
dependencies: [
|
||||
@@ -55,10 +79,15 @@ let package = Package(
|
||||
.product(name: "DependenciesTestSupport", package: "swift-dependencies"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "ApiRouteTests",
|
||||
.target(
|
||||
name: "ViewController",
|
||||
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 {
|
||||
|
||||
case api(Self.Api)
|
||||
case health
|
||||
case view(Self.View)
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.api)) {
|
||||
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.
|
||||
///
|
||||
/// 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