import Dependencies import DependenciesMacros import Elementary import EnvClient import FileClient import Foundation 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 } } } @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) } } extension PdfClient: DependencyKey { public static let testValue = Self() public static let liveValue = Self( html: { request in request.toHTML() }, generatePdf: { projectID, html in @Dependency(\.fileClient) var fileClient @Dependency(\.env) var env let envVars = try env() let baseUrl = "/tmp/\(projectID)" try await fileClient.writeFile(html.render(), "\(baseUrl).html") let process = Process() let standardInput = Pipe() let standardOutput = Pipe() process.standardInput = standardInput process.standardOutput = standardOutput process.executableURL = URL(fileURLWithPath: envVars.pandocPath) process.arguments = [ "\(baseUrl).html", "--pdf-engine=\(envVars.pdfEngine)", "--from=html", "--css=Public/css/pdf.css", "--output=\(baseUrl).pdf", ] try process.run() process.waitUntilExit() return .init(htmlPath: "\(baseUrl).html", pdfPath: "\(baseUrl).pdf") } ) } 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 public let rooms: [Room] public let componentLosses: [ComponentPressureLoss] public let ductSizes: DuctSizes public let equipmentInfo: EquipmentInfo public let maxSupplyTEL: EffectiveLength public let maxReturnTEL: EffectiveLength public let frictionRate: FrictionRate public let projectSHR: Double var totalEquivalentLength: Double { maxReturnTEL.totalEquivalentLength + maxSupplyTEL.totalEquivalentLength } public init( project: Project, rooms: [Room], componentLosses: [ComponentPressureLoss], ductSizes: DuctSizes, equipmentInfo: EquipmentInfo, maxSupplyTEL: EffectiveLength, maxReturnTEL: EffectiveLength, frictionRate: FrictionRate, projectSHR: Double ) { self.project = project self.rooms = rooms self.componentLosses = componentLosses self.ductSizes = ductSizes self.equipmentInfo = equipmentInfo self.maxSupplyTEL = maxSupplyTEL self.maxReturnTEL = maxReturnTEL self.frictionRate = frictionRate self.projectSHR = projectSHR } } public struct Response: Equatable, Sendable { public let htmlPath: String public let pdfPath: String public init(htmlPath: String, pdfPath: String) { self.htmlPath = htmlPath self.pdfPath = pdfPath } } } #if DEBUG extension PdfClient.Request { public static func mock(project: Project = .mock) -> Self { let rooms = Room.mock(projectID: project.id) let trunks = TrunkSize.mock(projectID: project.id, rooms: rooms) let equipmentInfo = EquipmentInfo.mock(projectID: project.id) let equivalentLengths = EffectiveLength.mock(projectID: project.id) return .init( project: project, rooms: rooms, componentLosses: ComponentPressureLoss.mock(projectID: project.id), ductSizes: .mock(equipmentInfo: equipmentInfo, rooms: rooms, trunks: trunks), equipmentInfo: equipmentInfo, maxSupplyTEL: equivalentLengths.first { $0.type == .supply }!, maxReturnTEL: equivalentLengths.first { $0.type == .return }!, frictionRate: .mock, projectSHR: 0.83 ) } } #endif