diff --git a/Package.resolved b/Package.resolved index 315e075..9609c6d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/routing-kit.git", "state" : { - "revision" : "88077f2c9d12777dcc89562fa581888ff7ba14ae", - "version" : "4.8.1" + "revision" : "17a7a3facce8285fd257aa7c72d5e480351e7698", + "version" : "4.8.2" } }, { @@ -185,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor.git", "state" : { - "revision" : "0fa646e517dff34aa5d0ae12d221021ec07a801d", - "version" : "4.85.1" + "revision" : "d682e05fdb64c9f7da01af096a73cd11bb7ab755", + "version" : "4.86.2" } }, { diff --git a/Package.swift b/Package.swift index 665507d..35235a7 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "4.85.1"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.86.2"), .package(url: "https://github.com/vapor/leaf.git", from: "4.2.4"), ], targets: [ diff --git a/Sources/VHX/Commands/AsyncCommand.swift b/Sources/VHX/Commands/HXAsyncCommand.swift similarity index 89% rename from Sources/VHX/Commands/AsyncCommand.swift rename to Sources/VHX/Commands/HXAsyncCommand.swift index 495672c..7eff017 100644 --- a/Sources/VHX/Commands/AsyncCommand.swift +++ b/Sources/VHX/Commands/HXAsyncCommand.swift @@ -3,14 +3,14 @@ import Vapor // Code is taken from the following tutorial: // https://theswiftdev.com/running-and-testing-async-vapor-commands/ -public protocol CustomAsyncCommand: Command { +public protocol HXAsyncCommand: Command { func command( using context: CommandContext, signature: Signature ) async throws } -public extension CustomAsyncCommand { +public extension HXAsyncCommand { func run( using context: CommandContext, signature: Signature diff --git a/Sources/VHX/Htmx/HXConfiguration.swift b/Sources/VHX/Htmx/HXConfiguration.swift index 805905b..6d54a81 100644 --- a/Sources/VHX/Htmx/HXConfiguration.swift +++ b/Sources/VHX/Htmx/HXConfiguration.swift @@ -9,7 +9,11 @@ public extension HtmxConfiguration { pageSource = hxPageLeafSource(template: nil) } - init(template: @escaping (_ name: String) -> String) { - pageSource = hxPageLeafSource(template: template) + init(pagePrefix prefix: String) { + pageSource = hxPageLeafSource(prefix: prefix, template: nil) + } + + init(pagePrefix prefix: String = "--page", pageTemplate template: @escaping (_ name: String) -> String) { + pageSource = hxPageLeafSource(prefix: prefix, template: template) } } diff --git a/Sources/VHX/Htmx/ReponseHeaders/HXLocationHeader.swift b/Sources/VHX/Htmx/ReponseHeaders/HXLocationHeader.swift index 97d1bdf..ac9495b 100644 --- a/Sources/VHX/Htmx/ReponseHeaders/HXLocationHeader.swift +++ b/Sources/VHX/Htmx/ReponseHeaders/HXLocationHeader.swift @@ -39,7 +39,11 @@ public struct HXLocationHeader { } func add(to resp: Response) { - resp.headers.replaceOrAdd(name: "HX-Location", value: serialise()) + let serialised = serialise() + + if !serialised.isEmpty { + resp.headers.replaceOrAdd(name: "HX-Location", value: serialised) + } } } diff --git a/Sources/VHX/Htmx/ReponseHeaders/HXPushUrlHeader.swift b/Sources/VHX/Htmx/ReponseHeaders/HXPushUrlHeader.swift index f2dbfe0..cf505af 100644 --- a/Sources/VHX/Htmx/ReponseHeaders/HXPushUrlHeader.swift +++ b/Sources/VHX/Htmx/ReponseHeaders/HXPushUrlHeader.swift @@ -6,21 +6,25 @@ public struct HXPushUrlHeader { case disable case custom(String) } - + let url: HXPushType - + func serialise() -> String { switch url { case .enable: "true" case .disable: "false" - case .custom(let custom): + case let .custom(custom): "\(custom)" } } - + func add(to resp: Response) { - resp.headers.replaceOrAdd(name: "HX-Push-Url", value: serialise()) + let serialised = serialise() + + if !serialised.isEmpty { + resp.headers.replaceOrAdd(name: "HX-Push-Url", value: serialised) + } } } diff --git a/Sources/VHX/Htmx/ReponseHeaders/HXRedirectHeader.swift b/Sources/VHX/Htmx/ReponseHeaders/HXRedirectHeader.swift index 3aa8b1b..eb4764c 100644 --- a/Sources/VHX/Htmx/ReponseHeaders/HXRedirectHeader.swift +++ b/Sources/VHX/Htmx/ReponseHeaders/HXRedirectHeader.swift @@ -2,11 +2,11 @@ import Vapor public struct HXRedirectHeader { let location: String - + func serialise() -> String { location } - + func add(to resp: Response) { if !location.isEmpty { resp.headers.replaceOrAdd(name: "HX-Redirect", value: serialise()) diff --git a/Sources/VHX/Htmx/ReponseHeaders/HXRefreshHeader.swift b/Sources/VHX/Htmx/ReponseHeaders/HXRefreshHeader.swift index dfebbdc..9f6234b 100644 --- a/Sources/VHX/Htmx/ReponseHeaders/HXRefreshHeader.swift +++ b/Sources/VHX/Htmx/ReponseHeaders/HXRefreshHeader.swift @@ -2,11 +2,11 @@ import Vapor public struct HXRefreshHeader { let value: Bool - + func serialise() -> String { if value { "true" } else { "" } } - + func add(to resp: Response) { if value { resp.headers.replaceOrAdd(name: "HX-Refresh", value: serialise()) diff --git a/Sources/VHX/Htmx/ReponseHeaders/HXReplaceUrlHeader.swift b/Sources/VHX/Htmx/ReponseHeaders/HXReplaceUrlHeader.swift index c76b0f9..eb5597b 100644 --- a/Sources/VHX/Htmx/ReponseHeaders/HXReplaceUrlHeader.swift +++ b/Sources/VHX/Htmx/ReponseHeaders/HXReplaceUrlHeader.swift @@ -6,21 +6,25 @@ public struct HXReplaceUrlHeader { case disable case custom(String) } - + let url: HXReplaceType - + func serialise() -> String { switch url { case .enable: "true" case .disable: "false" - case .custom(let custom): + case let .custom(custom): "\(custom)" } } - + func add(to resp: Response) { - resp.headers.replaceOrAdd(name: "HX-Replace-Url", value: serialise()) + let serialised = serialise() + + if !serialised.isEmpty { + resp.headers.replaceOrAdd(name: "HX-Replace-Url", value: serialised) + } } } diff --git a/Sources/VHX/Htmx/ReponseHeaders/HXReselectHeader.swift b/Sources/VHX/Htmx/ReponseHeaders/HXReselectHeader.swift index 34fd13d..ce04309 100644 --- a/Sources/VHX/Htmx/ReponseHeaders/HXReselectHeader.swift +++ b/Sources/VHX/Htmx/ReponseHeaders/HXReselectHeader.swift @@ -2,11 +2,11 @@ import Vapor public struct HXReselectHeader { let value: String - + func serialise() -> String { value } - + func add(to resp: Response) { if !value.isEmpty { resp.headers.replaceOrAdd(name: "HX-Reselect", value: serialise()) diff --git a/Sources/VHX/Htmx/ReponseHeaders/HXResponseHeaders.swift b/Sources/VHX/Htmx/ReponseHeaders/HXResponseHeaders.swift new file mode 100644 index 0000000..82bda6e --- /dev/null +++ b/Sources/VHX/Htmx/ReponseHeaders/HXResponseHeaders.swift @@ -0,0 +1,29 @@ +import Vapor + +public struct HXResponseHeaders { + var location: HXLocationHeader? + var pushUrl: HXPushUrlHeader? + var redirect: HXRedirectHeader? + var refresh: HXRefreshHeader? + var replaceUrl: HXReplaceUrlHeader? + var reselect: HXReselectHeader? + var reswap: HXReswapHeader? + var retarget: HXRetargetHeader? + var trigger: HXTriggerHeader? + var triggerAfterSettle: HXTriggerAfterSettleHeader? + var triggerAfterSwap: HXTriggerAfterSwapHeader? + + public func add(to resp: Response) { + location.map { $0.add(to: resp) } + pushUrl.map { $0.add(to: resp) } + redirect.map { $0.add(to: resp) } + refresh.map { $0.add(to: resp) } + replaceUrl.map { $0.add(to: resp) } + reselect.map { $0.add(to: resp) } + reswap.map { $0.add(to: resp) } + retarget.map { $0.add(to: resp) } + trigger.map { $0.add(to: resp) } + triggerAfterSettle.map { $0.add(to: resp) } + triggerAfterSwap.map { $0.add(to: resp) } + } +} diff --git a/Sources/VHX/Htmx/ReponseHeaders/HXRetargetHeader.swift b/Sources/VHX/Htmx/ReponseHeaders/HXRetargetHeader.swift index c51b03c..d5d7e4a 100644 --- a/Sources/VHX/Htmx/ReponseHeaders/HXRetargetHeader.swift +++ b/Sources/VHX/Htmx/ReponseHeaders/HXRetargetHeader.swift @@ -2,11 +2,11 @@ import Vapor public struct HXRetargetHeader { let value: String - + func serialise() -> String { value } - + func add(to resp: Response) { if !value.isEmpty { resp.headers.replaceOrAdd(name: "HX-Retarget", value: serialise()) diff --git a/Sources/VHX/Htmx/ReponseHeaders/HXTriggerHeader.swift b/Sources/VHX/Htmx/ReponseHeaders/HXTriggerHeader.swift new file mode 100644 index 0000000..54882e6 --- /dev/null +++ b/Sources/VHX/Htmx/ReponseHeaders/HXTriggerHeader.swift @@ -0,0 +1,104 @@ +import Vapor + +public enum HXTriggerEvent { + case basic([String]) + case custom([HXTriggerEventKind]) +} + +public enum HXTriggerEventKind { + case message(name: String, value: String) + case object(name: String, value: any Encodable) +} + +public struct HXTriggerHeader { + let value: HXTriggerEvent + + func serialise() -> String { + value.serialise() + } + + func add(to resp: Response) { + let serialised = serialise() + + if !serialised.isEmpty { + resp.headers.replaceOrAdd(name: "HX-Trigger", value: serialised) + } + } +} + +public struct HXTriggerAfterSettleHeader { + let value: HXTriggerEvent + + func serialise() -> String { + value.serialise() + } + + func add(to resp: Response) { + let serialised = serialise() + + if !serialised.isEmpty { + resp.headers.replaceOrAdd(name: "HX-Trigger-After-Settle", value: serialised) + } + } +} + +public struct HXTriggerAfterSwapHeader { + let value: HXTriggerEvent + + func serialise() -> String { + value.serialise() + } + + func add(to resp: Response) { + let serialised = serialise() + + if !serialised.isEmpty { + resp.headers.replaceOrAdd(name: "HX-Trigger-After-Swap", value: serialised) + } + } +} + +extension [HXTriggerEventKind] { + // The solution taken from: + // https://forums.swift.org/t/how-to-encode-objects-of-unknown-type/12253/2 + private struct AnyEncodable: Encodable { + private let _encode: (Encoder) throws -> Void + public init(_ wrapped: some Encodable) { + _encode = wrapped.encode + } + + func encode(to encoder: Encoder) throws { + try _encode(encoder) + } + } + + func serialise() -> String { + var data: [String: AnyEncodable] = [:] + + for i in self { + switch i { + case let .message(name: name, value: value): + data[name] = AnyEncodable(value) + case let .object(name: name, value: value): + data[name] = AnyEncodable(value) + } + } + + guard let values = try? String(data: JSONEncoder().encode(data), encoding: .utf8) else { + return "" + } + + return values + } +} + +extension HXTriggerEvent { + func serialise() -> String { + switch self { + case let .basic(events): + events.joined(separator: ", ") + case let .custom(events): + events.serialise() + } + } +} diff --git a/Sources/VHX/VHX.swift b/Sources/VHX/VHX.swift index dcb5a6c..185b2af 100644 --- a/Sources/VHX/VHX.swift +++ b/Sources/VHX/VHX.swift @@ -1,12 +1,18 @@ import Vapor -public func configureHtmx(_ app: Application, template: ((_ name: String) -> String)? = nil) throws { - if let template { - app.htmx = HtmxConfiguration(template: template) +public func configureHtmx(_ app: Application, pageTemplate template: ((_ name: String) -> String)? = nil) throws { + let config = if let template { + HtmxConfiguration(pageTemplate: template) } else { - app.htmx = HtmxConfiguration() + HtmxConfiguration() } + try configureHtmx(app, configuration: config) +} + +public func configureHtmx(_ app: Application, configuration: HtmxConfiguration) throws { + app.htmx = configuration + // Saving currnet sources in case these are the default sources app.leaf.sources = app.leaf.sources