Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring back the vapor provider #100

Merged
merged 4 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PackageDescription
let package = Package(
name: "HTMLKit",
platforms: [
.macOS(.v10_14),
.macOS(.v10_15),
],
products: [
.library(
Expand All @@ -24,7 +24,8 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/miroslavkovac/Lingo.git", from: "3.1.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.1"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.65.2")
],
targets: [
.target(
Expand All @@ -49,6 +50,13 @@ let package = Package(
.process("Resources")
]
),
.target(
name: "HTMLKitVaporProvider",
dependencies: [
.target(name: "HTMLKit"),
.product(name: "Vapor", package: "vapor")
]
),
.testTarget(
name: "HTMLKitTests",
dependencies: [
Expand All @@ -74,6 +82,14 @@ let package = Package(
.target(name: "HTMLKit")
]
),
.testTarget(
name: "HTMLKitVaporProviderTests",
dependencies: [
.target(name: "HTMLKitVaporProvider"),
.target(name: "HTMLKit"),
.product(name: "XCTVapor", package: "vapor")
]
),
.executableTarget(
name: "ConvertCommand",
dependencies: [
Expand Down
63 changes: 63 additions & 0 deletions Sources/HTMLKitVaporProvider/Extensions/Vapor+HTMLKit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import HTMLKit
import Vapor

extension Application.Views.Provider {

public static var htmlkit: Self {
return .init {
$0.views.use {
$0.htmlkit.renderer
}
}
}
}

extension Application {

public var htmlkit: HtmlKit {
return .init(application: self)
}

public struct HtmlKit {

public let application: Application

public var renderer: VaporRenderer {
return .init(eventLoop: self.application.eventLoopGroup.next(), cache: self.cache)
}

public struct VariablesStorageKey: StorageKey {
public typealias Value = VaporCache
}

public var cache: VaporCache {

if let cache = self.application.storage[VariablesStorageKey.self] {
return cache

} else {

let cache = VaporCache()

self.application.storage[VariablesStorageKey.self] = cache

return cache
}
}

public func add<T: HTMLKit.Page>(page: T) {
self.renderer.add(page: page)
}

public func add<T: HTMLKit.View>(view: T) {
self.renderer.add(view: view)
}
}
}

extension Request {

public var htmlkit: VaporRenderer {
return .init(eventLoop: self.eventLoop, cache: self.application.htmlkit.cache)
}
}
33 changes: 33 additions & 0 deletions Sources/HTMLKitVaporProvider/VaporCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import HTMLKit
import Vapor

public class VaporCache {

private var cache: [String: HTMLKit.Renderer.Formula]

public var count: Int {
return self.cache.keys.count
}

public init() {
self.cache = .init()
}

public func retrieve(name: String, on loop: EventLoop) -> EventLoopFuture<HTMLKit.Renderer.Formula?> {

if let cache = self.cache[name] {
return loop.makeSucceededFuture(cache)

} else {
return loop.makeSucceededFuture(nil)
}
}

public func upsert(name: String, formula: HTMLKit.Renderer.Formula) {
self.cache.updateValue(formula, forKey: name)
}

public func remove(name: String) {
self.cache.removeValue(forKey: name)
}
}
85 changes: 85 additions & 0 deletions Sources/HTMLKitVaporProvider/VaporRenderer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import HTMLKit
import Vapor

public class VaporRenderer {

public enum RendererError: Error {

case unkownTemplate(String)

public var description: String {

switch self {
case .unkownTemplate(let name):
return "Template '\(name)' not found."
}
}
}

private var renderer: HTMLKit.Renderer {
return .init()
}

private var eventLoop: EventLoop

private var cache: VaporCache

public init(eventLoop: EventLoop, cache: VaporCache) {
self.eventLoop = eventLoop
self.cache = cache
}

public func render(name: String, context: Encodable) -> EventLoopFuture<ByteBuffer> {

return self.cache.retrieve(name: name, on: self.eventLoop).flatMap { formula in

guard let formula = formula else {
return self.eventLoop.makeFailedFuture(RendererError.unkownTemplate(name))
}

var buffer = ByteBufferAllocator().buffer(capacity: 4096)

let manager = HTMLKit.Renderer.ContextManager(rootContext: context)

for ingredient in formula.ingredient {

if let value = try? ingredient.render(with: manager) {
buffer.writeString(value)
}
}

return self.eventLoop.makeSucceededFuture(buffer)
}
}

public func add<T:HTMLKit.Page>(page: T) {

let formula = HTMLKit.Renderer.Formula()

try? page.prerender(formula)

self.cache.upsert(name: String(describing: type(of: page)), formula: formula)
}

public func add<T:HTMLKit.View>(view: T) {

let formula = HTMLKit.Renderer.Formula()

try? view.prerender(formula)

self.cache.upsert(name: String(describing: type(of: view)), formula: formula)
}
}

extension VaporRenderer: ViewRenderer {

public func `for`(_ request: Request) -> ViewRenderer {
return request.htmlkit
}

public func render<E:Encodable>(_ name: String, _ context: E) -> EventLoopFuture<Vapor.View> {
return self.render(name: name, context: context).map { buffer in
return View(data: buffer)
}
}
}
131 changes: 131 additions & 0 deletions Tests/HTMLKitVaporProviderTests/ProviderTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import XCTVapor
import HTMLKit
import HTMLKitVaporProvider

final class ProviderTests: XCTestCase {

struct TestContext: Vapor.Content {
let greeting: String
}

struct TestView: HTMLKit.View {

@TemplateValue(TestContext.self)
var context

var body: AnyContent {
Document(type: .html5)
Html {
Head {
Title {
"title"
}
}
Body {
Paragraph {
context.greeting
}
}
}
}
}

func testEventLoopIntegrationWithViewRenderer() throws {

let app = Application(.testing)

defer { app.shutdown() }

app.views.use(.htmlkit)

app.htmlkit.add(view: TestView())

app.get("test") { request -> EventLoopFuture<Vapor.View> in
return request.view.render("TestView", TestContext(greeting: "hello world"))
}

try app.test(.GET, "test") { response in
XCTAssertEqual(response.status, .ok)
XCTAssertEqual(response.body.string,
"""
<!DOCTYPE html>\
<html>\
<head>\
<title>title</title>\
</head>\
<body>\
<p>\
hello world\
</p>\
</body>\
</html>
"""
)
}
}

func testEventLoopIntegration() throws {

let app = Application(.testing)

defer { app.shutdown() }

app.htmlkit.add(view: TestView())

app.get("test") { request -> EventLoopFuture<Vapor.View> in
return request.htmlkit.render("TestView", TestContext(greeting: "hello world"))
}

try app.test(.GET, "test") { response in
XCTAssertEqual(response.status, .ok)
XCTAssertEqual(response.body.string,
"""
<!DOCTYPE html>\
<html>\
<head>\
<title>title</title>\
</head>\
<body>\
<p>\
hello world\
</p>\
</body>\
</html>
"""
)
}
}

@available(macOS 12, *)
func testConcurrencyIntegration() throws {

let app = Application(.testing)

defer { app.shutdown() }

app.htmlkit.add(view: TestView())

app.get("test") { request async throws -> Vapor.View in
return try await request.htmlkit.render("TestView", TestContext(greeting: "hello world"))
}

try app.test(.GET, "test") { response in
XCTAssertEqual(response.status, .ok)
XCTAssertEqual(response.body.string,
"""
<!DOCTYPE html>\
<html>\
<head>\
<title>title</title>\
</head>\
<body>\
<p>\
hello world\
</p>\
</body>\
</html>
"""
)
}
}
}