Skip to content

Commit

Permalink
Added some basic types
Browse files Browse the repository at this point in the history
  • Loading branch information
RussBaz committed Nov 5, 2023
1 parent d909d17 commit 1a63a73
Show file tree
Hide file tree
Showing 29 changed files with 1,102 additions and 3 deletions.
18 changes: 18 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@
"version" : "4.10.1"
}
},
{
"identity" : "leaf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/leaf.git",
"state" : {
"revision" : "6fe0e843c6599f5189e45c7b08739ebc5c410c3b",
"version" : "4.2.4"
}
},
{
"identity" : "leaf-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/leaf-kit.git",
"state" : {
"revision" : "13f2fc4c8479113cd23876d9a434ef4573e368bb",
"version" : "1.10.2"
}
},
{
"identity" : "multipart-kit",
"kind" : "remoteSourceControl",
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.85.1"),
.package(url: "https://github.com/vapor/leaf.git", from: "4.2.4"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "VHX", dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Leaf", package: "leaf"),
]
),
.testTarget(
Expand Down
6 changes: 5 additions & 1 deletion README.md
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
32 changes: 32 additions & 0 deletions Sources/VHX/Commands/AsyncCommand.swift
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()
}
}
20 changes: 20 additions & 0 deletions Sources/VHX/Htmx/Application+HtmxConfiguration.swift
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")
}
}
}
}
7 changes: 7 additions & 0 deletions Sources/VHX/Htmx/Content+HX.swift
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)
}
}
7 changes: 7 additions & 0 deletions Sources/VHX/Htmx/HTTPHeaders+HXRequestHeaders.swift
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)
}
}
7 changes: 7 additions & 0 deletions Sources/VHX/Htmx/HTTPStatus+HX.swift
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)
}
}
25 changes: 25 additions & 0 deletions Sources/VHX/Htmx/HX.swift
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)
}
}
}
}
15 changes: 15 additions & 0 deletions Sources/VHX/Htmx/HXConfiguration.swift
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)
}
}
1 change: 1 addition & 0 deletions Sources/VHX/Htmx/HXError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

46 changes: 46 additions & 0 deletions Sources/VHX/Htmx/HXLeafSource.swift
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)
}
}
21 changes: 21 additions & 0 deletions Sources/VHX/Htmx/HXRequestHeaders.swift
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 }
}
}
90 changes: 90 additions & 0 deletions Sources/VHX/Htmx/Htmx.swift
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 }
}
Loading

0 comments on commit 1a63a73

Please sign in to comment.