Skip to content

Commit 5262cb4

Browse files
authored
type-erased HTMLResponse + headers (#4)
1 parent 000019f commit 5262cb4

4 files changed

Lines changed: 92 additions & 16 deletions

File tree

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ let package = Package(
2323
],
2424
dependencies: [
2525
.package(url: "https://github.com/vapor/vapor.git", from: "4.102.0"),
26-
.package(url: "https://github.com/sliemeobn/elementary.git", .upToNextMajor(from: "0.1.2")),
26+
.package(url: "https://github.com/sliemeobn/elementary.git", .upToNextMajor(from: "0.3.0")),
2727
],
2828
targets: [
2929
.target(

Sources/VaporElementary/HTMLResponse.swift

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,43 +15,53 @@ import Vapor
1515
/// }
1616
/// }
1717
/// ```
18-
public struct HTMLResponse<Content: HTML & Sendable>: Sendable {
18+
public struct HTMLResponse: Sendable {
1919
// NOTE: The Sendable requirement on Content can probably be removed in Swift 6 using a sending parameter, and some fancy ~Copyable @unchecked Sendable box type.
2020
// We only need to pass the HTML value to the response generator body closure
21-
private let content: Content
21+
private let content: any HTML & Sendable
2222

2323
/// The number of bytes to write to the response body at a time.
2424
///
2525
/// The default is 1024 bytes.
2626
public var chunkSize: Int
2727

28+
/// Response headers
29+
///
30+
/// It can be used to add additional headers to a predefined set of fields.
31+
///
32+
/// - Note: If a new set of headers is assigned, all predefined headers are removed.
33+
///
34+
/// ```swift
35+
/// var response = HTMLResponse { ... }
36+
/// response.headers.add(name: "foo", value: "bar")
37+
/// return response
38+
/// ```
39+
public var headers: HTTPHeaders = ["Content-Type": "text/html; charset=utf-8"]
40+
2841
/// Creates a new HTMLResponse
2942
///
3043
/// - Parameters:
3144
/// - chunkSize: The number of bytes to write to the response body at a time.
45+
/// - additionalHeaders: Additional headers to be merged with predefined headers.
3246
/// - content: The `HTML` content to render in the response.
33-
public init(chunkSize: Int = 1024, @HTMLBuilder content: () -> Content) {
47+
public init(chunkSize: Int = 1024, additionalHeaders: HTTPHeaders = [:], @HTMLBuilder content: () -> some HTML & Sendable) {
3448
self.chunkSize = chunkSize
49+
if additionalHeaders.contains(name: .contentType) {
50+
self.headers = additionalHeaders
51+
} else {
52+
self.headers.add(contentsOf: additionalHeaders)
53+
}
3554
self.content = content()
3655
}
3756
}
3857

3958
extension HTMLResponse: AsyncResponseEncodable {
40-
struct StreamWriter: HTMLStreamWriter {
41-
var writer: any AsyncBodyStreamWriter
42-
var allocator: ByteBufferAllocator
43-
44-
func write(_ bytes: ArraySlice<UInt8>) async throws {
45-
try await self.writer.writeBuffer(self.allocator.buffer(bytes: bytes))
46-
}
47-
}
48-
4959
public func encodeResponse(for request: Request) async throws -> Response {
5060
Response(
5161
status: .ok,
52-
headers: ["Content-Type": "text/html; charset=utf-8"],
53-
body: .init(asyncStream: { [content] writer in
54-
try await content.render(into: StreamWriter(writer: writer, allocator: request.byteBufferAllocator))
62+
headers: self.headers,
63+
body: .init(asyncStream: { [content, chunkSize] writer in
64+
try await writer.writeHTML(content, chunkSize: chunkSize)
5565
try await writer.write(.end)
5666
})
5767
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Elementary
2+
import Vapor
3+
4+
struct HTMLResponseBodyStreamWriter: HTMLStreamWriter {
5+
let allocator: ByteBufferAllocator = .init()
6+
var writer: any AsyncBodyStreamWriter
7+
8+
mutating func write(_ bytes: ArraySlice<UInt8>) async throws {
9+
try await self.writer.writeBuffer(self.allocator.buffer(bytes: bytes))
10+
}
11+
}
12+
13+
public extension AsyncBodyStreamWriter {
14+
/// Writes HTML by rendering chuncks of bytes to the response body
15+
///
16+
/// - Parameters:
17+
/// - html: The HTML content to render in the response
18+
/// - chunkSize: The number of bytes to write to the response body at a time (default is 1024 bytes)
19+
func writeHTML(_ html: consuming some HTML, chunkSize: Int = 1204) async throws {
20+
try await html.render(
21+
into: HTMLResponseBodyStreamWriter(writer: self),
22+
chunkSize: chunkSize
23+
)
24+
}
25+
}

Tests/VaporElementaryTests/HTMLResponseTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,47 @@ final class HTMLResponseTests: XCTestCase {
4949
let response = try await app.sendRequest(.GET, "/")
5050
XCTAssertEqual(String(buffer: response.body), Array(repeating: "<p></p>", count: count).joined())
5151
}
52+
53+
func testRespondsWithCustomHeaders() async throws {
54+
self.app.get { _ in
55+
var response = HTMLResponse(additionalHeaders: ["foo": "bar"]) { EmptyHTML() }
56+
response.headers.add(name: "hx-refresh", value: "true")
57+
return response
58+
}
59+
60+
let response = try await app.sendRequest(.GET, "/")
61+
62+
XCTAssertEqual(response.headers["foo"], ["bar"])
63+
XCTAssertEqual(response.headers["hx-refresh"], ["true"])
64+
XCTAssertEqual(response.headers.contentType?.description, "text/html; charset=utf-8")
65+
}
66+
67+
func testRespondsWithOverwrittenContentType() async throws {
68+
self.app.get { _ in
69+
HTMLResponse(additionalHeaders: ["Content-Type": "some"]) { EmptyHTML() }
70+
}
71+
72+
let response = try await app.sendRequest(.GET, "/")
73+
74+
XCTAssertEqual(response.headers["Content-Type"], ["some"])
75+
}
76+
77+
func testRespondsByWritingToStream() async throws {
78+
self.app.get { _ in
79+
Response(
80+
status: .ok,
81+
headers: [:],
82+
body: .init(asyncStream: { writer in
83+
try await writer.writeHTML(p { "Hello" })
84+
try await writer.write(.end)
85+
})
86+
)
87+
}
88+
89+
let response = try await app.sendRequest(.GET, "/")
90+
91+
XCTAssertEqual(String(buffer: response.body), "<p>Hello</p>")
92+
}
5293
}
5394

5495
struct TestPage: HTMLDocument {

0 commit comments

Comments
 (0)