diff --git a/Dockerfile b/Dockerfile index cceff7f..da7b88e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,8 +27,8 @@ COPY . . # N.B.: The static version of jemalloc is incompatible with the static Swift runtime. RUN swift build -c release \ --product App \ - --static-swift-stdlib \ - -Xlinker -ljemalloc + --static-swift-stdlib \ + -Xlinker -ljemalloc # Switch to the staging area WORKDIR /staging diff --git a/Package.swift b/Package.swift index aa277e4..7ed7b87 100644 --- a/Package.swift +++ b/Package.swift @@ -164,5 +164,5 @@ var swiftSettings: [SwiftSetting] { [ .enableExperimentalFeature("StrictConcurrency=complete"), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("ConciseMagicFile"), - .enableUpcomingFeature("ForwardTrailinClosures") + .enableUpcomingFeature("ForwardTrailingClosures") ] } diff --git a/Sources/App/Middleware/ViewRoute+middleware.swift b/Sources/App/Middleware/ViewRoute+middleware.swift index 968ffe1..13e168a 100644 --- a/Sources/App/Middleware/ViewRoute+middleware.swift +++ b/Sources/App/Middleware/ViewRoute+middleware.swift @@ -16,36 +16,13 @@ extension SharedModels.ViewRoute { var middleware: [any Middleware]? { switch self { - // case .index: return viewProtectedMiddleware - case let .employee(route): return route.middleware + case .employee, + .purchaseOrder, + .user, + .vendor, + .vendorBranch: + return viewProtectedMiddleware case .login: return nil - case let .purchaseOrder(route): return route.middleware - case let .user(route): return route.middleware - case let .vendor(route): return route.middleware - case let .vendorBranch(route): return route.middleware } } } - -extension SharedModels.ViewRoute.EmployeeRoute { - - var middleware: [any Middleware]? { viewProtectedMiddleware } -} - -extension SharedModels.ViewRoute.PurchaseOrderRoute { - var middleware: [any Middleware]? { viewProtectedMiddleware } -} - -extension SharedModels.ViewRoute.UserRoute { - var middleware: [any Middleware]? { - viewProtectedMiddleware - } -} - -extension SharedModels.ViewRoute.VendorRoute { - var middleware: [any Middleware]? { viewProtectedMiddleware } -} - -extension SharedModels.ViewRoute.VendorBranchRoute { - var middleware: [any Middleware]? { viewProtectedMiddleware } -} diff --git a/Sources/DatabaseClientLive/Employees.swift b/Sources/DatabaseClientLive/Employees+live.swift similarity index 94% rename from Sources/DatabaseClientLive/Employees.swift rename to Sources/DatabaseClientLive/Employees+live.swift index 3112d04..7420ea8 100644 --- a/Sources/DatabaseClientLive/Employees.swift +++ b/Sources/DatabaseClientLive/Employees+live.swift @@ -6,8 +6,8 @@ import Vapor public extension DatabaseClient.Employees { - static func live(database: any Database) -> DatabaseClient.Employees { - .init { create in + static func live(database: any Database) -> Self { + Self { create in let model = try create.toModel() try await model.save(on: database) return try model.toDTO() @@ -18,13 +18,13 @@ public extension DatabaseClient.Employees { try await model.delete(on: database) } fetchAll: { request in var query = EmployeeModel.query(on: database) - .sort(\.$lastName) + .sort(\EmployeeModel.$lastName) switch request { case .active: - query = query.filter(\.$active == true) + query = query.filter(\EmployeeModel.$active == true) case .inactive: - query = query.filter(\.$active == false) + query = query.filter(\EmployeeModel.$active == false) case .all: break } diff --git a/Sources/DatabaseClientLive/PurchaseOrders.swift b/Sources/DatabaseClientLive/PurchaseOrders+live.swift similarity index 89% rename from Sources/DatabaseClientLive/PurchaseOrders.swift rename to Sources/DatabaseClientLive/PurchaseOrders+live.swift index 32495f1..506b19a 100644 --- a/Sources/DatabaseClientLive/PurchaseOrders.swift +++ b/Sources/DatabaseClientLive/PurchaseOrders+live.swift @@ -9,7 +9,8 @@ public extension DatabaseClient.PurchaseOrders { .init { create in let model = try create.toModel() try await model.save(on: database) - let fetched = try await PurchaseOrderModel.allQuery(on: database).filter(\.$id == model.id!).first()! + let fetched = try await PurchaseOrderModel.allQuery(on: database) + .filter(\PurchaseOrderModel.$id == model.id!).first()! return try fetched.toDTO() } fetchAll: { try await PurchaseOrderModel.allQuery(on: database) @@ -21,7 +22,7 @@ public extension DatabaseClient.PurchaseOrders { .map { try $0.toDTO() } } get: { id in try await PurchaseOrderModel.allQuery(on: database) - .filter(\.$id == id) + .filter(\PurchaseOrderModel.$id == id) .first() .map { try $0.toDTO() } } delete: { id in @@ -34,17 +35,17 @@ public extension DatabaseClient.PurchaseOrders { switch search { case let .employee(id): - return try await query.filter(\.$createdFor.$id == id) + return try await query.filter(\PurchaseOrderModel.$createdFor.$id == id) .paginate(page) .map { try $0.toDTO() } case let .customer(search): - return try await query.filter(\.$customer ~~ search) + return try await query.filter(\PurchaseOrderModel.$customer ~~ search) .paginate(page) .map { try $0.toDTO() } case let .vendor(id): - return try await query.filter(\.$vendorBranch.$id == id) + return try await query.filter(\PurchaseOrderModel.$vendorBranch.$id == id) .paginate(page) .map { try $0.toDTO() } } @@ -190,11 +191,11 @@ final class PurchaseOrderModel: Model, Codable, @unchecked Sendable { static func allQuery(on db: any Database) -> QueryBuilder { PurchaseOrderModel.query(on: db) - .sort(\.$id, .descending) - .with(\.$createdBy) - .with(\.$createdFor) - .with(\.$vendorBranch) { branch in - branch.with(\.$vendor) + .sort(\PurchaseOrderModel.$id, .descending) + .with(\PurchaseOrderModel.$createdBy) + .with(\PurchaseOrderModel.$createdFor) + .with(\PurchaseOrderModel.$vendorBranch) { branch in + branch.with(\VendorBranchModel.$vendor) } } } diff --git a/Sources/DatabaseClientLive/Users.swift b/Sources/DatabaseClientLive/Users+live.swift similarity index 95% rename from Sources/DatabaseClientLive/Users.swift rename to Sources/DatabaseClientLive/Users+live.swift index 73f1265..f654513 100644 --- a/Sources/DatabaseClientLive/Users.swift +++ b/Sources/DatabaseClientLive/Users+live.swift @@ -28,9 +28,9 @@ public extension DatabaseClient.Users { var query = UserModel.query(on: database) if let username = login.username { - query = query.filter(\.$username == username) + query = query.filter(\UserModel.$username == username) } else { - query = query.filter(\.$email == login.email!) + query = query.filter(\UserModel.$email == login.email!) } guard let user = try await query.first() else { @@ -265,7 +265,7 @@ public struct UserPasswordAuthenticator: AsyncBasicAuthenticator { public func authenticate(basic: BasicAuthorization, for request: Request) async throws { guard let user = try await UserModel.query(on: request.db) - .filter(\.$username == basic.username) + .filter(\UserModel.$username == basic.username) .first(), try Bcrypt.verify(basic.password, created: user.passwordHash) else { @@ -282,8 +282,8 @@ public struct UserTokenAuthenticator: AsyncBearerAuthenticator { public func authenticate(bearer: BearerAuthorization, for request: Request) async throws { guard let token = try await UserTokenModel.query(on: request.db) - .filter(\.$value == bearer.token) - .with(\.$user) + .filter(\UserTokenModel.$value == bearer.token) + .with(\UserTokenModel.$user) .first() else { throw Abort(.unauthorized) @@ -299,7 +299,7 @@ public struct UserSessionAuthenticator: AsyncSessionAuthenticator { public func authenticate(sessionID: User.SessionID, for request: Request) async throws { guard let user = try await UserModel.query(on: request.db) - .filter(\.$username == sessionID) + .filter(\UserModel.$username == sessionID) .first() else { throw Abort(.unauthorized) diff --git a/Sources/DatabaseClientLive/VendorBranches.swift b/Sources/DatabaseClientLive/VendorBranches+live.swift similarity index 96% rename from Sources/DatabaseClientLive/VendorBranches.swift rename to Sources/DatabaseClientLive/VendorBranches+live.swift index 81968e6..08a5c85 100644 --- a/Sources/DatabaseClientLive/VendorBranches.swift +++ b/Sources/DatabaseClientLive/VendorBranches+live.swift @@ -23,8 +23,8 @@ public extension DatabaseClient.VendorBranches { break case let .for(vendorID: vendorID): let branches = try await VendorModel.query(on: db) - .filter(\.$id == vendorID) - .with(\.$branches) + .filter(\VendorModel.$id == vendorID) + .with(\VendorModel.$branches) .first()? .branches .map { try $0.toDTO() } @@ -36,7 +36,7 @@ public extension DatabaseClient.VendorBranches { return try await query.all().map { try $0.toDTO() } } fetchAllWithDetail: { try await VendorBranchModel.query(on: db) - .with(\.$vendor) + .with(\VendorBranchModel.$vendor) .all() .map { try $0.toDetail() } } get: { id in diff --git a/Sources/DatabaseClientLive/Vendors.swift b/Sources/DatabaseClientLive/Vendors+live.swift similarity index 92% rename from Sources/DatabaseClientLive/Vendors.swift rename to Sources/DatabaseClientLive/Vendors+live.swift index 098a0ae..7e58677 100644 --- a/Sources/DatabaseClientLive/Vendors.swift +++ b/Sources/DatabaseClientLive/Vendors+live.swift @@ -17,13 +17,13 @@ public extension DatabaseClient.Vendors { } try await model.delete(on: db) } fetchAll: { request in - var query = VendorModel.query(on: db).sort(\.$name, .ascending) + var query = VendorModel.query(on: db).sort(\VendorModel.$name, .ascending) let withBranches = request == .withBranches switch request { case .withBranches: - query = query.with(\.$branches) + query = query.with(\VendorModel.$branches) case .all: break } @@ -34,7 +34,7 @@ public extension DatabaseClient.Vendors { var query = VendorModel.query(on: db).filter(\.$id == id) let withBranches = request == .withBranches if withBranches { - query = query.with(\.$branches) + query = query.with(\VendorModel.$branches) } return try await query.first().map { try $0.toDTO(includeBranches: withBranches) } } update: { id, updates, withBranches in @@ -47,8 +47,8 @@ public extension DatabaseClient.Vendors { return try model.toDTO() } return try await VendorModel.query(on: db) - .filter(\.$id == id) - .with(\.$branches) + .filter(\VendorModel.$id == id) + .with(\VendorModel.$branches) .first()! .toDTO() } diff --git a/Sources/ViewControllerLive/Routes+view.swift b/Sources/ViewControllerLive/Routes+view.swift index 080d46a..a2a12e0 100644 --- a/Sources/ViewControllerLive/Routes+view.swift +++ b/Sources/ViewControllerLive/Routes+view.swift @@ -8,6 +8,7 @@ import ViewController public extension SharedModels.ViewRoute { + @Sendable func view( isHtmxRequest: Bool, logger: Logger, @@ -55,9 +56,10 @@ extension SharedModels.ViewRoute.EmployeeRoute { private func mainPage( _ html: C - ) async throws -> some SendableHTMLDocument where C: Sendable { + ) async throws -> AnySendableHTML where C: Sendable { @Dependency(\.database) var database let employees = try await database.employees.fetchAll() + // return EmployeeMainPage(employees: employees, html: html) return MainPage(displayNav: true, route: .employees) { div(.class("container")) { html @@ -66,6 +68,7 @@ extension SharedModels.ViewRoute.EmployeeRoute { } } + @Sendable func view(isHtmxRequest: Bool) async throws -> AnySendableHTML { @Dependency(\.database.employees) var employees @@ -98,7 +101,7 @@ extension SharedModels.ViewRoute.EmployeeRoute { extension SharedModels.ViewRoute.PurchaseOrderRoute { private func mainPage( _ html: C - ) async throws -> some SendableHTMLDocument where C: Sendable { + ) async throws -> AnySendableHTML where C: Sendable { @Dependency(\.database.purchaseOrders) var purchaseOrders let page = try await purchaseOrders.fetchPage(.init(page: 1, per: 25)) return MainPage(displayNav: true, route: .purchaseOrders) { @@ -109,6 +112,7 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute { } } + @Sendable func view(isHtmxRequest: Bool) async throws -> AnySendableHTML { @Dependency(\.database.purchaseOrders) var purchaseOrders @@ -146,7 +150,7 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute { extension SharedModels.ViewRoute.PurchaseOrderRoute.Search { - func mainPage(search: PurchaseOrderSearch = .init()) -> some SendableHTMLDocument { + func mainPage(search: PurchaseOrderSearch = .init()) -> AnySendableHTML { MainPage(displayNav: true, route: .purchaseOrders) { div(.class("container"), .id("purchase-order-content")) { search @@ -158,6 +162,7 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute.Search { } } + @Sendable func view(isHtmxRequest: Bool) async throws -> AnySendableHTML { @Dependency(\.database) var database switch self { @@ -177,7 +182,7 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute.Search { extension SharedModels.ViewRoute.UserRoute { - private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { + private func mainPage(_ html: C) async throws -> AnySendableHTML where C: Sendable { @Dependency(\.database) var database let users = try await database.users.fetchAll() return MainPage(displayNav: true, route: .users) { @@ -188,6 +193,7 @@ extension SharedModels.ViewRoute.UserRoute { } } + @Sendable func view(isHtmxRequest: Bool) async throws -> AnySendableHTML { @Dependency(\.database.users) var users @@ -215,7 +221,7 @@ extension SharedModels.ViewRoute.UserRoute { } extension SharedModels.ViewRoute.VendorRoute { - private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { + private func mainPage(_ html: C) async throws -> AnySendableHTML where C: Sendable { @Dependency(\.database) var database let vendors = try await database.vendors.fetchAll(.withBranches) return MainPage(displayNav: true, route: .vendors) { @@ -226,6 +232,7 @@ extension SharedModels.ViewRoute.VendorRoute { } } + @Sendable func view(isHtmxRequest: Bool) async throws -> AnySendableHTML { @Dependency(\.database) var database @@ -262,6 +269,7 @@ extension SharedModels.ViewRoute.VendorRoute { extension SharedModels.ViewRoute.VendorBranchRoute { + @Sendable func view(isHtmxRequest: Bool) async throws -> AnySendableHTML { @Dependency(\.database) var database @@ -286,6 +294,7 @@ extension SharedModels.ViewRoute.VendorBranchRoute { extension SharedModels.ViewRoute.PurchaseOrderRoute.Search.Request { + @Sendable func toDatabaseQuery() throws -> PurchaseOrder.SearchContext { switch context { case .employee: @@ -308,6 +317,8 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute.Search.Request { } extension SharedModels.ViewRoute.SelectContext { + + @Sendable func toHTML(employees: [Employee]) -> EmployeeSelect { switch self { case .purchaseOrderForm: @@ -327,8 +338,9 @@ extension SharedModels.ViewRoute.SelectContext { } } +@Sendable private func render( - _ mainPage: (C) async throws -> any SendableHTMLDocument, + _ mainPage: (C) async throws -> AnySendableHTML, _ isHtmxRequest: Bool, @HTMLBuilder html: () -> C ) async rethrows -> AnySendableHTML where C: Sendable { @@ -338,8 +350,9 @@ private func render( return html() } +@Sendable private func render( - _ mainPage: (C) async throws -> any SendableHTMLDocument, + _ mainPage: (C) async throws -> AnySendableHTML, _ isHtmxRequest: Bool, _ html: @autoclosure @escaping () -> C ) async rethrows -> AnySendableHTML where C: Sendable { diff --git a/Sources/ViewControllerLive/Views/Employees/EmployeeTable.swift b/Sources/ViewControllerLive/Views/Employees/EmployeeTable.swift index 4e6ae33..d58ae30 100644 --- a/Sources/ViewControllerLive/Views/Employees/EmployeeTable.swift +++ b/Sources/ViewControllerLive/Views/Employees/EmployeeTable.swift @@ -2,7 +2,7 @@ import Elementary import ElementaryHTMX import SharedModels -struct EmployeeTable: HTML { +struct EmployeeTable: HTML, Sendable { let employees: [Employee] var content: some HTML { @@ -29,7 +29,7 @@ struct EmployeeTable: HTML { } } - struct Row: HTML { + struct Row: HTML, Sendable { let employee: Employee var content: some HTML { diff --git a/Sources/ViewControllerLive/Views/HTMXExtensions.swift b/Sources/ViewControllerLive/Views/HTMXExtensions.swift index 0069294..98209b4 100644 --- a/Sources/ViewControllerLive/Views/HTMXExtensions.swift +++ b/Sources/ViewControllerLive/Views/HTMXExtensions.swift @@ -3,24 +3,29 @@ import ElementaryHTMX import SharedModels extension HTMLAttribute.hx { + @Sendable static func get(route: SharedModels.ViewRoute) -> HTMLAttribute { get(SharedModels.ViewRoute.router.path(for: route)) } + @Sendable static func post(route: SharedModels.ViewRoute) -> HTMLAttribute { post(SharedModels.ViewRoute.router.path(for: route)) } + @Sendable static func put(route: SharedModels.ViewRoute) -> HTMLAttribute { put(SharedModels.ViewRoute.router.path(for: route)) } + @Sendable static func delete(route: SharedModels.ApiRoute) -> HTMLAttribute { delete(SharedModels.ApiRoute.router.path(for: route)) } } extension HTMLAttribute.hx { + @Sendable static func target(_ target: HXTarget) -> HTMLAttribute { Self.target(target.selector) } @@ -28,37 +33,43 @@ extension HTMLAttribute.hx { extension HTMLAttribute.hx where Tag: HTMLTrait.Attributes.Global { + @Sendable static func on(_ event: HXOnEvent, value: String) -> HTMLAttribute { HTMLAttribute.custom(name: "hx-on::\(event.rawValue)", value: value) } + @Sendable static func on(_ event: HXOnEvent, _ value: HXOnValue) -> HTMLAttribute { on(event, value: value.value) } + @Sendable static func on(_ event: HXOnEvent, _ values: HXOnValue...) -> HTMLAttribute { on(event, value: values.value) } } -enum HXOnEvent: String { +enum HXOnEvent: String, Sendable { case afterRequest = "after-request" } -indirect enum HXOnValue { +indirect enum HXOnValue: Sendable { case ifSuccessful([Self]) case resetForm case setWindowLocation(String) case toggleContent(id: String) + @Sendable static func toggleContent(_ toggle: Toggle) -> Self { toggleContent(id: toggle.rawValue) } + @Sendable static func setWindowLocation(to route: ViewRoute) -> Self { setWindowLocation(ViewRoute.router.path(for: route)) } + @Sendable static func ifSuccessful(_ values: Self...) -> Self { .ifSuccessful(values) } @@ -76,7 +87,7 @@ indirect enum HXOnValue { } } - enum Toggle: String { + enum Toggle: String, Sendable { case float } } @@ -89,12 +100,14 @@ extension Array where Element == HXOnValue { } extension HTMLAttribute where Tag: HTMLTrait.Attributes.Global { + + @Sendable static func id(_ key: IDKey) -> Self { id(key.description) } } -enum IDKey: CustomStringConvertible { +enum IDKey: CustomStringConvertible, Sendable { case branch(Branch) case employee(Employee) case float @@ -115,7 +128,7 @@ enum IDKey: CustomStringConvertible { } } - enum Branch: CustomStringConvertible { + enum Branch: CustomStringConvertible, Sendable { case list case form case row(id: VendorBranch.ID) @@ -129,7 +142,7 @@ enum IDKey: CustomStringConvertible { } } - enum Employee: CustomStringConvertible { + enum Employee: CustomStringConvertible, Sendable { case table case row(id: SharedModels.Employee.ID) @@ -141,7 +154,7 @@ enum IDKey: CustomStringConvertible { } } - enum PurchaseOrder: CustomStringConvertible { + enum PurchaseOrder: CustomStringConvertible, Sendable { case content case row(id: SharedModels.PurchaseOrder.ID) case search @@ -157,7 +170,7 @@ enum IDKey: CustomStringConvertible { } } - enum User: CustomStringConvertible { + enum User: CustomStringConvertible, Sendable { case form case row(id: SharedModels.User.ID) case table @@ -171,7 +184,7 @@ enum IDKey: CustomStringConvertible { } } - enum Vendor: CustomStringConvertible { + enum Vendor: CustomStringConvertible, Sendable { case form case row(id: SharedModels.Vendor.ID) @@ -185,7 +198,7 @@ enum IDKey: CustomStringConvertible { } } -enum HXTarget: CustomStringConvertible { +enum HXTarget: CustomStringConvertible, Sendable { case body case id(IDKey) case this diff --git a/Sources/ViewControllerLive/Views/MainPage.swift b/Sources/ViewControllerLive/Views/MainPage.swift index 93fe7bd..779c75a 100644 --- a/Sources/ViewControllerLive/Views/MainPage.swift +++ b/Sources/ViewControllerLive/Views/MainPage.swift @@ -49,7 +49,7 @@ extension MainPage where Inner == LoggedIn { } } -struct LoggedIn: HTML { +struct LoggedIn: HTML, Sendable { let next: String? var content: some HTML { div( @@ -72,7 +72,7 @@ struct LoggedIn: HTML { } } -struct RouteHeaderView: HTML { +struct RouteHeaderView: HTML, Sendable { let title: String let description: String @@ -95,7 +95,7 @@ struct RouteHeaderView: HTML { } } - enum ViewRoute: String { + enum ViewRoute: String, Sendable { case employees case login diff --git a/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderForm.swift b/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderForm.swift index 83b8ec5..20aee58 100644 --- a/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderForm.swift +++ b/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderForm.swift @@ -3,7 +3,7 @@ import Elementary import ElementaryHTMX import SharedModels -struct PurchaseOrderForm: HTML { +struct PurchaseOrderForm: HTML, Sendable { @Dependency(\.dateFormatter) var dateFormatter diff --git a/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderSearch.swift b/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderSearch.swift index 78c6f4d..559b1c5 100644 --- a/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderSearch.swift +++ b/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderSearch.swift @@ -3,7 +3,7 @@ import ElementaryHTMX import SharedModels import Vapor -struct PurchaseOrderSearch: HTML { +struct PurchaseOrderSearch: HTML, Sendable { typealias Context = SharedModels.ViewRoute.PurchaseOrderRoute.Search.Context diff --git a/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderTable.swift b/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderTable.swift index f143b21..b8311d9 100644 --- a/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderTable.swift +++ b/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderTable.swift @@ -4,7 +4,7 @@ import Fluent import SharedModels import Vapor -struct PurchaseOrderTable: HTML { +struct PurchaseOrderTable: HTML, Sendable { typealias SearchContext = SharedModels.ViewRoute.PurchaseOrderRoute.Search.Context let page: Page @@ -72,7 +72,7 @@ struct PurchaseOrderTable: HTML { } // Produces only the rows for the given page - struct Rows: HTML { + struct Rows: HTML, Sendable { let page: Page var content: some HTML { @@ -98,7 +98,7 @@ struct PurchaseOrderTable: HTML { } // A single row. - struct Row: HTML { + struct Row: HTML, Sendable { let purchaseOrder: PurchaseOrder var content: some HTML { @@ -124,7 +124,7 @@ struct PurchaseOrderTable: HTML { } } - enum Context: String { + enum Context: String, Sendable { case `default` case search } diff --git a/Sources/ViewControllerLive/Views/Users/UserForm.swift b/Sources/ViewControllerLive/Views/Users/UserForm.swift index 4ce7c95..c514299 100644 --- a/Sources/ViewControllerLive/Views/Users/UserForm.swift +++ b/Sources/ViewControllerLive/Views/Users/UserForm.swift @@ -58,7 +58,7 @@ struct UserForm: HTML, Sendable { } } - enum Context: Equatable { + enum Context: Equatable, Sendable { case create case login(next: String?) diff --git a/Sources/ViewControllerLive/Views/Users/UserTable.swift b/Sources/ViewControllerLive/Views/Users/UserTable.swift index 54bb936..963e192 100644 --- a/Sources/ViewControllerLive/Views/Users/UserTable.swift +++ b/Sources/ViewControllerLive/Views/Users/UserTable.swift @@ -4,7 +4,7 @@ import Elementary import ElementaryHTMX import SharedModels -struct UserTable: HTML { +struct UserTable: HTML, Sendable { let users: [User] @@ -32,7 +32,7 @@ struct UserTable: HTML { } } - struct Row: HTML { + struct Row: HTML, Sendable { let user: User init(user: User) { diff --git a/Sources/ViewControllerLive/Views/Utils/Buttons.swift b/Sources/ViewControllerLive/Views/Utils/Buttons.swift index 98333f8..c63fe82 100644 --- a/Sources/ViewControllerLive/Views/Utils/Buttons.swift +++ b/Sources/ViewControllerLive/Views/Utils/Buttons.swift @@ -2,7 +2,8 @@ import Elementary import SharedModels import URLRouting -struct ToggleFormButton: HTML { +// TODO: Remove. +struct ToggleFormButton: HTML, Sendable { var content: some HTML { a(.href("javascript:void(0)"), .on(.click, "toggleContent('form')"), .class("btn-add")) { "+" @@ -12,20 +13,24 @@ struct ToggleFormButton: HTML { enum Button { + @Sendable static func add() -> some HTML { button(.class("btn btn-add")) { "+" } } + @Sendable static func danger(@HTMLBuilder body: () -> C) -> some HTML { button(.class("danger")) { body() } } + @Sendable static func close(id: String, resetURL: String? = nil) -> some HTML { button(.class("btn-close"), .on(.click, makeOnClick(id, resetURL))) { "x" } } + @Sendable static func close(id: IDKey, resetURL route: ViewRoute? = nil) -> some HTML { close( id: id.description, @@ -33,16 +38,19 @@ enum Button { ) } + @Sendable static func update() -> some HTML { button(.class("btn-update")) { "Update" } } + @Sendable static func detail() -> some HTML { button(.class("btn-detail")) { "〉" } } + @Sendable private static func makeOnClick(_ id: String, _ resetURL: String?) -> String { let output = "toggleContent('\(id)');" if let resetURL { diff --git a/Sources/ViewControllerLive/Views/Utils/Float.swift b/Sources/ViewControllerLive/Views/Utils/Float.swift index cf459d7..28e4d5e 100644 --- a/Sources/ViewControllerLive/Views/Utils/Float.swift +++ b/Sources/ViewControllerLive/Views/Utils/Float.swift @@ -49,7 +49,7 @@ struct Float: HTML { } } -struct DefaultCloseButton: HTML { +struct DefaultCloseButton: HTML, Sendable { let id: String let resetURL: String? diff --git a/Sources/ViewControllerLive/Views/Utils/Select.swift b/Sources/ViewControllerLive/Views/Utils/Select.swift index 0e9bb90..ddf241d 100644 --- a/Sources/ViewControllerLive/Views/Utils/Select.swift +++ b/Sources/ViewControllerLive/Views/Utils/Select.swift @@ -3,7 +3,7 @@ import ElementaryHTMX import SharedModels import Vapor -struct EmployeeSelect: HTML { +struct EmployeeSelect: HTML, Sendable { let employees: [Employee]? let context: ViewRoute.SelectContext @@ -40,7 +40,7 @@ struct EmployeeSelect: HTML { } -struct VendorBranchSelect: HTML { +struct VendorBranchSelect: HTML, Sendable { let branches: [VendorBranch.Detail]? let context: ViewRoute.SelectContext diff --git a/Sources/ViewControllerLive/Views/VendorBranches/VendorBranchForm.swift b/Sources/ViewControllerLive/Views/VendorBranches/VendorBranchForm.swift index 9d8b3fd..663fc68 100644 --- a/Sources/ViewControllerLive/Views/VendorBranches/VendorBranchForm.swift +++ b/Sources/ViewControllerLive/Views/VendorBranches/VendorBranchForm.swift @@ -2,7 +2,7 @@ import Elementary import ElementaryHTMX import SharedModels -struct VendorBranchForm: HTML { +struct VendorBranchForm: HTML, Sendable { let vendorID: Vendor.ID var content: some HTML { diff --git a/Sources/ViewControllerLive/Views/VendorBranches/VendorBranchList.swift b/Sources/ViewControllerLive/Views/VendorBranches/VendorBranchList.swift index bfc8c8e..df3905c 100644 --- a/Sources/ViewControllerLive/Views/VendorBranches/VendorBranchList.swift +++ b/Sources/ViewControllerLive/Views/VendorBranches/VendorBranchList.swift @@ -2,7 +2,7 @@ import Elementary import ElementaryHTMX import SharedModels -struct VendorBranchList: HTML { +struct VendorBranchList: HTML, Sendable { let vendorID: Vendor.ID let branches: [VendorBranch]? @@ -25,7 +25,7 @@ struct VendorBranchList: HTML { } } - struct Row: HTML { + struct Row: HTML, Sendable { let branch: VendorBranch var content: some HTML { diff --git a/Sources/ViewControllerLive/Views/Vendors/VendorDetail.swift b/Sources/ViewControllerLive/Views/Vendors/VendorDetail.swift index e459950..2f8adf1 100644 --- a/Sources/ViewControllerLive/Views/Vendors/VendorDetail.swift +++ b/Sources/ViewControllerLive/Views/Vendors/VendorDetail.swift @@ -2,7 +2,7 @@ import Elementary import ElementaryHTMX import SharedModels -struct VendorDetail: HTML { +struct VendorDetail: HTML, Sendable { let vendor: Vendor diff --git a/Sources/ViewControllerLive/Views/Vendors/VendorForm.swift b/Sources/ViewControllerLive/Views/Vendors/VendorForm.swift index f140200..4394172 100644 --- a/Sources/ViewControllerLive/Views/Vendors/VendorForm.swift +++ b/Sources/ViewControllerLive/Views/Vendors/VendorForm.swift @@ -2,7 +2,7 @@ import Elementary import ElementaryHTMX import SharedModels -struct VendorForm: HTML { +struct VendorForm: HTML, Sendable { let context: Context var vendor: Vendor? { context.vendor } @@ -15,7 +15,7 @@ struct VendorForm: HTML { init() { self.init(.float(nil)) } - enum Context { + enum Context: Sendable { case float(Vendor? = nil, shouldShow: Bool = false) case formOnly(Vendor) diff --git a/Sources/ViewControllerLive/Views/Vendors/VendorTable.swift b/Sources/ViewControllerLive/Views/Vendors/VendorTable.swift index 3858a01..f192d08 100644 --- a/Sources/ViewControllerLive/Views/Vendors/VendorTable.swift +++ b/Sources/ViewControllerLive/Views/Vendors/VendorTable.swift @@ -2,7 +2,7 @@ import Elementary import ElementaryHTMX import SharedModels -struct VendorTable: HTML { +struct VendorTable: HTML, Sendable { let vendors: [Vendor] var content: some HTML { @@ -30,7 +30,7 @@ struct VendorTable: HTML { } } - struct Row: HTML { + struct Row: HTML, Sendable { let vendor: Vendor var content: some HTML { diff --git a/Tests/ViewControllerTests/ViewControllerTests.swift b/Tests/ViewControllerTests/ViewControllerTests.swift index 8b1fbb0..fc33eea 100644 --- a/Tests/ViewControllerTests/ViewControllerTests.swift +++ b/Tests/ViewControllerTests/ViewControllerTests.swift @@ -101,80 +101,86 @@ struct ViewControllerTests { @Test func userViews() async throws { - try await withDependencies { - $0.dateFormatter = .liveValue - $0.database.users = .mock - $0.viewController = .liveValue - } operation: { - @Dependency(\.database) var database - @Dependency(\.viewController) var viewController + try await withSnapshotTesting(record: record) { + try await withDependencies { + $0.dateFormatter = .liveValue + $0.database.users = .mock + $0.viewController = .liveValue + } operation: { + @Dependency(\.database) var database + @Dependency(\.viewController) var viewController - var htmlString = try await viewController.render(.user(.index)) - assertSnapshot(of: htmlString, as: .html) + var htmlString = try await viewController.render(.user(.index)) + assertSnapshot(of: htmlString, as: .html) - htmlString = try await viewController.render(.user(.form)) - assertSnapshot(of: htmlString, as: .html) + htmlString = try await viewController.render(.user(.form)) + assertSnapshot(of: htmlString, as: .html) - htmlString = try await viewController.render(.user(.create(.mock))) - assertSnapshot(of: htmlString, as: .html) + htmlString = try await viewController.render(.user(.create(.mock))) + assertSnapshot(of: htmlString, as: .html) - htmlString = try await viewController.render(.user(.get(id: UUID(0)))) - assertSnapshot(of: htmlString, as: .html) + htmlString = try await viewController.render(.user(.get(id: UUID(0)))) + assertSnapshot(of: htmlString, as: .html) - htmlString = try await viewController.render(.user(.update(id: UUID(0), updates: .mock))) - assertSnapshot(of: htmlString, as: .html) + htmlString = try await viewController.render(.user(.update(id: UUID(0), updates: .mock))) + assertSnapshot(of: htmlString, as: .html) + } } } @Test func vendorViews() async throws { - try await withDependencies { - $0.dateFormatter = .liveValue - $0.database.vendors = .mock - $0.viewController = .liveValue - } operation: { - @Dependency(\.database) var database - @Dependency(\.viewController) var viewController + try await withSnapshotTesting(record: record) { + try await withDependencies { + $0.dateFormatter = .liveValue + $0.database.vendors = .mock + $0.viewController = .liveValue + } operation: { + @Dependency(\.database) var database + @Dependency(\.viewController) var viewController - var htmlString = try await viewController.render(.vendor(.index)) - assertSnapshot(of: htmlString, as: .html) + var htmlString = try await viewController.render(.vendor(.index)) + assertSnapshot(of: htmlString, as: .html) - htmlString = try await viewController.render(.vendor(.form)) - assertSnapshot(of: htmlString, as: .html) + htmlString = try await viewController.render(.vendor(.form)) + assertSnapshot(of: htmlString, as: .html) - htmlString = try await viewController.render(.vendor(.create(.mock))) - assertSnapshot(of: htmlString, as: .html) + htmlString = try await viewController.render(.vendor(.create(.mock))) + assertSnapshot(of: htmlString, as: .html) - htmlString = try await viewController.render(.vendor(.get(id: UUID(0)))) - assertSnapshot(of: htmlString, as: .html) + htmlString = try await viewController.render(.vendor(.get(id: UUID(0)))) + assertSnapshot(of: htmlString, as: .html) - htmlString = try await viewController.render(.vendor(.update(id: UUID(0), updates: .mock))) - assertSnapshot(of: htmlString, as: .html) + htmlString = try await viewController.render(.vendor(.update(id: UUID(0), updates: .mock))) + assertSnapshot(of: htmlString, as: .html) + } } } @Test func vendorBranchViews() async throws { - try await withDependencies { - $0.dateFormatter = .liveValue - $0.database.vendors = .mock - $0.database.vendorBranches = .mock - $0.viewController = .liveValue - } operation: { - @Dependency(\.database) var database - @Dependency(\.viewController) var viewController + try await withSnapshotTesting(record: record) { + try await withDependencies { + $0.dateFormatter = .liveValue + $0.database.vendors = .mock + $0.database.vendorBranches = .mock + $0.viewController = .liveValue + } operation: { + @Dependency(\.database) var database + @Dependency(\.viewController) var viewController - var htmlString = try await viewController.render(.vendorBranch(.index(for: UUID(0)))) - assertSnapshot(of: htmlString, as: .html) + var htmlString = try await viewController.render(.vendorBranch(.index(for: UUID(0)))) + assertSnapshot(of: htmlString, as: .html) - htmlString = try await viewController.render(.vendorBranch(.select(context: .purchaseOrderSearch))) - assertSnapshot(of: htmlString, as: .html) + htmlString = try await viewController.render(.vendorBranch(.select(context: .purchaseOrderSearch))) + assertSnapshot(of: htmlString, as: .html) - htmlString = try await viewController.render(.vendorBranch(.select(context: .purchaseOrderForm))) - assertSnapshot(of: htmlString, as: .html) + htmlString = try await viewController.render(.vendorBranch(.select(context: .purchaseOrderForm))) + assertSnapshot(of: htmlString, as: .html) - htmlString = try await viewController.render(.vendorBranch(.create(.mock))) - assertSnapshot(of: htmlString, as: .html) + htmlString = try await viewController.render(.vendorBranch(.create(.mock))) + assertSnapshot(of: htmlString, as: .html) + } } } } @@ -182,14 +188,12 @@ struct ViewControllerTests { extension ViewController { func render(_ route: ViewRoute) async throws -> String { - guard let html = try await view( + let html = try await view( for: route, isHtmxRequest: true, logger: .init(label: "tests"), authenticate: { _ in } - ) else { - throw TestError() - } + ) return html.renderFormatted() } diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/employeeViews.3.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/employeeViews.3.html index 46bbcff..c7fd027 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/employeeViews.3.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/employeeViews.3.html @@ -10,7 +10,7 @@
- +
\ No newline at end of file diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/userViews.4.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/userViews.4.html index bd0fc68..61877f3 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/userViews.4.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/userViews.4.html @@ -12,7 +12,7 @@
Created:Updated:
- +
\ No newline at end of file diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorBranchViews.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorBranchViews.1.html index 94c82f7..274c090 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorBranchViews.1.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorBranchViews.1.html @@ -1,7 +1,7 @@
  • Mock -
  • diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorBranchViews.4.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorBranchViews.4.html index c7ed01a..6cc3a2d 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorBranchViews.4.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorBranchViews.4.html @@ -1,6 +1,6 @@
  • Mock -
  • \ No newline at end of file diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorViews.3.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorViews.3.html index 7feeda8..1be0dea 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorViews.3.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorViews.3.html @@ -6,7 +6,7 @@
    - +
    diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorViews.4.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorViews.4.html index 5312bd8..42ad697 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorViews.4.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorViews.4.html @@ -5,7 +5,7 @@
    - +
    diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorViews.5.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorViews.5.html index 5312bd8..42ad697 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorViews.5.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/vendorViews.5.html @@ -5,7 +5,7 @@
    - +
    diff --git a/Tests/ViewRouteTests/EmployeeViewRouteTests.swift b/Tests/ViewRouteTests/EmployeeViewRouteTests.swift index 76826a2..765f863 100644 --- a/Tests/ViewRouteTests/EmployeeViewRouteTests.swift +++ b/Tests/ViewRouteTests/EmployeeViewRouteTests.swift @@ -21,18 +21,18 @@ struct EmployeeViewRouteTests { ) } - @Test - func employeeDelete() throws { - let id = UUID(0) - var request = URLRequestData( - method: "DELETE", - path: "/employees/\(id)" - ) - let route = try router.parse(&request) - #expect( - route == .employee(.delete(id: id)) - ) - } + // @Test + // func employeeDelete() throws { + // let id = UUID(0) + // var request = URLRequestData( + // method: "DELETE", + // path: "/employees/\(id)" + // ) + // let route = try router.parse(&request) + // #expect( + // route == .employee(.delete(id: id)) + // ) + // } @Test func employeeForm() throws { diff --git a/Tests/ViewRouteTests/PurchaseOrderViewRouteTests.swift b/Tests/ViewRouteTests/PurchaseOrderViewRouteTests.swift index ed4aa9b..8ef8dd8 100644 --- a/Tests/ViewRouteTests/PurchaseOrderViewRouteTests.swift +++ b/Tests/ViewRouteTests/PurchaseOrderViewRouteTests.swift @@ -29,16 +29,16 @@ struct PurchaseOrderViewRouteTests { )))) } - @Test - func delete() throws { - let id = 1 - var request = URLRequestData( - method: "DELETE", - path: "/purchase-orders/\(id)" - ) - let route = try router.parse(&request) - #expect(route == .purchaseOrder(.delete(id: id))) - } + // @Test + // func delete() throws { + // let id = 1 + // var request = URLRequestData( + // method: "DELETE", + // path: "/purchase-orders/\(id)" + // ) + // let route = try router.parse(&request) + // #expect(route == .purchaseOrder(.delete(id: id))) + // } @Test func form() throws { diff --git a/Tests/ViewRouteTests/UserViewRouteTests.swift b/Tests/ViewRouteTests/UserViewRouteTests.swift index 6619721..4e8f5fc 100644 --- a/Tests/ViewRouteTests/UserViewRouteTests.swift +++ b/Tests/ViewRouteTests/UserViewRouteTests.swift @@ -25,16 +25,16 @@ struct UserViewRouteTests { )))) } - @Test - func delete() throws { - let id = UUID(0) - var request = URLRequestData( - method: "DELETE", - path: "/users/\(id)" - ) - let route = try router.parse(&request) - #expect(route == .user(.delete(id: id))) - } + // @Test + // func delete() throws { + // let id = UUID(0) + // var request = URLRequestData( + // method: "DELETE", + // path: "/users/\(id)" + // ) + // let route = try router.parse(&request) + // #expect(route == .user(.delete(id: id))) + // } @Test func form() throws { diff --git a/Tests/ViewRouteTests/VendorBranchViewRouteTests.swift b/Tests/ViewRouteTests/VendorBranchViewRouteTests.swift index e64d37e..154e73f 100644 --- a/Tests/ViewRouteTests/VendorBranchViewRouteTests.swift +++ b/Tests/ViewRouteTests/VendorBranchViewRouteTests.swift @@ -20,16 +20,16 @@ struct VendorBranchViewRouteTests { #expect(route == .vendorBranch(.create(.init(name: "Test", vendorID: id)))) } - @Test - func delete() throws { - let id = UUID(0) - var request = URLRequestData( - method: "DELETE", - path: "/vendors/branches/\(id)" - ) - let route = try router.parse(&request) - #expect(route == .vendorBranch(.delete(id: id))) - } + // @Test + // func delete() throws { + // let id = UUID(0) + // var request = URLRequestData( + // method: "DELETE", + // path: "/vendors/branches/\(id)" + // ) + // let route = try router.parse(&request) + // #expect(route == .vendorBranch(.delete(id: id))) + // } @Test func index() throws { diff --git a/Tests/ViewRouteTests/VendorViewRouteTests.swift b/Tests/ViewRouteTests/VendorViewRouteTests.swift index 09a773c..3328abd 100644 --- a/Tests/ViewRouteTests/VendorViewRouteTests.swift +++ b/Tests/ViewRouteTests/VendorViewRouteTests.swift @@ -19,16 +19,16 @@ struct VendorViewRouteTests { #expect(route == .vendor(.create(.init(name: "Test")))) } - @Test - func delete() throws { - let id = UUID(0) - var request = URLRequestData( - method: "DELETE", - path: "/vendors/\(id)" - ) - let route = try router.parse(&request) - #expect(route == .vendor(.delete(id: id))) - } + // @Test + // func delete() throws { + // let id = UUID(0) + // var request = URLRequestData( + // method: "DELETE", + // path: "/vendors/\(id)" + // ) + // let route = try router.parse(&request) + // #expect(route == .vendor(.delete(id: id))) + // } @Test func get() throws {