feat: Begin using Tagged types
All checks were successful
CI / Linux Tests (push) Successful in 5m23s

This commit is contained in:
2026-01-29 17:10:35 -05:00
parent 18a5ef06d3
commit 9379774fae
22 changed files with 167 additions and 150 deletions

View File

@@ -31,7 +31,7 @@ struct DependenciesMiddleware: AsyncMiddleware {
try await values.yield {
try await withDependencies {
$0.apiController = apiController
$0.authClient = .live(on: request)
$0.auth = .live(on: request)
$0.database = database
// $0.dateFormatter = .liveValue
$0.viewController = viewController

View File

@@ -5,17 +5,23 @@ import ManualDCore
import Vapor
extension DependencyValues {
public var authClient: AuthClient {
/// Authentication dependency, for handling authentication tasks.
public var auth: AuthClient {
get { self[AuthClient.self] }
set { self[AuthClient.self] = newValue }
}
}
/// Represents authentication tasks that are used in the application.
@DependencyClient
public struct AuthClient: Sendable {
/// Create a new user and log them in.
public var createAndLogin: @Sendable (User.Create) async throws -> User
/// Get the current user.
public var currentUser: @Sendable () throws -> User
/// Login a user.
public var login: @Sendable (User.Login) async throws -> User
/// Logout a user.
public var logout: @Sendable () throws -> Void
}

View File

@@ -4,22 +4,33 @@ import FluentKit
import ManualDCore
extension DependencyValues {
/// The database dependency.
public var database: DatabaseClient {
get { self[DatabaseClient.self] }
set { self[DatabaseClient.self] = newValue }
}
}
/// Represents the database interactions used by the application.
@DependencyClient
public struct DatabaseClient: Sendable {
/// Database migrations.
public var migrations: Migrations
/// Interactions with the projects table.
public var projects: Projects
/// Interactions with the rooms table.
public var rooms: Rooms
/// Interactions with the equipment table.
public var equipment: Equipment
/// Interactions with the component losses table.
public var componentLosses: ComponentLosses
/// Interactions with the equivalent lengths table.
public var equivalentLengths: EquivalentLengths
/// Interactions with the users table.
public var users: Users
/// Interactions with the user profiles table.
public var userProfiles: UserProfiles
/// Interactions with the trunk sizes table.
public var trunkSizes: TrunkSizes
@DependencyClient
@@ -147,23 +158,3 @@ extension DatabaseClient: TestDependencyKey {
)
}
}
extension DatabaseClient.Migrations: DependencyKey {
public static let testValue = Self()
public static let liveValue = Self(
all: {
[
Project.Migrate(),
User.Migrate(),
User.Token.Migrate(),
User.Profile.Migrate(),
ComponentPressureLoss.Migrate(),
EquipmentInfo.Migrate(),
Room.Migrate(),
EquivalentLength.Migrate(),
TrunkSize.Migrate(),
]
}
)
}

View File

@@ -0,0 +1,22 @@
import Dependencies
import ManualDCore
extension DatabaseClient.Migrations: DependencyKey {
public static let testValue = Self()
public static let liveValue = Self(
all: {
[
Project.Migrate(),
User.Migrate(),
User.Token.Migrate(),
User.Profile.Migrate(),
ComponentPressureLoss.Migrate(),
EquipmentInfo.Migrate(),
Room.Migrate(),
EquivalentLength.Migrate(),
TrunkSize.Migrate(),
]
}
)
}

View File

@@ -4,6 +4,7 @@ import Foundation
import Vapor
extension DependencyValues {
/// Dependency used for file operations.
public var fileClient: FileClient {
get { self[FileClient.self] }
set { self[FileClient.self] = newValue }
@@ -14,9 +15,27 @@ extension DependencyValues {
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
/// Write contents to a file.
///
/// > Warning: This will overwrite a file if it exists.
public var writeFile: @Sendable (_ contents: String, _ path: String) async throws -> Void
/// Remove a file.
public var removeFile: @Sendable (_ path: String) async throws -> Void
/// Stream a file.
public var streamFile:
@Sendable (_ path: String, @escaping OnCompleteHandler) async throws -> Response
/// Stream a file at the given path.
///
/// - Paramters:
/// - path: The path to the file to stream.
/// - onComplete: Completion handler to run when done streaming the file.
public func streamFile(
at path: String,
onComplete: @escaping OnCompleteHandler = {}
) async throws -> Response {
try await streamFile(path, onComplete)
}
}
extension FileClient: TestDependencyKey {

View File

@@ -97,16 +97,16 @@ func roundSize(_ size: Double) throws -> Int {
}
}
func velocity(cfm: Int, roundSize: Int) -> Int {
let cfm = Double(cfm)
func velocity(cfm: CFM, roundSize: Int) -> Int {
let cfm = Double(cfm.rawValue)
let roundSize = Double(roundSize)
let velocity = cfm / (pow(roundSize / 24, 2) * 3.14)
return Int(round(velocity))
}
func flexSize(_ request: ManualDClient.DuctSizeRequest) throws -> Int {
let cfm = pow(Double(request.designCFM), 0.4)
let fr = pow(request.frictionRate / 1.76, 0.2)
func flexSize(_ cfm: CFM, _ frictionRate: DesignFrictionRate) throws -> Int {
let cfm = pow(Double(cfm.rawValue), 0.4)
let fr = pow(frictionRate.rawValue / 1.76, 0.2)
let size = 0.55 * (cfm / fr)
return try roundSize(size)
}

View File

@@ -4,6 +4,7 @@ import Logging
import ManualDCore
extension DependencyValues {
/// Dependency that performs manual-d duct sizing calculations.
public var manualD: ManualDClient {
get { self[ManualDClient.self] }
set { self[ManualDClient.self] = newValue }
@@ -15,12 +16,24 @@ extension DependencyValues {
///
@DependencyClient
public struct ManualDClient: Sendable {
public var ductSize: @Sendable (DuctSizeRequest) async throws -> DuctSizeResponse
public var ductSize: @Sendable (CFM, DesignFrictionRate) async throws -> DuctSizeResponse
public var frictionRate: @Sendable (FrictionRateRequest) async throws -> FrictionRate
public var totalEquivalentLength: @Sendable (TotalEquivalentLengthRequest) async throws -> Int
public var rectangularSize:
@Sendable (RectangularSizeRequest) async throws -> RectangularSizeResponse
public func ductSize(
cfm designCFM: Int,
frictionRate designFrictionRate: Double
) async throws -> DuctSizeResponse {
try await ductSize(.init(rawValue: designCFM), .init(rawValue: designFrictionRate))
}
public func ductSize(
cfm designCFM: Double,
frictionRate designFrictionRate: Double
) async throws -> DuctSizeResponse {
try await ductSize(.init(rawValue: Int(designCFM)), .init(rawValue: designFrictionRate))
}
}
extension ManualDClient: TestDependencyKey {
@@ -29,19 +42,6 @@ extension ManualDClient: TestDependencyKey {
extension ManualDClient {
public struct DuctSizeRequest: Codable, Equatable, Sendable {
public let designCFM: Int
public let frictionRate: Double
public init(
designCFM: Int,
frictionRate: Double
) {
self.designCFM = designCFM
self.frictionRate = frictionRate
}
}
public struct DuctSizeResponse: Codable, Equatable, Sendable {
public let calculatedSize: Double
@@ -82,31 +82,14 @@ extension ManualDClient {
public struct FrictionRateResponse: Codable, Equatable, Sendable {
public let availableStaticPressure: Double
public let frictionRate: Double
public let frictionRate: DesignFrictionRate
public init(availableStaticPressure: Double, frictionRate: Double) {
public init(availableStaticPressure: Double, frictionRate: DesignFrictionRate) {
self.availableStaticPressure = availableStaticPressure
self.frictionRate = frictionRate
}
}
public struct TotalEquivalentLengthRequest: Codable, Equatable, Sendable {
public let trunkLengths: [Int]
public let runoutLengths: [Int]
public let effectiveLengthGroups: [EffectiveLengthGroup]
public init(
trunkLengths: [Int],
runoutLengths: [Int],
effectiveLengthGroups: [EffectiveLengthGroup]
) {
self.trunkLengths = trunkLengths
self.runoutLengths = runoutLengths
self.effectiveLengthGroups = effectiveLengthGroups
}
}
public struct RectangularSizeRequest: Codable, Equatable, Sendable {
public let roundSize: Int
public let height: Int

View File

@@ -4,19 +4,19 @@ import ManualDCore
extension ManualDClient: DependencyKey {
public static let liveValue: Self = .init(
ductSize: { request in
guard request.designCFM > 0 else {
ductSize: { cfm, frictionRate in
guard cfm > 0 else {
throw ManualDError(message: "Design CFM should be greater than 0.")
}
let fr = pow(request.frictionRate, 0.5)
let ductulatorSize = pow(Double(request.designCFM) / (3.12 * fr), 0.38)
let fr = pow(frictionRate.rawValue, 0.5)
let ductulatorSize = pow(Double(cfm.rawValue) / (3.12 * fr), 0.38)
let finalSize = try roundSize(ductulatorSize)
let flexSize = try flexSize(request)
let flexSize = try flexSize(cfm, frictionRate)
return .init(
calculatedSize: ductulatorSize,
finalSize: finalSize,
flexSize: flexSize,
velocity: velocity(cfm: request.designCFM, roundSize: finalSize)
velocity: velocity(cfm: cfm, roundSize: finalSize)
)
},
frictionRate: { request in
@@ -28,14 +28,17 @@ extension ManualDClient: DependencyKey {
let totalComponentLosses = request.componentPressureLosses.total
let availableStaticPressure = request.externalStaticPressure - totalComponentLosses
let frictionRate = availableStaticPressure * 100.0 / Double(request.totalEffectiveLength)
return .init(availableStaticPressure: availableStaticPressure, value: frictionRate)
},
totalEquivalentLength: { request in
let trunkLengths = request.trunkLengths.reduce(0) { $0 + $1 }
let runoutLengths = request.runoutLengths.reduce(0) { $0 + $1 }
let groupLengths = request.effectiveLengthGroups.totalEffectiveLength
return trunkLengths + runoutLengths + groupLengths
return .init(
availableStaticPressure: availableStaticPressure,
value: .init(rawValue: frictionRate)
)
},
// totalEquivalentLength: { request in
// let trunkLengths = request.trunkLengths.reduce(0) { $0 + $1 }
// let runoutLengths = request.runoutLengths.reduce(0) { $0 + $1 }
// let groupLengths = request.effectiveLengthGroups.totalEffectiveLength
// return trunkLengths + runoutLengths + groupLengths
// },
rectangularSize: { request in
let width = (Double.pi * (pow(Double(request.roundSize) / 2.0, 2.0))) / Double(request.height)
return .init(height: request.height, width: Int(width.rounded(.toNearestOrEven)))

View File

@@ -1,4 +1,17 @@
import Foundation
import Tagged
extension Tagged where RawValue == Double {
public func string(digits: Int = 2) -> String {
rawValue.string(digits: digits)
}
}
extension Tagged where RawValue == Int {
public func string() -> String {
rawValue.string()
}
}
extension Double {

View File

@@ -7,13 +7,13 @@ public struct FrictionRate: Codable, Equatable, Sendable {
/// minus the ``ComponentPressureLoss``es for the project.
public let availableStaticPressure: Double
/// The calculated design friction rate value.
public let value: Double
public let value: DesignFrictionRate
/// Whether the design friction rate is within a valid range.
public var hasErrors: Bool { error != nil }
public init(
availableStaticPressure: Double,
value: Double
value: DesignFrictionRate
) {
self.availableStaticPressure = availableStaticPressure
self.value = value

View File

@@ -0,0 +1,7 @@
import Tagged
public enum CFMTag {}
public typealias CFM = Tagged<CFMTag, Int>
public enum DesignFrictionRateTag {}
public typealias DesignFrictionRate = Tagged<DesignFrictionRateTag, Double>

View File

@@ -52,7 +52,8 @@ extension ManualDClient {
let coolingCFM = coolingPercent * Double(sharedRequest.equipmentInfo.coolingCFM)
let designCFM = DuctSizes.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
let sizes = try await self.ductSize(
.init(designCFM: Int(designCFM.value), frictionRate: sharedRequest.designFrictionRate)
cfm: designCFM.value,
frictionRate: sharedRequest.designFrictionRate
)
for n in 1...room.registerCount {
@@ -111,7 +112,8 @@ extension ManualDClient {
let coolingCFM = coolingPercent * Double(sharedRequest.equipmentInfo.coolingCFM)
let designCFM = DuctSizes.DesignCFM(heating: heatingCFM, cooling: coolingCFM)
let sizes = try await self.ductSize(
.init(designCFM: Int(designCFM.value), frictionRate: sharedRequest.designFrictionRate)
cfm: designCFM.value,
frictionRate: sharedRequest.designFrictionRate
)
var width: Int? = nil
if let height = trunk.height {

View File

@@ -42,13 +42,10 @@ extension ProjectClient: DependencyKey {
request: database.makePdfRequest(projectID)
)
let response = try await fileClient.streamFile(
pdfResponse.pdfPath,
{
try await fileClient.removeFile(pdfResponse.htmlPath)
try await fileClient.removeFile(pdfResponse.pdfPath)
}
)
let response = try await fileClient.streamFile(at: 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(

View File

@@ -1,4 +1,5 @@
import Elementary
import Tagged
public struct Badge<Inner: HTML>: HTML, Sendable where Inner: Sendable {
@@ -25,4 +26,12 @@ extension Badge where Inner == Number {
public init(number: Double, digits: Int = 2) {
self.inner = Number(number, digits: digits)
}
public init<T>(number: Tagged<T, Int>) {
self.inner = Number(number.rawValue)
}
public init<T>(number: Tagged<T, Double>, digits: Int = 2) {
self.inner = Number(number.rawValue, digits: digits)
}
}

View File

@@ -54,14 +54,14 @@ extension ViewController: DependencyKey {
extension ViewController.Request {
func currentUser() throws -> User {
@Dependency(\.authClient.currentUser) var currentUser
@Dependency(\.auth.currentUser) var currentUser
return try currentUser()
}
func authenticate(
_ login: User.Login
) async throws -> User {
@Dependency(\.authClient) var auth
@Dependency(\.auth) var auth
return try await auth.login(login)
}
@@ -69,7 +69,7 @@ extension ViewController.Request {
func createAndAuthenticate(
_ signup: User.Create
) async throws -> User {
@Dependency(\.authClient) var auth
@Dependency(\.auth) var auth
return try await auth.createAndLogin(signup)
}
}