feat: WIP

This commit is contained in:
2025-03-05 07:52:47 -05:00
parent 43914716d6
commit 65f05dfc20
8 changed files with 178 additions and 179 deletions

View File

@@ -4,15 +4,14 @@ ARG SWIFT_MODE="debug"
# ================================ # ================================
# Build image # Build image
# ================================ # ================================
FROM swift:6.0-noble AS build FROM swift:6.0.3-noble AS build
ARG SWIFT_MODE ARG SWIFT_MODE
# Install OS updates # Install OS updates
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
&& apt-get -q update \ && apt-get -q update \
&& apt-get -q dist-upgrade -y \ && apt-get -q dist-upgrade -y
&& apt-get install -y libjemalloc-dev
# Set up a build area # Set up a build area
WORKDIR /build WORKDIR /build
@@ -34,8 +33,7 @@ RUN --mount=type=cache,target=/build/.build swift build \
-c ${SWIFT_MODE} \ -c ${SWIFT_MODE} \
--product App \ --product App \
--static-swift-stdlib \ --static-swift-stdlib \
-Xswiftc -g \ -Xswiftc -g
-Xlinker -ljemalloc
# Switch to the staging area # Switch to the staging area
WORKDIR /staging WORKDIR /staging

View File

@@ -4,58 +4,58 @@ import Logging
import PsychrometricClient import PsychrometricClient
import Routes import Routes
public extension DependencyValues { // public extension DependencyValues {
var apiController: ApiController { // var apiController: ApiController {
get { self[ApiController.self] } // get { self[ApiController.self] }
set { self[ApiController.self] = newValue } // set { self[ApiController.self] = newValue }
} // }
} // }
//
@DependencyClient // @DependencyClient
public struct ApiController: Sendable { // public struct ApiController: Sendable {
public var json: @Sendable (SiteRoute.Api, Logger) async throws -> (any Encodable)? // public var json: @Sendable (SiteRoute.Api, Logger) async throws -> (any Encodable)?
} // }
//
extension ApiController: TestDependencyKey { // extension ApiController: TestDependencyKey {
public static let testValue: ApiController = Self() // public static let testValue: ApiController = Self()
} // }
//
extension ApiController: DependencyKey { // extension ApiController: DependencyKey {
public static var liveValue: ApiController { // public static var liveValue: ApiController {
@Dependency(\.psychrometricClient) var psychrometricClient // @Dependency(\.psychrometricClient) var psychrometricClient
//
return .init(json: { route, logger in // return .init(json: { route, logger in
logger.debug("API Route: \(route)") // logger.debug("API Route: \(route)")
//
switch route { // switch route {
case let .calculateAtticVentilation(request): // case let .calculateAtticVentilation(request):
logger.debug("Calculating attic ventilation: \(request)") // logger.debug("Calculating attic ventilation: \(request)")
return try await request.respond(logger: logger) // return try await request.respond(logger: logger)
//
case let .calculateCapacitor(request): // case let .calculateCapacitor(request):
logger.debug("Calculating capacitor: \(request)") // logger.debug("Calculating capacitor: \(request)")
return try await request.respond(logger: logger) // return try await request.respond(logger: logger)
//
case let .calculateDehumidifierSize(request): // case let .calculateDehumidifierSize(request):
logger.debug("Calculating dehumidifier size: \(request)") // logger.debug("Calculating dehumidifier size: \(request)")
return try await request.respond(logger) // return try await request.respond(logger)
//
case let .calculateFilterPressureDrop(request): // case let .calculateFilterPressureDrop(request):
logger.debug("Calculating filter pressure drop: \(request)") // logger.debug("Calculating filter pressure drop: \(request)")
return try await request.respond(logger: logger) // return try await request.respond(logger: logger)
//
case let .calculateHVACSystemPerformance(request): // case let .calculateHVACSystemPerformance(request):
logger.debug("Calculating hvac system performance: \(request)") // logger.debug("Calculating hvac system performance: \(request)")
return try await request.respond(logger: logger) // return try await request.respond(logger: logger)
//
case let .calculateMoldRisk(request): // case let .calculateMoldRisk(request):
logger.debug("Calculating mold risk: \(request)") // logger.debug("Calculating mold risk: \(request)")
return try await psychrometricClient.respond(request, logger) // return try await psychrometricClient.respond(request, logger)
//
case let .calculateRoomPressure(request): // case let .calculateRoomPressure(request):
logger.debug("Calculating room pressure: \(request)") // logger.debug("Calculating room pressure: \(request)")
return try await request.respond(logger: logger) // return try await request.respond(logger: logger)
} // }
}) // })
} // }
} // }

View File

@@ -108,6 +108,7 @@ private extension HeatingBalancePoint.Request.Thermal {
switch buildingHeatLoss { switch buildingHeatLoss {
case let .known(btu: btu): return btu case let .known(btu: btu): return btu
case let .estimated(squareFeet: squareFeet): case let .estimated(squareFeet: squareFeet):
// TODO: Should this be 65 - designTemperature
return squareFeet * climateZone!.averageHeatLossPerSquareFoot * (70 - designTemperature) return squareFeet * climateZone!.averageHeatLossPerSquareFoot * (70 - designTemperature)
} }
} }

View File

@@ -1,35 +1,35 @@
import ApiController // import ApiController
import Routes // import Routes
import Vapor // import Vapor
//
extension ApiController { // extension ApiController {
//
func respond(_ route: SiteRoute.Api, request: Vapor.Request) async throws -> any AsyncResponseEncodable { // func respond(_ route: SiteRoute.Api, request: Vapor.Request) async throws -> any AsyncResponseEncodable {
guard let encodable = try await json(route, request.logger) else { // guard let encodable = try await json(route, request.logger) else {
return HTTPStatus.ok // return HTTPStatus.ok
} // }
return AnyJSONResponse(value: encodable) // return AnyJSONResponse(value: encodable)
} // }
} // }
//
struct AnyJSONResponse: AsyncResponseEncodable { // struct AnyJSONResponse: AsyncResponseEncodable {
public var headers: HTTPHeaders = ["Content-Type": "application/json"] // public var headers: HTTPHeaders = ["Content-Type": "application/json"]
let value: any Encodable // let value: any Encodable
//
init(additionalHeaders: HTTPHeaders = [:], value: any Encodable) { // init(additionalHeaders: HTTPHeaders = [:], value: any Encodable) {
if additionalHeaders.contains(name: .contentType) { // if additionalHeaders.contains(name: .contentType) {
self.headers = additionalHeaders // self.headers = additionalHeaders
} else { // } else {
headers.add(contentsOf: additionalHeaders) // headers.add(contentsOf: additionalHeaders)
} // }
self.value = value // self.value = value
} // }
//
func encodeResponse(for request: Request) async throws -> Response { // func encodeResponse(for request: Request) async throws -> Response {
try Response( // try Response(
status: .ok, // status: .ok,
headers: headers, // headers: headers,
body: .init(data: JSONEncoder().encode(value)) // body: .init(data: JSONEncoder().encode(value))
) // )
} // }
} // }

View File

@@ -1,4 +1,4 @@
import ApiController // import ApiController
import Dependencies import Dependencies
import PsychrometricClientLive import PsychrometricClientLive
import Vapor import Vapor
@@ -9,19 +9,19 @@ import ViewController
struct DependenciesMiddleware: AsyncMiddleware { struct DependenciesMiddleware: AsyncMiddleware {
private let values: DependencyValues.Continuation private let values: DependencyValues.Continuation
private let apiController: ApiController // private let apiController: ApiController
private let psychrometricClient: PsychrometricClient private let psychrometricClient: PsychrometricClient
// private let database: DatabaseClient // private let database: DatabaseClient
private let viewController: ViewController private let viewController: ViewController
init( init(
// database: DatabaseClient, // database: DatabaseClient,
apiController: ApiController = .liveValue, // apiController: ApiController = .liveValue,
psychrometricClient: PsychrometricClient = .liveValue, psychrometricClient: PsychrometricClient = .liveValue,
viewController: ViewController = .liveValue viewController: ViewController = .liveValue
) { ) {
self.values = withEscapedDependencies { $0 } self.values = withEscapedDependencies { $0 }
self.apiController = apiController // self.apiController = apiController
// self.database = database // self.database = database
self.psychrometricClient = psychrometricClient self.psychrometricClient = psychrometricClient
self.viewController = viewController self.viewController = viewController
@@ -30,7 +30,7 @@ struct DependenciesMiddleware: AsyncMiddleware {
func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response { func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
try await values.yield { try await values.yield {
try await withDependencies { try await withDependencies {
$0.apiController = apiController // $0.apiController = apiController
// $0.database = database // $0.database = database
// $0.dateFormatter = .liveValue // $0.dateFormatter = .liveValue
$0.psychrometricClient = psychrometricClient $0.psychrometricClient = psychrometricClient

View File

@@ -55,12 +55,12 @@ private func siteHandler(
request: Request, request: Request,
route: SiteRoute route: SiteRoute
) async throws -> any AsyncResponseEncodable { ) async throws -> any AsyncResponseEncodable {
@Dependency(\.apiController) var apiController // @Dependency(\.apiController) var apiController
@Dependency(\.viewController) var viewController @Dependency(\.viewController) var viewController
switch route { switch route {
case let .api(route): // case let .api(route):
return try await apiController.respond(route, request: request) // return try await apiController.respond(route, request: request)
case .health: case .health:
return HTTPStatus.ok return HTTPStatus.ok
case let .view(route): case let .view(route):

View File

@@ -22,8 +22,8 @@ public extension HTMLAttribute.hx {
put(SiteRoute.View.router.path(for: route)) put(SiteRoute.View.router.path(for: route))
} }
@Sendable // @Sendable
static func delete(route: SiteRoute.Api) -> HTMLAttribute { // static func delete(route: SiteRoute.Api) -> HTMLAttribute {
delete(SiteRoute.Api.router.path(for: route)) // delete(SiteRoute.Api.router.path(for: route))
} // }
} }

View File

@@ -7,14 +7,14 @@ import PsychrometricClient
// swiftlint:disable type_body_length // swiftlint:disable type_body_length
public enum SiteRoute: Equatable, Sendable { public enum SiteRoute: Equatable, Sendable {
case api(Api) // case api(Api)
case health case health
case view(View) case view(View)
public static let router = OneOf { public static let router = OneOf {
Route(.case(Self.api)) { // Route(.case(Self.api)) {
Api.router // Api.router
} // }
Route(.case(Self.health)) { Route(.case(Self.health)) {
Path { "health" } Path { "health" }
Method.get Method.get
@@ -25,74 +25,74 @@ public enum SiteRoute: Equatable, Sendable {
} }
} }
public extension SiteRoute { // public extension SiteRoute {
//
enum Api: Equatable, Sendable { // enum Api: Equatable, Sendable {
//
case calculateAtticVentilation(AtticVentilation.Request) // case calculateAtticVentilation(AtticVentilation.Request)
case calculateCapacitor(Capacitor.Request) // case calculateCapacitor(Capacitor.Request)
case calculateDehumidifierSize(DehumidifierSize.Request) // case calculateDehumidifierSize(DehumidifierSize.Request)
case calculateFilterPressureDrop(FilterPressureDrop.Request) // case calculateFilterPressureDrop(FilterPressureDrop.Request)
case calculateHVACSystemPerformance(HVACSystemPerformance.Request) // case calculateHVACSystemPerformance(HVACSystemPerformance.Request)
case calculateMoldRisk(MoldRisk.Request) // case calculateMoldRisk(MoldRisk.Request)
case calculateRoomPressure(RoomPressure.Request) // case calculateRoomPressure(RoomPressure.Request)
//
static let rootPath = Path { "api"; "v1" } // static let rootPath = Path { "api"; "v1" }
//
public static let router = OneOf { // public static let router = OneOf {
Route(.case(Self.calculateAtticVentilation)) { // Route(.case(Self.calculateAtticVentilation)) {
Path { "api"; "v1"; "calculateAtticPressure" } // Path { "api"; "v1"; "calculateAtticPressure" }
Method.post // Method.post
Body(.json(AtticVentilation.Request.self)) // Body(.json(AtticVentilation.Request.self))
} // }
Route(.case(Self.calculateCapacitor)) { // Route(.case(Self.calculateCapacitor)) {
Path { "api"; "v1"; "calculateRoomPressure" } // Path { "api"; "v1"; "calculateRoomPressure" }
Method.post // Method.post
OneOf { // OneOf {
Body(.json(Capacitor.Request.SizeRequest.self)) // Body(.json(Capacitor.Request.SizeRequest.self))
.map(.case(Capacitor.Request.size)) // .map(.case(Capacitor.Request.size))
Body(.json(Capacitor.Request.TestRequest.self)) // Body(.json(Capacitor.Request.TestRequest.self))
.map(.case(Capacitor.Request.test)) // .map(.case(Capacitor.Request.test))
} // }
} // }
Route(.case(Self.calculateDehumidifierSize)) { // Route(.case(Self.calculateDehumidifierSize)) {
Path { "api"; "v1"; "calculateDehumidifierSize" } // Path { "api"; "v1"; "calculateDehumidifierSize" }
Method.post // Method.post
Body(.json(DehumidifierSize.Request.self)) // Body(.json(DehumidifierSize.Request.self))
} // }
Route(.case(Self.calculateFilterPressureDrop)) { // Route(.case(Self.calculateFilterPressureDrop)) {
Path { "api"; "v1"; "calculateFilterPressureDrop" } // Path { "api"; "v1"; "calculateFilterPressureDrop" }
Method.post // Method.post
OneOf { // OneOf {
Body(.json(FilterPressureDrop.Request.Basic.self)) // Body(.json(FilterPressureDrop.Request.Basic.self))
.map(.case(FilterPressureDrop.Request.basic)) // .map(.case(FilterPressureDrop.Request.basic))
Body(.json(FilterPressureDrop.Request.FanLaw.self)) // Body(.json(FilterPressureDrop.Request.FanLaw.self))
.map(.case(FilterPressureDrop.Request.fanLaw)) // .map(.case(FilterPressureDrop.Request.fanLaw))
} // }
} // }
Route(.case(Self.calculateHVACSystemPerformance)) { // Route(.case(Self.calculateHVACSystemPerformance)) {
Path { "api"; "v1"; "calculateHVACSystemPerformance" } // Path { "api"; "v1"; "calculateHVACSystemPerformance" }
Method.post // Method.post
Body(.json(HVACSystemPerformance.Request.self)) // Body(.json(HVACSystemPerformance.Request.self))
} // }
Route(.case(Self.calculateMoldRisk)) { // Route(.case(Self.calculateMoldRisk)) {
Path { "api"; "v1"; "calculateMoldRisk" } // Path { "api"; "v1"; "calculateMoldRisk" }
Method.post // Method.post
Body(.json(MoldRisk.Request.self)) // Body(.json(MoldRisk.Request.self))
} // }
Route(.case(Self.calculateRoomPressure)) { // Route(.case(Self.calculateRoomPressure)) {
Path { "api"; "v1"; "calculateRoomPressure" } // Path { "api"; "v1"; "calculateRoomPressure" }
Method.post // Method.post
OneOf { // OneOf {
Body(.json(RoomPressure.Request.KnownAirflow.self)) // Body(.json(RoomPressure.Request.KnownAirflow.self))
.map(.case(RoomPressure.Request.knownAirflow)) // .map(.case(RoomPressure.Request.knownAirflow))
Body(.json(RoomPressure.Request.MeasuredPressure.self)) // Body(.json(RoomPressure.Request.MeasuredPressure.self))
.map(.case(RoomPressure.Request.measuredPressure)) // .map(.case(RoomPressure.Request.measuredPressure))
} // }
} // }
} // }
} // }
} // }
public extension SiteRoute { public extension SiteRoute {
enum View: Equatable, Sendable { enum View: Equatable, Sendable {