feat: Begins live database client tests.
All checks were successful
CI / Linux Tests (push) Successful in 5m35s

This commit is contained in:
2026-01-30 12:02:11 -05:00
parent 4f3cc2c7ea
commit c32ffcff8c
17 changed files with 332 additions and 178 deletions

View File

@@ -1,9 +0,0 @@
import Foundation
public struct ProjectClientError: Error {
public let reason: String
public init(_ reason: String) {
self.reason = reason
}
}

View File

@@ -18,9 +18,12 @@ extension DependencyValues {
/// for the view controller to render views.
@DependencyClient
public struct ProjectClient: Sendable {
public var calculateDuctSizes: @Sendable (Project.ID) async throws -> DuctSizes
/// Calculates the room duct sizes for the given project.
public var calculateRoomDuctSizes:
@Sendable (Project.ID) async throws -> [DuctSizes.RoomContainer]
/// Calculates the trunk duct sizes for the given project.
public var calculateTrunkDuctSizes:
@Sendable (Project.ID) async throws -> [DuctSizes.TrunkContainer]
@@ -28,6 +31,14 @@ public struct ProjectClient: Sendable {
@Sendable (User.ID, Project.Create) async throws -> CreateProjectResponse
public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse
public var generatePdf: @Sendable (Project.ID) async throws -> Response
public func calculateDuctSizes(_ projectID: Project.ID) async throws -> DuctSizes {
.init(
rooms: try await calculateRoomDuctSizes(projectID),
trunks: try await calculateTrunkDuctSizes(projectID)
)
}
}
extension ProjectClient: TestDependencyKey {

View File

@@ -9,32 +9,18 @@ extension DatabaseClient {
details: Project.Detail
) async throws -> (DuctSizes, DuctSizeSharedRequest) {
let (rooms, shared) = try await calculateRoomDuctSizes(details: details)
let (trunks, _) = try await calculateTrunkDuctSizes(details: details)
return (.init(rooms: rooms, trunks: trunks), shared)
}
func calculateDuctSizes(
projectID: Project.ID
) async throws -> (DuctSizes, DuctSizeSharedRequest, [Room]) {
@Dependency(\.manualD) var manualD
let shared = try await sharedDuctRequest(projectID)
let rooms = try await rooms.fetch(projectID)
return try await (
manualD.calculateDuctSizes(
.init(
rooms: rooms,
trunks: trunkSizes.fetch(projectID),
sharedRequest: shared
trunks: calculateTrunkDuctSizes(details: details, shared: shared)
),
shared,
rooms
shared
)
}
func calculateRoomDuctSizes(
details: Project.Detail
) async throws -> ([DuctSizes.RoomContainer], DuctSizeSharedRequest) {
) async throws -> (rooms: [DuctSizes.RoomContainer], shared: DuctSizeSharedRequest) {
@Dependency(\.manualD) var manualD
let shared = try sharedDuctRequest(details: details)
@@ -42,54 +28,29 @@ extension DatabaseClient {
return (rooms, shared)
}
func calculateRoomDuctSizes(
projectID: Project.ID
) async throws -> ([DuctSizes.RoomContainer], DuctSizeSharedRequest) {
@Dependency(\.manualD) var manualD
let shared = try await sharedDuctRequest(projectID)
return try await (
manualD.calculateRoomSizes(
rooms: rooms.fetch(projectID),
sharedRequest: shared
),
shared
)
}
func calculateTrunkDuctSizes(
details: Project.Detail
) async throws -> ([DuctSizes.TrunkContainer], DuctSizeSharedRequest) {
details: Project.Detail,
shared: DuctSizeSharedRequest? = nil
) async throws -> [DuctSizes.TrunkContainer] {
@Dependency(\.manualD) var manualD
let shared = try sharedDuctRequest(details: details)
let trunks = try await manualD.calculateTrunkSizes(
let sharedRequest: DuctSizeSharedRequest
if let shared {
sharedRequest = shared
} else {
sharedRequest = try sharedDuctRequest(details: details)
}
return try await manualD.calculateTrunkSizes(
rooms: details.rooms,
trunks: details.trunks,
sharedRequest: shared
)
return (trunks, shared)
}
func calculateTrunkDuctSizes(
projectID: Project.ID
) async throws -> ([DuctSizes.TrunkContainer], DuctSizeSharedRequest) {
@Dependency(\.manualD) var manualD
let shared = try await sharedDuctRequest(projectID)
return try await (
manualD.calculateTrunkSizes(
rooms: rooms.fetch(projectID),
trunks: trunkSizes.fetch(projectID),
sharedRequest: shared
),
shared
sharedRequest: sharedRequest
)
}
func sharedDuctRequest(details: Project.Detail) throws -> DuctSizeSharedRequest {
let projectSHR = try details.project.ensuredSHR()
guard
let dfrResponse = designFrictionRate(
componentLosses: details.componentLosses,
@@ -100,10 +61,6 @@ extension DatabaseClient {
throw ProjectClientError("Project not complete.")
}
guard let projectSHR = details.project.sensibleHeatRatio else {
throw ProjectClientError("Project sensible heat ratio not set.")
}
let ensuredTEL = try dfrResponse.ensureMaxContainer()
return .init(
@@ -115,32 +72,6 @@ extension DatabaseClient {
)
}
func sharedDuctRequest(_ projectID: Project.ID) async throws -> DuctSizeSharedRequest {
guard let dfrResponse = try await designFrictionRate(projectID: projectID) else {
throw ProjectClientError("Project not complete.")
}
let ensuredTEL = try dfrResponse.ensureMaxContainer()
return try await .init(
equipmentInfo: dfrResponse.equipmentInfo,
maxSupplyLength: ensuredTEL.supply,
maxReturnLenght: ensuredTEL.return,
designFrictionRate: dfrResponse.designFrictionRate,
projectSHR: ensuredSHR(projectID)
)
}
// Fetches the project sensible heat ratio or throws an error if it's nil.
func ensuredSHR(_ projectID: Project.ID) async throws -> Double {
guard let projectSHR = try await projects.getSensibleHeatRatio(projectID) else {
throw ProjectClientError("Project sensible heat ratio not set.")
}
return projectSHR
}
// Internal container.
struct DesignFrictionRateResponse: Equatable, Sendable {
@@ -183,18 +114,13 @@ extension DatabaseClient {
}
func designFrictionRate(
projectID: Project.ID
) async throws -> DesignFrictionRateResponse? {
}
guard let equipmentInfo = try await equipment.fetch(projectID) else {
return nil
extension Project {
func ensuredSHR() throws -> Double {
guard let shr = sensibleHeatRatio else {
throw ProjectClientError("Sensible heat ratio not set on project id: \(id)")
}
return try await designFrictionRate(
componentLosses: componentLosses.fetch(projectID),
equipmentInfo: equipmentInfo,
equivalentLengths: equivalentLengths.fetchMax(projectID)
)
return shr
}
}

View File

@@ -0,0 +1,53 @@
import DatabaseClient
import Dependencies
import ManualDClient
import ManualDCore
import PdfClient
extension DatabaseClient {
/// Generate a pdf request for the given project.
func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
@Dependency(\.manualD) var manualD
guard let projectDetails = try await projects.detail(projectID) else {
throw ProjectClientError.notFound(.project(projectID))
}
let (ductSizes, shared) = try await calculateDuctSizes(details: projectDetails)
let frictionRateResponse = try await manualD.frictionRate(details: projectDetails)
guard let frictionRate = frictionRateResponse.frictionRate else {
throw ProjectClientError.notFound(.frictionRate(projectID))
}
return .init(
details: projectDetails,
ductSizes: ductSizes,
shared: shared,
frictionRate: frictionRate
)
}
}
extension PdfClient.Request {
fileprivate init(
details: Project.Detail,
ductSizes: DuctSizes,
shared: DuctSizeSharedRequest,
frictionRate: FrictionRate
) {
self.init(
project: details.project,
rooms: details.rooms,
componentLosses: details.componentLosses,
ductSizes: ductSizes,
equipmentInfo: details.equipmentInfo,
maxSupplyTEL: shared.maxSupplyLength,
maxReturnTEL: shared.maxReturnLenght,
frictionRate: frictionRate,
projectSHR: shared.projectSHR
)
}
}

View File

@@ -15,14 +15,17 @@ extension ProjectClient: DependencyKey {
@Dependency(\.fileClient) var fileClient
return .init(
calculateDuctSizes: { projectID in
try await database.calculateDuctSizes(projectID: projectID).0
},
calculateRoomDuctSizes: { projectID in
try await database.calculateRoomDuctSizes(projectID: projectID).0
guard let details = try await database.projects.detail(projectID) else {
throw ProjectClientError.notFound(.project(projectID))
}
return try await database.calculateRoomDuctSizes(details: details).rooms
},
calculateTrunkDuctSizes: { projectID in
try await database.calculateTrunkDuctSizes(projectID: projectID).0
guard let details = try await database.projects.detail(projectID) else {
throw ProjectClientError.notFound(.project(projectID))
}
return try await database.calculateTrunkDuctSizes(details: details)
},
createProject: { userID, request in
let project = try await database.projects.create(userID, request)
@@ -58,50 +61,3 @@ extension ProjectClient: DependencyKey {
}
}
extension DatabaseClient {
fileprivate func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
@Dependency(\.manualD) var manualD
guard let projectDetails = try await projects.detail(projectID) else {
throw ProjectClientError("Project not found. id: \(projectID)")
}
let (ductSizes, shared) = try await calculateDuctSizes(details: projectDetails)
let frictionRateResponse = try await manualD.frictionRate(details: projectDetails)
guard let frictionRate = frictionRateResponse.frictionRate else {
throw ProjectClientError("Friction rate not found. id: \(projectID)")
}
return .init(
details: projectDetails,
ductSizes: ductSizes,
shared: shared,
frictionRate: frictionRate
)
}
}
extension PdfClient.Request {
init(
details: Project.Detail,
ductSizes: DuctSizes,
shared: DuctSizeSharedRequest,
frictionRate: FrictionRate
) {
self.init(
project: details.project,
rooms: details.rooms,
componentLosses: details.componentLosses,
ductSizes: ductSizes,
equipmentInfo: details.equipmentInfo,
maxSupplyTEL: shared.maxSupplyLength,
maxReturnTEL: shared.maxReturnLenght,
frictionRate: frictionRate,
projectSHR: shared.projectSHR
)
}
}

View File

@@ -0,0 +1,35 @@
import Foundation
import ManualDCore
public struct ProjectClientError: Error {
public let reason: String
public init(_ reason: String) {
self.reason = reason
}
static func notFound(_ notFound: NotFound) -> Self {
.init(notFound.reason)
}
enum NotFound {
case project(Project.ID)
case frictionRate(Project.ID)
var reason: String {
switch self {
case .project(let id):
return "Project not found. id: \(id)"
case .frictionRate(let id):
return """
Friction unable to be calculated. id: \(id)
This usually means that not all the required steps have been completed.
Calculating the friction rate requires the component pressure losses to be set and
have a max equivalent length for both the supply and return.
"""
}
}
}
}