Skip to content

Commit 9d911d5

Browse files
authored
Bring back the vapor provider (#100)
* Add the new targets * Add a vapor renderer The vapor renderer lays on top of the htmlkit renderer. In this way the htmlkit renderer stays indepented of the web framework. * Add tests for the provider * Add context handling
1 parent e6aa999 commit 9d911d5

File tree

5 files changed

+330
-2
lines changed

5 files changed

+330
-2
lines changed

Package.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PackageDescription
55
let package = Package(
66
name: "HTMLKit",
77
platforms: [
8-
.macOS(.v10_14),
8+
.macOS(.v10_15),
99
],
1010
products: [
1111
.library(
@@ -24,7 +24,8 @@ let package = Package(
2424
dependencies: [
2525
.package(url: "https://github.com/miroslavkovac/Lingo.git", from: "3.1.0"),
2626
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.1"),
27-
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
27+
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
28+
.package(url: "https://github.com/vapor/vapor.git", from: "4.65.2")
2829
],
2930
targets: [
3031
.target(
@@ -49,6 +50,13 @@ let package = Package(
4950
.process("Resources")
5051
]
5152
),
53+
.target(
54+
name: "HTMLKitVaporProvider",
55+
dependencies: [
56+
.target(name: "HTMLKit"),
57+
.product(name: "Vapor", package: "vapor")
58+
]
59+
),
5260
.testTarget(
5361
name: "HTMLKitTests",
5462
dependencies: [
@@ -74,6 +82,14 @@ let package = Package(
7482
.target(name: "HTMLKit")
7583
]
7684
),
85+
.testTarget(
86+
name: "HTMLKitVaporProviderTests",
87+
dependencies: [
88+
.target(name: "HTMLKitVaporProvider"),
89+
.target(name: "HTMLKit"),
90+
.product(name: "XCTVapor", package: "vapor")
91+
]
92+
),
7793
.executableTarget(
7894
name: "ConvertCommand",
7995
dependencies: [
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import HTMLKit
2+
import Vapor
3+
4+
extension Application.Views.Provider {
5+
6+
public static var htmlkit: Self {
7+
return .init {
8+
$0.views.use {
9+
$0.htmlkit.renderer
10+
}
11+
}
12+
}
13+
}
14+
15+
extension Application {
16+
17+
public var htmlkit: HtmlKit {
18+
return .init(application: self)
19+
}
20+
21+
public struct HtmlKit {
22+
23+
public let application: Application
24+
25+
public var renderer: VaporRenderer {
26+
return .init(eventLoop: self.application.eventLoopGroup.next(), cache: self.cache)
27+
}
28+
29+
public struct VariablesStorageKey: StorageKey {
30+
public typealias Value = VaporCache
31+
}
32+
33+
public var cache: VaporCache {
34+
35+
if let cache = self.application.storage[VariablesStorageKey.self] {
36+
return cache
37+
38+
} else {
39+
40+
let cache = VaporCache()
41+
42+
self.application.storage[VariablesStorageKey.self] = cache
43+
44+
return cache
45+
}
46+
}
47+
48+
public func add<T: HTMLKit.Page>(page: T) {
49+
self.renderer.add(page: page)
50+
}
51+
52+
public func add<T: HTMLKit.View>(view: T) {
53+
self.renderer.add(view: view)
54+
}
55+
}
56+
}
57+
58+
extension Request {
59+
60+
public var htmlkit: VaporRenderer {
61+
return .init(eventLoop: self.eventLoop, cache: self.application.htmlkit.cache)
62+
}
63+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import HTMLKit
2+
import Vapor
3+
4+
public class VaporCache {
5+
6+
private var cache: [String: HTMLKit.Renderer.Formula]
7+
8+
public var count: Int {
9+
return self.cache.keys.count
10+
}
11+
12+
public init() {
13+
self.cache = .init()
14+
}
15+
16+
public func retrieve(name: String, on loop: EventLoop) -> EventLoopFuture<HTMLKit.Renderer.Formula?> {
17+
18+
if let cache = self.cache[name] {
19+
return loop.makeSucceededFuture(cache)
20+
21+
} else {
22+
return loop.makeSucceededFuture(nil)
23+
}
24+
}
25+
26+
public func upsert(name: String, formula: HTMLKit.Renderer.Formula) {
27+
self.cache.updateValue(formula, forKey: name)
28+
}
29+
30+
public func remove(name: String) {
31+
self.cache.removeValue(forKey: name)
32+
}
33+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import HTMLKit
2+
import Vapor
3+
4+
public class VaporRenderer {
5+
6+
public enum RendererError: Error {
7+
8+
case unkownTemplate(String)
9+
10+
public var description: String {
11+
12+
switch self {
13+
case .unkownTemplate(let name):
14+
return "Template '\(name)' not found."
15+
}
16+
}
17+
}
18+
19+
private var renderer: HTMLKit.Renderer {
20+
return .init()
21+
}
22+
23+
private var eventLoop: EventLoop
24+
25+
private var cache: VaporCache
26+
27+
public init(eventLoop: EventLoop, cache: VaporCache) {
28+
self.eventLoop = eventLoop
29+
self.cache = cache
30+
}
31+
32+
public func render(name: String, context: Encodable) -> EventLoopFuture<ByteBuffer> {
33+
34+
return self.cache.retrieve(name: name, on: self.eventLoop).flatMap { formula in
35+
36+
guard let formula = formula else {
37+
return self.eventLoop.makeFailedFuture(RendererError.unkownTemplate(name))
38+
}
39+
40+
var buffer = ByteBufferAllocator().buffer(capacity: 4096)
41+
42+
let manager = HTMLKit.Renderer.ContextManager(rootContext: context)
43+
44+
for ingredient in formula.ingredient {
45+
46+
if let value = try? ingredient.render(with: manager) {
47+
buffer.writeString(value)
48+
}
49+
}
50+
51+
return self.eventLoop.makeSucceededFuture(buffer)
52+
}
53+
}
54+
55+
public func add<T:HTMLKit.Page>(page: T) {
56+
57+
let formula = HTMLKit.Renderer.Formula()
58+
59+
try? page.prerender(formula)
60+
61+
self.cache.upsert(name: String(describing: type(of: page)), formula: formula)
62+
}
63+
64+
public func add<T:HTMLKit.View>(view: T) {
65+
66+
let formula = HTMLKit.Renderer.Formula()
67+
68+
try? view.prerender(formula)
69+
70+
self.cache.upsert(name: String(describing: type(of: view)), formula: formula)
71+
}
72+
}
73+
74+
extension VaporRenderer: ViewRenderer {
75+
76+
public func `for`(_ request: Request) -> ViewRenderer {
77+
return request.htmlkit
78+
}
79+
80+
public func render<E:Encodable>(_ name: String, _ context: E) -> EventLoopFuture<Vapor.View> {
81+
return self.render(name: name, context: context).map { buffer in
82+
return View(data: buffer)
83+
}
84+
}
85+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import XCTVapor
2+
import HTMLKit
3+
import HTMLKitVaporProvider
4+
5+
final class ProviderTests: XCTestCase {
6+
7+
struct TestContext: Vapor.Content {
8+
let greeting: String
9+
}
10+
11+
struct TestView: HTMLKit.View {
12+
13+
@TemplateValue(TestContext.self)
14+
var context
15+
16+
var body: AnyContent {
17+
Document(type: .html5)
18+
Html {
19+
Head {
20+
Title {
21+
"title"
22+
}
23+
}
24+
Body {
25+
Paragraph {
26+
context.greeting
27+
}
28+
}
29+
}
30+
}
31+
}
32+
33+
func testEventLoopIntegrationWithViewRenderer() throws {
34+
35+
let app = Application(.testing)
36+
37+
defer { app.shutdown() }
38+
39+
app.views.use(.htmlkit)
40+
41+
app.htmlkit.add(view: TestView())
42+
43+
app.get("test") { request -> EventLoopFuture<Vapor.View> in
44+
return request.view.render("TestView", TestContext(greeting: "hello world"))
45+
}
46+
47+
try app.test(.GET, "test") { response in
48+
XCTAssertEqual(response.status, .ok)
49+
XCTAssertEqual(response.body.string,
50+
"""
51+
<!DOCTYPE html>\
52+
<html>\
53+
<head>\
54+
<title>title</title>\
55+
</head>\
56+
<body>\
57+
<p>\
58+
hello world\
59+
</p>\
60+
</body>\
61+
</html>
62+
"""
63+
)
64+
}
65+
}
66+
67+
func testEventLoopIntegration() throws {
68+
69+
let app = Application(.testing)
70+
71+
defer { app.shutdown() }
72+
73+
app.htmlkit.add(view: TestView())
74+
75+
app.get("test") { request -> EventLoopFuture<Vapor.View> in
76+
return request.htmlkit.render("TestView", TestContext(greeting: "hello world"))
77+
}
78+
79+
try app.test(.GET, "test") { response in
80+
XCTAssertEqual(response.status, .ok)
81+
XCTAssertEqual(response.body.string,
82+
"""
83+
<!DOCTYPE html>\
84+
<html>\
85+
<head>\
86+
<title>title</title>\
87+
</head>\
88+
<body>\
89+
<p>\
90+
hello world\
91+
</p>\
92+
</body>\
93+
</html>
94+
"""
95+
)
96+
}
97+
}
98+
99+
@available(macOS 12, *)
100+
func testConcurrencyIntegration() throws {
101+
102+
let app = Application(.testing)
103+
104+
defer { app.shutdown() }
105+
106+
app.htmlkit.add(view: TestView())
107+
108+
app.get("test") { request async throws -> Vapor.View in
109+
return try await request.htmlkit.render("TestView", TestContext(greeting: "hello world"))
110+
}
111+
112+
try app.test(.GET, "test") { response in
113+
XCTAssertEqual(response.status, .ok)
114+
XCTAssertEqual(response.body.string,
115+
"""
116+
<!DOCTYPE html>\
117+
<html>\
118+
<head>\
119+
<title>title</title>\
120+
</head>\
121+
<body>\
122+
<p>\
123+
hello world\
124+
</p>\
125+
</body>\
126+
</html>
127+
"""
128+
)
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)