-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
29 changed files
with
1,102 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,5 @@ | ||
# VHX | ||
# VHX - Vapor + Htmx + Extensions | ||
|
||
Work in progress. | ||
|
||
TODO: Write the docs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import Vapor | ||
|
||
// Code is taken from the following tutorial: | ||
// https://theswiftdev.com/running-and-testing-async-vapor-commands/ | ||
|
||
public protocol CustomAsyncCommand: Command { | ||
func command( | ||
using context: CommandContext, | ||
signature: Signature | ||
) async throws | ||
} | ||
|
||
public extension CustomAsyncCommand { | ||
func run( | ||
using context: CommandContext, | ||
signature: Signature | ||
) throws { | ||
let promise = context | ||
.application | ||
.eventLoopGroup | ||
.next() | ||
.makePromise(of: Void.self) | ||
|
||
promise.completeWithTask { | ||
try await command( | ||
using: context, | ||
signature: signature | ||
) | ||
} | ||
try promise.futureResult.wait() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import Vapor | ||
|
||
public extension Application { | ||
struct HtmxStorageKey: StorageKey { | ||
public typealias Value = HtmxConfiguration | ||
} | ||
|
||
var htmx: HtmxConfiguration { | ||
get { | ||
storage[HtmxStorageKey.self] ?? HtmxConfiguration() | ||
} | ||
set { | ||
if storage[HtmxStorageKey.self] == nil { | ||
storage[HtmxStorageKey.self] = newValue | ||
} else { | ||
fatalError("Rediclaration of HTMX configuraion") | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import Vapor | ||
|
||
public extension Content where Self: AsyncResponseEncodable & Encodable { | ||
func hx(template name: String? = nil, page: Bool? = nil) -> HX<Self> { | ||
.init(context: self, template: name, page: page) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import Vapor | ||
|
||
public extension HTTPHeaders { | ||
var htmx: HXRequestHeaders { | ||
.init(headers: self) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import Vapor | ||
|
||
public extension HTTPStatus { | ||
func hx(template name: String? = nil, page: Bool? = nil) -> HX<Self> { | ||
.init(context: self, template: name, page: page) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import Vapor | ||
|
||
public struct HX<T: AsyncResponseEncodable & Encodable> { | ||
let context: T | ||
let template: String? | ||
let page: Bool? | ||
} | ||
|
||
extension HX: AsyncResponseEncodable { | ||
public func encodeResponse(for request: Request) async throws -> Response { | ||
switch request.htmx.prefers { | ||
case .api: try await context.encodeResponse(for: request) | ||
case .htmx: if let template { | ||
try await request.htmx.render(template, context, page: page ?? false) | ||
} else { | ||
Response(status: .noContent) | ||
} | ||
case .html: if let template { | ||
try await request.htmx.render(template, context, page: page ?? true) | ||
} else { | ||
Response(status: .noContent) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import Vapor | ||
|
||
public struct HtmxConfiguration { | ||
let pageSource: HXLeafSource | ||
} | ||
|
||
public extension HtmxConfiguration { | ||
init() { | ||
pageSource = hxPageLeafSource(template: nil) | ||
} | ||
|
||
init(template: @escaping (_ name: String) -> String) { | ||
pageSource = hxPageLeafSource(template: template) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import LeafKit | ||
import Vapor | ||
|
||
public protocol HXLeafSource: LeafSource { | ||
var pagePrefix: String { get } | ||
} | ||
|
||
public struct HXBasicLeafSource: HXLeafSource { | ||
public let pagePrefix: String | ||
public let pageTemplate: (_ name: String) -> String | ||
|
||
public enum HtmxPageLeafSourceError: Error { | ||
case illegalFormat | ||
} | ||
|
||
public func file(template: String, escape _: Bool, on eventLoop: EventLoop) throws -> EventLoopFuture<ByteBuffer> { | ||
guard template.starts(with: "\(pagePrefix)/") else { | ||
throw HtmxPageLeafSourceError.illegalFormat | ||
} | ||
|
||
let remainder = template.dropFirst(7) | ||
|
||
guard remainder.distance(from: remainder.startIndex, to: template.endIndex) > 0 else { | ||
throw HtmxPageLeafSourceError.illegalFormat | ||
} | ||
|
||
let result = pageTemplate(String(remainder)) | ||
|
||
let buffer = ByteBuffer(string: result) | ||
|
||
return eventLoop.makeSucceededFuture(buffer) | ||
} | ||
} | ||
|
||
public func hxPageLeafSource(prefix: String = "--page", template: ((_ name: String) -> String)?) -> HXLeafSource { | ||
if let template { | ||
return HXBasicLeafSource(pagePrefix: prefix, pageTemplate: template) | ||
} else { | ||
func template(_ name: String) -> String { | ||
""" | ||
#extend("\(name)") | ||
""" | ||
} | ||
return HXBasicLeafSource(pagePrefix: prefix, pageTemplate: template) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import Vapor | ||
|
||
public struct HXRequestHeaders { | ||
let headers: HTTPHeaders | ||
|
||
var boosted: Bool { bool(for: "HX-Boosted") } | ||
var currentUrl: String? { headers["HX-Current-URL"].first } | ||
var historyRestoreRequest: Bool { bool(for: "HX-History-Restore-Request") } | ||
var prompt: Bool { bool(for: "HX-Prompt") } | ||
var request: Bool { bool(for: "HX-Request") } | ||
var target: String? { headers["HX-Target"].first } | ||
var triggerName: String? { headers["HX-Trigger-Name"].first } | ||
var trigger: String? { headers["HX-Trigger"].first } | ||
} | ||
|
||
public extension HXRequestHeaders { | ||
private func bool(for name: String) -> Bool { | ||
guard !name.isEmpty else { return false } | ||
return if let restore = headers[name].first { restore == "true" } else { false } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import Vapor | ||
|
||
public struct Htmx { | ||
public enum Preference { | ||
case htmx, html, api | ||
} | ||
|
||
let req: Request | ||
} | ||
|
||
public extension Htmx { | ||
var prefered: Bool { | ||
switch prefers { | ||
case .htmx: true | ||
default: false | ||
} | ||
} | ||
|
||
var prefers: Preference { | ||
// Preferences: | ||
// GET method with expected content json -> standard api response | ||
// GET method with expected content html without HX-Request header -> standard text/html response | ||
// GET method with expected content html with HX-Request header -> HTMX response (text/html) | ||
// GET method with no content prefrences without HX-Request header -> standard text/html response | ||
// GET method with no content prefrences with HX-Request header -> HTMX response (text/html) | ||
|
||
// All other methods with expected content json -> standard api response | ||
// All other methods with expected content html without HX-Request header -> standard text/html response | ||
// All other methods with expected content html with HX-Request header -> HTMX response (text/html) | ||
// All other methods with no content prefrences without HX-Request header -> standard api response | ||
// All other methods with no content prefrences with HX-Request header -> HTMX response (text/html) | ||
|
||
let preference = req.headers.accept.comparePreference(for: .json, to: .html) | ||
|
||
return switch preference { | ||
case .orderedSame: | ||
if req.method == .GET { | ||
if req.headers["HX-Request"].isEmpty { | ||
.html | ||
} else { | ||
.htmx | ||
} | ||
} else { | ||
if req.headers["HX-Request"].isEmpty { | ||
.api | ||
} else { | ||
.htmx | ||
} | ||
} | ||
case .orderedAscending: | ||
if req.headers["HX-Request"].isEmpty { | ||
.html | ||
} else { | ||
.htmx | ||
} | ||
case .orderedDescending: | ||
.api | ||
} | ||
} | ||
} | ||
|
||
public extension Htmx { | ||
func render(_ name: String, _ context: some Encodable, page: Bool? = nil) async throws -> Response { | ||
let page = page ?? (req.method == .GET && !req.headers.htmx.request) | ||
|
||
let view = if page { | ||
try await req.view.render("\(req.application.htmx.pageSource.pagePrefix)/\(name)", context).get() | ||
} else { | ||
try await req.view.render(name, context).get() | ||
} | ||
|
||
return try await view.encodeResponse(for: req) | ||
} | ||
|
||
func render(_ name: String, page: Bool? = nil) async throws -> Response { | ||
let page = page ?? (req.method == .GET && !req.headers.htmx.request) | ||
|
||
let view = if page { | ||
try await req.view.render("\(req.application.htmx.pageSource.pagePrefix)/\(name)") | ||
} else { | ||
try await req.view.render(name) | ||
} | ||
|
||
return try await view.encodeResponse(for: req) | ||
} | ||
} | ||
|
||
public extension Htmx { | ||
var headers: HXRequestHeaders { req.headers.htmx } | ||
} |
Oops, something went wrong.