diff --git a/Package.swift b/Package.swift
index 625543d..45082d0 100644
--- a/Package.swift
+++ b/Package.swift
@@ -93,6 +93,7 @@ let package = Package(
dependencies: [
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"),
+ .product(name: "Vapor", package: "vapor"),
]
),
.target(
@@ -113,6 +114,18 @@ let package = Package(
.product(name: "Elementary", package: "elementary"),
]
),
+ .testTarget(
+ name: "PdfClientTests",
+ dependencies: [
+ .target(name: "HTMLSnapshotTesting"),
+ .target(name: "PdfClient"),
+ .product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
+ ]
+ // ,
+ // resources: [
+ // .copy("__Snapshots__")
+ // ]
+ ),
.target(
name: "ProjectClient",
dependencies: [
diff --git a/Sources/App/Middleware/DependenciesMiddleware.swift b/Sources/App/Middleware/DependenciesMiddleware.swift
index 4ef42a9..8a16843 100644
--- a/Sources/App/Middleware/DependenciesMiddleware.swift
+++ b/Sources/App/Middleware/DependenciesMiddleware.swift
@@ -36,6 +36,7 @@ struct DependenciesMiddleware: AsyncMiddleware {
// $0.dateFormatter = .liveValue
$0.viewController = viewController
$0.pdfClient = .liveValue
+ $0.fileClient = .live(fileIO: request.fileio)
} operation: {
try await next.respond(to: request)
}
diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift
index 941a74c..6bc6232 100644
--- a/Sources/App/configure.swift
+++ b/Sources/App/configure.swift
@@ -114,43 +114,6 @@ extension SiteRoute {
extension DuctSizes: Content {}
-// FIX: Move
-func handlePdf(_ projectID: Project.ID, on request: Request) async throws -> Response {
- @Dependency(\.projectClient) var projectClient
- return try await projectClient.generatePdf(projectID, request.fileio)
-
- // let html = try await projectClient.toHTML(projectID)
- // let url = "/tmp/\(projectID)"
- // try await request.fileio.writeFile(.init(string: html.render()), at: "\(url).html")
- //
- // let process = Process()
- // let standardInput = Pipe()
- // let standardOutput = Pipe()
- // process.standardInput = standardInput
- // process.standardOutput = standardOutput
- // process.executableURL = URL(fileURLWithPath: "/bin/pandoc")
- // process.arguments = [
- // "\(url).html",
- // "--pdf-engine=weasyprint",
- // "--from=html",
- // "--css=Public/css/pdf.css",
- // "-o", "\(url).pdf",
- // ]
- // try process.run()
- // process.waitUntilExit()
- //
- // let response = try await request.fileio.asyncStreamFile(at: "\(url).pdf", mediaType: .pdf) { _ in
- // // Remove files here.
- // try FileManager.default.removeItem(atPath: "\(url).pdf")
- // try FileManager.default.removeItem(atPath: "\(url).html")
- // }
- // response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream")
- // response.headers.replaceOrAdd(
- // name: .contentDisposition, value: "attachment; filename=Duct-Calc.pdf"
- // )
- // return response
-}
-
@Sendable
private func siteHandler(
request: Request,
@@ -165,9 +128,10 @@ private func siteHandler(
return try await apiController.respond(route, request: request)
case .health:
return HTTPStatus.ok
- // FIX: Move
+ // Generating a pdf return's a `Response` instead of `HTML` like other views, so we
+ // need to handle it seperately.
case .view(.project(.detail(let projectID, .pdf))):
- return try await handlePdf(projectID, on: request)
+ return try await projectClient.generatePdf(projectID)
case .view(let route):
return try await viewController.respond(route: route, request: request)
}
diff --git a/Sources/FileClient/Interface.swift b/Sources/FileClient/Interface.swift
index f29ff42..359f8ea 100644
--- a/Sources/FileClient/Interface.swift
+++ b/Sources/FileClient/Interface.swift
@@ -1,6 +1,7 @@
import Dependencies
import DependenciesMacros
import Foundation
+import Vapor
extension DependencyValues {
public var fileClient: FileClient {
@@ -11,19 +12,29 @@ extension DependencyValues {
@DependencyClient
public struct FileClient: Sendable {
+ public typealias OnCompleteHandler = @Sendable () async throws -> Void
+
public var writeFile: @Sendable (String, String) async throws -> Void
public var removeFile: @Sendable (String) async throws -> Void
+ public var streamFile: @Sendable (String, @escaping OnCompleteHandler) async throws -> Response
}
-extension FileClient: DependencyKey {
+extension FileClient: TestDependencyKey {
public static let testValue = Self()
- public static let liveValue = Self(
- writeFile: { contents, path in
- try contents.write(to: URL(fileURLWithPath: path), atomically: true, encoding: .utf8)
- },
- removeFile: { path in
- try FileManager.default.removeItem(atPath: path)
- }
- )
+ public static func live(fileIO: FileIO) -> Self {
+ .init(
+ writeFile: { contents, path in
+ try await fileIO.writeFile(ByteBuffer(string: contents), at: path)
+ },
+ removeFile: { path in
+ try FileManager.default.removeItem(atPath: path)
+ },
+ streamFile: { path, onComplete in
+ try await fileIO.asyncStreamFile(at: path) { _ in
+ try await onComplete()
+ }
+ }
+ )
+ }
}
diff --git a/Sources/PdfClient/Interface.swift b/Sources/PdfClient/Interface.swift
index 008e881..6aa0b05 100644
--- a/Sources/PdfClient/Interface.swift
+++ b/Sources/PdfClient/Interface.swift
@@ -8,6 +8,8 @@ import ManualDCore
extension DependencyValues {
+ /// Access the pdf client dependency that can be used to generate pdf's for
+ /// a project.
public var pdfClient: PdfClient {
get { self[PdfClient.self] }
set { self[PdfClient.self] = newValue }
@@ -16,9 +18,19 @@ extension DependencyValues {
@DependencyClient
public struct PdfClient: Sendable {
+ /// Generate the html used to convert to pdf for a project.
public var html: @Sendable (Request) async throws -> (any HTML & Sendable)
+
+ /// Converts the generated html to a pdf.
+ ///
+ /// **NOTE:** This is generally not used directly, instead use the overload that accepts a request,
+ /// which generates the html and does the conversion all in one step.
public var generatePdf: @Sendable (Project.ID, any HTML & Sendable) async throws -> Response
+ /// Generate a pdf for the given project request.
+ ///
+ /// - Parameters:
+ /// - request: The project data used to generate the pdf.
public func generatePdf(request: Request) async throws -> Response {
let html = try await self.html(request)
return try await self.generatePdf(request.project.id, html)
@@ -64,7 +76,7 @@ extension PdfClient: DependencyKey {
}
extension PdfClient {
-
+ /// Container for the data required to generate a pdf for a given project.
public struct Request: Codable, Equatable, Sendable {
public let project: Project
diff --git a/Sources/ProjectClient/Interface.swift b/Sources/ProjectClient/Interface.swift
index 5ba9cb9..47fd81a 100644
--- a/Sources/ProjectClient/Interface.swift
+++ b/Sources/ProjectClient/Interface.swift
@@ -28,12 +28,7 @@ public struct ProjectClient: Sendable {
@Sendable (User.ID, Project.Create) async throws -> CreateProjectResponse
public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse
-
- // FIX: Name to something to do with generating a pdf, just experimenting now.
- // public var toMarkdown: @Sendable (Project.ID) async throws -> String
- // public var toHTML: @Sendable (Project.ID) async throws -> (any HTML & Sendable)
-
- public var generatePdf: @Sendable (Project.ID, FileIO) async throws -> Response
+ public var generatePdf: @Sendable (Project.ID) async throws -> Response
}
extension ProjectClient: TestDependencyKey {
diff --git a/Sources/ProjectClient/Live.swift b/Sources/ProjectClient/Live.swift
index fbb0e58..9d3b29c 100644
--- a/Sources/ProjectClient/Live.swift
+++ b/Sources/ProjectClient/Live.swift
@@ -37,14 +37,18 @@ extension ProjectClient: DependencyKey {
frictionRate: { projectID in
try await manualD.frictionRate(projectID: projectID)
},
- generatePdf: { projectID, fileIO in
+ generatePdf: { projectID in
let pdfResponse = try await pdfClient.generatePdf(
- request: database.makePdfRequest(projectID))
+ request: database.makePdfRequest(projectID)
+ )
- let response = try await fileIO.asyncStreamFile(at: pdfResponse.pdfPath) { _ in
- try await fileClient.removeFile(pdfResponse.htmlPath)
- try await fileClient.removeFile(pdfResponse.pdfPath)
- }
+ let response = try await fileClient.streamFile(
+ pdfResponse.pdfPath,
+ {
+ try await fileClient.removeFile(pdfResponse.htmlPath)
+ try await fileClient.removeFile(pdfResponse.pdfPath)
+ }
+ )
response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream")
response.headers.replaceOrAdd(
diff --git a/Tests/PdfClientTests/PdfClientTests.swift b/Tests/PdfClientTests/PdfClientTests.swift
new file mode 100644
index 0000000..7fcd0e0
--- /dev/null
+++ b/Tests/PdfClientTests/PdfClientTests.swift
@@ -0,0 +1,26 @@
+import Dependencies
+import Foundation
+import HTMLSnapshotTesting
+import PdfClient
+import SnapshotTesting
+import Testing
+
+@Suite(.snapshots(record: .missing))
+struct PdfClientTests {
+
+ @Test
+ func html() async throws {
+
+ try await withDependencies {
+ $0.pdfClient = .liveValue
+ $0.uuid = .incrementing
+ $0.date.now = Date(timeIntervalSince1970: 1_234_567_890)
+ } operation: {
+ @Dependency(\.pdfClient) var pdfClient
+
+ let html = try await pdfClient.html(.mock())
+ assertSnapshot(of: html, as: .html)
+ }
+
+ }
+}
diff --git a/Tests/PdfClientTests/__Snapshots__/PdfClientTests/html.1.html b/Tests/PdfClientTests/__Snapshots__/PdfClientTests/html.1.html
new file mode 100644
index 0000000..5a18633
--- /dev/null
+++ b/Tests/PdfClientTests/__Snapshots__/PdfClientTests/html.1.html
@@ -0,0 +1,583 @@
+
+
+
+ Duct Calc
+
+
+
+
+
Project
+
+
+
+
+ | Name |
+ Testy McTestface |
+
+
+ | Address |
+
+
+ 1234 Sesame Street
+
+ Monroe, OH 55555
+
+ |
+
+
+
+
+
+
+
+
Equipment
+ Friction Rate
+
+
+
+
+
+
+
+ | Equipment |
+ Value |
+
+
+
+
+ | Static Pressure |
+ 0.5 |
+
+
+ | Heating CFM |
+ 900 |
+
+
+ | Cooling CFM |
+ 1,000 |
+
+
+
+
+
+
+
+
+ | Friction Rate |
+ Value |
+
+
+
+
+ | evaporator-coil |
+ 0.2 |
+
+
+ | filter |
+ 0.1 |
+
+
+ | supply-outlet |
+ 0.03 |
+
+
+ | return-grille |
+ 0.03 |
+
+
+ | balancing-damper |
+ 0.03 |
+
+
+
+
+
+
+
+
+
Duct Sizes
+
+
+
+ | Name |
+ Dsn CFM |
+ Round Size |
+ Velocity |
+ Final Size |
+ Flex Size |
+ Height |
+ Width |
+
+
+
+
+ | Bed-1 |
+ 92 |
+ 7 |
+ 489 |
+ 8 |
+ 8 |
+ |
+ |
+
+
+ | Entry |
+ 88 |
+ 7 |
+ 489 |
+ 8 |
+ 8 |
+ |
+ |
+
+
+ | Entry |
+ 88 |
+ 7 |
+ 489 |
+ 8 |
+ 8 |
+ |
+ |
+
+
+ | Family Room |
+ 92 |
+ 7 |
+ 489 |
+ 8 |
+ 8 |
+ |
+ |
+
+
+ | Family Room |
+ 92 |
+ 7 |
+ 489 |
+ 8 |
+ 8 |
+ |
+ |
+
+
+ | Family Room |
+ 92 |
+ 7 |
+ 489 |
+ 8 |
+ 8 |
+ |
+ |
+
+
+ | Kitchen |
+ 95 |
+ 7 |
+ 489 |
+ 8 |
+ 8 |
+ |
+ |
+
+
+ | Kitchen |
+ 95 |
+ 7 |
+ 489 |
+ 8 |
+ 8 |
+ |
+ |
+
+
+ | Living Room |
+ 127 |
+ 7 |
+ 489 |
+ 8 |
+ 8 |
+ |
+ |
+
+
+ | Living Room |
+ 127 |
+ 7 |
+ 489 |
+ 8 |
+ 8 |
+ |
+ |
+
+
+ | Master |
+ 87 |
+ 7 |
+ 489 |
+ 8 |
+ 8 |
+ |
+ |
+
+
+ | Master |
+ 87 |
+ 7 |
+ 489 |
+ 8 |
+ 8 |
+ |
+ |
+
+
+
+
+
+
Supply Trunk / Run Outs
+
+
+
+ | Name |
+ Dsn CFM |
+ Round Size |
+ Velocity |
+ Final Size |
+ Flex Size |
+ Height |
+ Width |
+
+
+
+
+ |
+ 1,000 |
+ 18 |
+ 987 |
+ 20 |
+ 20 |
+ |
+ |
+
+
+
+
+
+
Return Trunk / Run Outs
+
+
+
+ | Name |
+ Dsn CFM |
+ Round Size |
+ Velocity |
+ Final Size |
+ Flex Size |
+ Height |
+ Width |
+
+
+
+
+ |
+ 1,000 |
+ 18 |
+ 987 |
+ 20 |
+ 20 |
+ |
+ |
+
+
+
+
+
+
Total Equivalent Lengths
+
+
+
+ | Name |
+ Type |
+ Straight Lengths |
+ Groups |
+ Total |
+
+
+
+
+ | Supply - 1 |
+ supply |
+
+
+ |
+
+
+
+
+
+
+
+ | 1-a |
+ 20 |
+ 1 |
+ 20 |
+
+
+ | 2-b |
+ 30 |
+ 1 |
+ 30 |
+
+
+ | 3-a |
+ 10 |
+ 1 |
+ 10 |
+
+
+ | 12-a |
+ 10 |
+ 1 |
+ 10 |
+
+
+
+ |
+ 105 |
+
+
+ | Return - 1 |
+ return |
+
+
+ |
+
+
+
+
+
+
+
+ | 5-a |
+ 10 |
+ 1 |
+ 10 |
+
+
+ | 6-a |
+ 15 |
+ 1 |
+ 15 |
+
+
+ | 7-a |
+ 20 |
+ 1 |
+ 20 |
+
+
+
+ |
+ 80 |
+
+
+
+
+
+
Register Detail
+
+
+
+ | Name |
+ Heating BTU |
+ Cooling BTU |
+ Heating CFM |
+ Cooling CFM |
+ Design CFM |
+
+
+
+
+ | Bed-1 |
+ 3,913 |
+ 2,472 |
+ 83 |
+ 92 |
+ 92 |
+
+
+ | Entry |
+ 4,142 |
+ 1,458 |
+ 88 |
+ 54 |
+ 88 |
+
+
+ | Entry |
+ 4,142 |
+ 1,458 |
+ 88 |
+ 54 |
+ 88 |
+
+
+ | Family Room |
+ 3,262 |
+ 2,482 |
+ 69 |
+ 92 |
+ 92 |
+
+
+ | Family Room |
+ 3,262 |
+ 2,482 |
+ 69 |
+ 92 |
+ 92 |
+
+
+ | Family Room |
+ 3,262 |
+ 2,482 |
+ 69 |
+ 92 |
+ 92 |
+
+
+ | Kitchen |
+ 2,259 |
+ 2,548 |
+ 48 |
+ 95 |
+ 95 |
+
+
+ | Kitchen |
+ 2,259 |
+ 2,548 |
+ 48 |
+ 95 |
+ 95 |
+
+
+ | Living Room |
+ 3,776 |
+ 3,414 |
+ 80 |
+ 127 |
+ 127 |
+
+
+ | Living Room |
+ 3,776 |
+ 3,414 |
+ 80 |
+ 127 |
+ 127 |
+
+
+ | Master |
+ 4,101 |
+ 1,038 |
+ 87 |
+ 39 |
+ 87 |
+
+
+ | Master |
+ 4,101 |
+ 1,038 |
+ 87 |
+ 39 |
+ 87 |
+
+
+
+
+
+
Room Detail
+
+
+
+ | Name |
+ Heating BTU |
+ Cooling Total BTU |
+ Cooling Sensible BTU |
+ Register Count |
+
+
+
+
+ | Bed-1 |
+ 3,913 |
+ 2,472 |
+ 2,052 |
+ 1 |
+
+
+ | Entry |
+ 8,284 |
+ 2,916 |
+ 2,420 |
+ 2 |
+
+
+ | Family Room |
+ 9,785 |
+ 7,446 |
+ 6,180 |
+ 3 |
+
+
+ | Kitchen |
+ 4,518 |
+ 5,096 |
+ 4,230 |
+ 2 |
+
+
+ | Living Room |
+ 7,553 |
+ 6,829 |
+ 5,668 |
+ 2 |
+
+
+ | Master |
+ 8,202 |
+ 2,076 |
+ 1,723 |
+ 2 |
+
+
+ | Totals |
+ 42,255 |
+ 26,835 |
+ 22,273 |
+ |
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tests/ViewControllerTests/ViewControllerTests.swift b/Tests/ViewControllerTests/ViewControllerTests.swift
index 6626c23..bc29891 100644
--- a/Tests/ViewControllerTests/ViewControllerTests.swift
+++ b/Tests/ViewControllerTests/ViewControllerTests.swift
@@ -11,7 +11,7 @@ import SnapshotTesting
import Testing
import ViewController
-@Suite(.snapshots(record: .missing))
+@Suite(.snapshots(record: .failed))
struct ViewControllerTests {
@Test
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/login.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/login.1.html
index c044deb..a6dc2a8 100644
--- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/login.1.html
+++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/login.1.html
@@ -16,8 +16,10 @@
+
+
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.1.html
index 4a98913..ca19d9c 100644
--- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.1.html
+++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.1.html
@@ -16,8 +16,10 @@
+
+
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.2.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.2.html
index e0d851c..12a954d 100644
--- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.2.html
+++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.2.html
@@ -16,8 +16,10 @@
+
+
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.3.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.3.html
index 7ba7bc4..0513085 100644
--- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.3.html
+++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.3.html
@@ -16,8 +16,10 @@
+
+
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.4.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.4.html
index 694d334..443518e 100644
--- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.4.html
+++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.4.html
@@ -16,8 +16,10 @@
+
+
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.5.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.5.html
index e0ce9c0..4bf598b 100644
--- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.5.html
+++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.5.html
@@ -16,8 +16,10 @@
+
+
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.6.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.6.html
index a579ecf..0013fdf 100644
--- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.6.html
+++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.6.html
@@ -16,8 +16,10 @@
+
+
@@ -55,7 +57,9 @@ p-6 w-full">
Must complete all the previous sections to display duct sizing calculations.
- PDF
+
+
+
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectIndex.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectIndex.1.html
index 8f6b58a..e1f8f9f 100644
--- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectIndex.1.html
+++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectIndex.1.html
@@ -16,8 +16,10 @@
+
+
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/signup.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/signup.1.html
index c044deb..a6dc2a8 100644
--- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/signup.1.html
+++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/signup.1.html
@@ -16,8 +16,10 @@
+
+
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/userProfile.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/userProfile.1.html
index 6a7df06..62a38d4 100644
--- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/userProfile.1.html
+++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/userProfile.1.html
@@ -16,8 +16,10 @@
+
+