feat: Updates to use swift-validations for database.
All checks were successful
CI / Linux Tests (push) Successful in 6m28s

This commit is contained in:
2026-02-01 00:55:44 -05:00
parent a3fb87f86e
commit 9276f88426
20 changed files with 667 additions and 361 deletions

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "300df15f5af26da69cfcf959d16ee1b9eada6101dc105a17fc01ddd244d476b4", "originHash" : "ed354a8e92f6b810403986d192b495a0e8e67cc9577e8ec24bce4ba275c0513d",
"pins" : [ "pins" : [
{ {
"identity" : "async-http-client", "identity" : "async-http-client",
@@ -415,6 +415,15 @@
"version" : "0.6.2" "version" : "0.6.2"
} }
}, },
{
"identity" : "swift-validations",
"kind" : "remoteSourceControl",
"location" : "https://github.com/m-housh/swift-validations.git",
"state" : {
"revision" : "95ea5d267e37f6cdb9f91c5c8a01e718b9299db6",
"version" : "0.3.4"
}
},
{ {
"identity" : "vapor", "identity" : "vapor",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -32,6 +32,7 @@ let package = Package(
.package(url: "https://github.com/elementary-swift/elementary.git", from: "0.6.0"), .package(url: "https://github.com/elementary-swift/elementary.git", from: "0.6.0"),
.package(url: "https://github.com/elementary-swift/elementary-htmx.git", from: "0.5.0"), .package(url: "https://github.com/elementary-swift/elementary-htmx.git", from: "0.5.0"),
.package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.1.0"), .package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.1.0"),
.package(url: "https://github.com/m-housh/swift-validations.git", from: "0.1.0"),
], ],
targets: [ targets: [
.executableTarget( .executableTarget(
@@ -67,6 +68,7 @@ let package = Package(
.product(name: "DependenciesMacros", package: "swift-dependencies"), .product(name: "DependenciesMacros", package: "swift-dependencies"),
.product(name: "Fluent", package: "fluent"), .product(name: "Fluent", package: "fluent"),
.product(name: "Vapor", package: "vapor"), .product(name: "Vapor", package: "vapor"),
.product(name: "Validations", package: "swift-validations"),
] ]
), ),
.testTarget( .testTarget(

View File

@@ -0,0 +1,48 @@
import Validations
extension Validator {
static func validate<Child: Validatable>(
_ toChild: KeyPath<Value, [Child]>
)
-> Self
{
self.mapValue({ $0[keyPath: toChild] }, with: ArrayValidator())
}
static func validate<Child: Validatable>(
_ toChild: KeyPath<Value, [Child]?>
)
-> Self
{
self.mapValue({ $0[keyPath: toChild] }, with: ArrayValidator().optional())
}
}
extension Array where Element: Validatable {
static func validator() -> some Validation<Self> {
ArrayValidator<Element>()
}
}
struct ArrayValidator<Element>: Validation where Element: Validatable {
func validate(_ value: [Element]) throws {
for item in value {
try item.validate()
}
}
}
struct ForEachValidator<T, E>: Validation where T: Validation, T.Value == E {
let validator: T
init(@ValidationBuilder<E> builder: () -> T) {
self.validator = builder()
}
func validate(_ value: [E]) throws {
for item in value {
try validator.validate(item)
}
}
}

View File

@@ -4,6 +4,7 @@ import Fluent
import Foundation import Foundation
import ManualDCore import ManualDCore
import SQLKit import SQLKit
import Validations
extension DatabaseClient.ComponentLosses: TestDependencyKey { extension DatabaseClient.ComponentLosses: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()
@@ -13,8 +14,8 @@ extension DatabaseClient.ComponentLosses {
public static func live(database: any Database) -> Self { public static func live(database: any Database) -> Self {
.init( .init(
create: { request in create: { request in
let model = try request.toModel() let model = request.toModel()
try await model.save(on: database) try await model.validateAndSave(on: database)
return try model.toDTO() return try model.toDTO()
}, },
delete: { id in delete: { id in
@@ -35,13 +36,13 @@ extension DatabaseClient.ComponentLosses {
try await ComponentLossModel.find(id, on: database).map { try $0.toDTO() } try await ComponentLossModel.find(id, on: database).map { try $0.toDTO() }
}, },
update: { id, updates in update: { id, updates in
try updates.validate() // try updates.validate()
guard let model = try await ComponentLossModel.find(id, on: database) else { guard let model = try await ComponentLossModel.find(id, on: database) else {
throw NotFoundError() throw NotFoundError()
} }
model.applyUpdates(updates) model.applyUpdates(updates)
if model.hasChanges { if model.hasChanges {
try await model.save(on: database) try await model.validateAndSave(on: database)
} }
return try model.toDTO() return try model.toDTO()
} }
@@ -51,40 +52,9 @@ extension DatabaseClient.ComponentLosses {
extension ComponentPressureLoss.Create { extension ComponentPressureLoss.Create {
func toModel() throws(ValidationError) -> ComponentLossModel { func toModel() -> ComponentLossModel {
try validate()
return .init(name: name, value: value, projectID: projectID) return .init(name: name, value: value, projectID: projectID)
} }
func validate() throws(ValidationError) {
guard !name.isEmpty else {
throw ValidationError("Component loss name should not be empty.")
}
guard value > 0 else {
throw ValidationError("Component loss value should be greater than 0.")
}
guard value < 1.0 else {
throw ValidationError("Component loss value should be less than 1.0.")
}
}
}
extension ComponentPressureLoss.Update {
func validate() throws(ValidationError) {
if let name {
guard !name.isEmpty else {
throw ValidationError("Component loss name should not be empty.")
}
}
if let value {
guard value > 0 else {
throw ValidationError("Component loss value should be greater than 0.")
}
guard value < 1.0 else {
throw ValidationError("Component loss value should be less than 1.0.")
}
}
}
} }
extension ComponentPressureLoss { extension ComponentPressureLoss {
@@ -171,3 +141,19 @@ final class ComponentLossModel: Model, @unchecked Sendable {
} }
} }
} }
extension ComponentLossModel: Validatable {
var body: some Validation<ComponentLossModel> {
Validator.accumulating {
Validator.validate(\.name, with: .notEmpty())
.errorLabel("Name", inline: true)
Validator.validate(\.value) {
Double.greaterThan(0.0)
Double.lessThanOrEquals(1.0)
}
.errorLabel("Value", inline: true)
}
}
}

View File

@@ -3,6 +3,7 @@ import DependenciesMacros
import Fluent import Fluent
import Foundation import Foundation
import ManualDCore import ManualDCore
import Validations
extension DatabaseClient.Equipment: TestDependencyKey { extension DatabaseClient.Equipment: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()
@@ -10,8 +11,8 @@ extension DatabaseClient.Equipment: TestDependencyKey {
public static func live(database: any Database) -> Self { public static func live(database: any Database) -> Self {
.init( .init(
create: { request in create: { request in
let model = try request.toModel() let model = request.toModel()
try await model.save(on: database) try await model.validateAndSave(on: database)
return try model.toDTO() return try model.toDTO()
}, },
delete: { id in delete: { id in
@@ -37,10 +38,9 @@ extension DatabaseClient.Equipment: TestDependencyKey {
guard let model = try await EquipmentModel.find(id, on: database) else { guard let model = try await EquipmentModel.find(id, on: database) else {
throw NotFoundError() throw NotFoundError()
} }
try updates.validate()
model.applyUpdates(updates) model.applyUpdates(updates)
if model.hasChanges { if model.hasChanges {
try await model.save(on: database) try await model.validateAndSave(on: database)
} }
return try model.toDTO() return try model.toDTO()
} }
@@ -50,8 +50,7 @@ extension DatabaseClient.Equipment: TestDependencyKey {
extension EquipmentInfo.Create { extension EquipmentInfo.Create {
func toModel() throws(ValidationError) -> EquipmentModel { func toModel() -> EquipmentModel {
try validate()
return .init( return .init(
staticPressure: staticPressure, staticPressure: staticPressure,
heatingCFM: heatingCFM, heatingCFM: heatingCFM,
@@ -60,44 +59,6 @@ extension EquipmentInfo.Create {
) )
} }
func validate() throws(ValidationError) {
guard staticPressure >= 0 else {
throw ValidationError("Equipment info static pressure should be greater than 0.")
}
guard staticPressure <= 1.0 else {
throw ValidationError("Equipment info static pressure should be less than 1.0.")
}
guard heatingCFM >= 0 else {
throw ValidationError("Equipment info heating CFM should be greater than 0.")
}
guard coolingCFM >= 0 else {
throw ValidationError("Equipment info heating CFM should be greater than 0.")
}
}
}
extension EquipmentInfo.Update {
var hasUpdates: Bool {
staticPressure != nil || heatingCFM != nil || coolingCFM != nil
}
func validate() throws(ValidationError) {
if let staticPressure {
guard staticPressure >= 0 else {
throw ValidationError("Equipment info static pressure should be greater than 0.")
}
}
if let heatingCFM {
guard heatingCFM >= 0 else {
throw ValidationError("Equipment info heating CFM should be greater than 0.")
}
}
if let coolingCFM {
guard coolingCFM >= 0 else {
throw ValidationError("Equipment info heating CFM should be greater than 0.")
}
}
}
} }
extension EquipmentInfo { extension EquipmentInfo {
@@ -197,3 +158,22 @@ final class EquipmentModel: Model, @unchecked Sendable {
} }
} }
} }
extension EquipmentModel: Validatable {
var body: some Validation<EquipmentModel> {
Validator.accumulating {
Validator.validate(\.staticPressure) {
Double.greaterThan(0.0)
Double.lessThan(1.0)
}
.errorLabel("Static Pressure", inline: true)
Validator.validate(\.heatingCFM, with: .greaterThan(0))
.errorLabel("Heating CFM", inline: true)
Validator.validate(\.coolingCFM, with: .greaterThan(0))
.errorLabel("Cooling CFM", inline: true)
}
}
}

View File

@@ -3,6 +3,7 @@ import DependenciesMacros
import Fluent import Fluent
import Foundation import Foundation
import ManualDCore import ManualDCore
import Validations
extension DatabaseClient.EquivalentLengths: TestDependencyKey { extension DatabaseClient.EquivalentLengths: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()
@@ -11,7 +12,7 @@ extension DatabaseClient.EquivalentLengths: TestDependencyKey {
.init( .init(
create: { request in create: { request in
let model = try request.toModel() let model = try request.toModel()
try await model.save(on: database) try await model.validateAndSave(on: database)
return try model.toDTO() return try model.toDTO()
}, },
delete: { id in delete: { id in
@@ -53,7 +54,7 @@ extension DatabaseClient.EquivalentLengths: TestDependencyKey {
} }
try model.applyUpdates(updates) try model.applyUpdates(updates)
if model.hasChanges { if model.hasChanges {
try await model.save(on: database) try await model.validateAndSave(on: database)
} }
return try model.toDTO() return try model.toDTO()
} }
@@ -64,7 +65,9 @@ extension DatabaseClient.EquivalentLengths: TestDependencyKey {
extension EquivalentLength.Create { extension EquivalentLength.Create {
func toModel() throws -> EffectiveLengthModel { func toModel() throws -> EffectiveLengthModel {
try validate() if groups.count > 0 {
try [EquivalentLength.FittingGroup].validator().validate(groups)
}
return try .init( return try .init(
name: name, name: name,
type: type.rawValue, type: type.rawValue,
@@ -73,12 +76,6 @@ extension EquivalentLength.Create {
projectID: projectID projectID: projectID
) )
} }
func validate() throws(ValidationError) {
guard !name.isEmpty else {
throw ValidationError("Effective length name can not be empty.")
}
}
} }
extension EquivalentLength { extension EquivalentLength {
@@ -184,7 +181,51 @@ final class EffectiveLengthModel: Model, @unchecked Sendable {
self.straightLengths = straightLengths self.straightLengths = straightLengths
} }
if let groups = updates.groups { if let groups = updates.groups {
if groups.count > 0 {
try [EquivalentLength.FittingGroup].validator().validate(groups)
}
self.groups = try JSONEncoder().encode(groups) self.groups = try JSONEncoder().encode(groups)
} }
} }
} }
extension EffectiveLengthModel: Validatable {
var body: some Validation<EffectiveLengthModel> {
Validator.accumulating {
Validator.validate(\.name, with: .notEmpty())
.errorLabel("Name", inline: true)
Validator.validate(
\.straightLengths,
with: [Int].empty().or(
ForEachValidator {
Int.greaterThan(0)
})
)
.errorLabel("Straight Lengths", inline: true)
}
}
}
extension EquivalentLength.FittingGroup: Validatable {
public var body: some Validation<Self> {
Validator.accumulating {
Validator.validate(\.group) {
Int.greaterThanOrEquals(1)
Int.lessThanOrEquals(12)
}
.errorLabel("Group", inline: true)
Validator.validate(\.letter, with: .regex(matching: "[a-zA-Z]"))
.errorLabel("Letter", inline: true)
Validator.validate(\.value, with: .greaterThan(0))
.errorLabel("Value", inline: true)
Validator.validate(\.quantity, with: .greaterThanOrEquals(1))
.errorLabel("Quantity", inline: true)
}
}
}

View File

@@ -0,0 +1,10 @@
import Fluent
import Validations
extension Model where Self: Validations.Validatable {
func validateAndSave(on database: any Database) async throws {
try self.validate()
try await self.save(on: database)
}
}

View File

@@ -3,6 +3,7 @@ import DependenciesMacros
import Fluent import Fluent
import Foundation import Foundation
import ManualDCore import ManualDCore
import Validations
extension DatabaseClient.Projects: TestDependencyKey { extension DatabaseClient.Projects: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()
@@ -10,8 +11,8 @@ extension DatabaseClient.Projects: TestDependencyKey {
public static func live(database: any Database) -> Self { public static func live(database: any Database) -> Self {
.init( .init(
create: { userID, request in create: { userID, request in
let model = try request.toModel(userID: userID) let model = request.toModel(userID: userID)
try await model.save(on: database) try await model.validateAndSave(on: database)
return try model.toDTO() return try model.toDTO()
}, },
delete: { id in delete: { id in
@@ -81,10 +82,9 @@ extension DatabaseClient.Projects: TestDependencyKey {
guard let model = try await ProjectModel.find(id, on: database) else { guard let model = try await ProjectModel.find(id, on: database) else {
throw NotFoundError() throw NotFoundError()
} }
try updates.validate()
model.applyUpdates(updates) model.applyUpdates(updates)
if model.hasChanges { if model.hasChanges {
try await model.save(on: database) try await model.validateAndSave(on: database)
} }
return try model.toDTO() return try model.toDTO()
} }
@@ -94,8 +94,7 @@ extension DatabaseClient.Projects: TestDependencyKey {
extension Project.Create { extension Project.Create {
func toModel(userID: User.ID) throws -> ProjectModel { func toModel(userID: User.ID) -> ProjectModel {
try validate()
return .init( return .init(
name: name, name: name,
streetAddress: streetAddress, streetAddress: streetAddress,
@@ -106,70 +105,6 @@ extension Project.Create {
) )
} }
func validate() throws(ValidationError) {
guard !name.isEmpty else {
throw ValidationError("Project name should not be empty.")
}
guard !streetAddress.isEmpty else {
throw ValidationError("Project street address should not be empty.")
}
guard !city.isEmpty else {
throw ValidationError("Project city should not be empty.")
}
guard !state.isEmpty else {
throw ValidationError("Project state should not be empty.")
}
guard !zipCode.isEmpty else {
throw ValidationError("Project zipCode should not be empty.")
}
if let sensibleHeatRatio {
guard sensibleHeatRatio >= 0 else {
throw ValidationError("Project sensible heat ratio should be greater than 0.")
}
guard sensibleHeatRatio <= 1 else {
throw ValidationError("Project sensible heat ratio should be less than 1.")
}
}
}
}
extension Project.Update {
func validate() throws(ValidationError) {
if let name {
guard !name.isEmpty else {
throw ValidationError("Project name should not be empty.")
}
}
if let streetAddress {
guard !streetAddress.isEmpty else {
throw ValidationError("Project street address should not be empty.")
}
}
if let city {
guard !city.isEmpty else {
throw ValidationError("Project city should not be empty.")
}
}
if let state {
guard !state.isEmpty else {
throw ValidationError("Project state should not be empty.")
}
}
if let zipCode {
guard !zipCode.isEmpty else {
throw ValidationError("Project zipCode should not be empty.")
}
}
if let sensibleHeatRatio {
guard sensibleHeatRatio >= 0 else {
throw ValidationError("Project sensible heat ratio should be greater than 0.")
}
guard sensibleHeatRatio <= 1 else {
throw ValidationError("Project sensible heat ratio should be less than 1.")
}
}
}
} }
extension Project { extension Project {
@@ -342,3 +277,35 @@ final class ProjectModel: Model, @unchecked Sendable {
return model return model
} }
} }
extension ProjectModel: Validatable {
var body: some Validation<ProjectModel> {
Validator.accumulating {
Validator.validate(\.name, with: .notEmpty())
.errorLabel("Name", inline: true)
Validator.validate(\.streetAddress, with: .notEmpty())
.errorLabel("Address", inline: true)
Validator.validate(\.city, with: .notEmpty())
.errorLabel("City", inline: true)
Validator.validate(\.state, with: .notEmpty())
.errorLabel("State", inline: true)
Validator.validate(\.zipCode, with: .notEmpty())
.errorLabel("Zip", inline: true)
Validator.validate(\.sensibleHeatRatio) {
Validator {
Double.greaterThan(0)
Double.lessThanOrEquals(1.0)
}
.optional()
}
.errorLabel("Sensible Heat Ratio", inline: true)
}
}
}

View File

@@ -3,6 +3,7 @@ import DependenciesMacros
import Fluent import Fluent
import Foundation import Foundation
import ManualDCore import ManualDCore
import Validations
extension DatabaseClient.Rooms: TestDependencyKey { extension DatabaseClient.Rooms: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()
@@ -11,7 +12,7 @@ extension DatabaseClient.Rooms: TestDependencyKey {
.init( .init(
create: { request in create: { request in
let model = try request.toModel() let model = try request.toModel()
try await model.save(on: database) try await model.validateAndSave(on: database)
return try model.toDTO() return try model.toDTO()
}, },
delete: { id in delete: { id in
@@ -31,7 +32,7 @@ extension DatabaseClient.Rooms: TestDependencyKey {
model.rectangularSizes = nil model.rectangularSizes = nil
} }
if model.hasChanges { if model.hasChanges {
try await model.save(on: database) try await model.validateAndSave(on: database)
} }
return try model.toDTO() return try model.toDTO()
}, },
@@ -50,11 +51,9 @@ extension DatabaseClient.Rooms: TestDependencyKey {
guard let model = try await RoomModel.find(id, on: database) else { guard let model = try await RoomModel.find(id, on: database) else {
throw NotFoundError() throw NotFoundError()
} }
try updates.validate()
model.applyUpdates(updates) model.applyUpdates(updates)
if model.hasChanges { if model.hasChanges {
try await model.save(on: database) try await model.validateAndSave(on: database)
} }
return try model.toDTO() return try model.toDTO()
}, },
@@ -77,8 +76,7 @@ extension DatabaseClient.Rooms: TestDependencyKey {
extension Room.Create { extension Room.Create {
func toModel() throws(ValidationError) -> RoomModel { func toModel() throws -> RoomModel {
try validate()
return .init( return .init(
name: name, name: name,
heatingLoad: heatingLoad, heatingLoad: heatingLoad,
@@ -88,57 +86,6 @@ extension Room.Create {
projectID: projectID projectID: projectID
) )
} }
func validate() throws(ValidationError) {
guard !name.isEmpty else {
throw ValidationError("Room name should not be empty.")
}
guard heatingLoad >= 0 else {
throw ValidationError("Room heating load should not be less than 0.")
}
guard coolingTotal >= 0 else {
throw ValidationError("Room cooling total should not be less than 0.")
}
if let coolingSensible {
guard coolingSensible >= 0 else {
throw ValidationError("Room cooling sensible should not be less than 0.")
}
}
guard registerCount >= 1 else {
throw ValidationError("Room cooling sensible should not be less than 1.")
}
}
}
extension Room.Update {
func validate() throws(ValidationError) {
if let name {
guard !name.isEmpty else {
throw ValidationError("Room name should not be empty.")
}
}
if let heatingLoad {
guard heatingLoad >= 0 else {
throw ValidationError("Room heating load should not be less than 0.")
}
}
if let coolingTotal {
guard coolingTotal >= 0 else {
throw ValidationError("Room cooling total should not be less than 0.")
}
}
if let coolingSensible {
guard coolingSensible >= 0 else {
throw ValidationError("Room cooling sensible should not be less than 0.")
}
}
if let registerCount {
guard registerCount >= 1 else {
throw ValidationError("Room cooling sensible should not be less than 1.")
}
}
}
} }
extension Room { extension Room {
@@ -169,7 +116,7 @@ extension Room {
} }
} }
final class RoomModel: Model, @unchecked Sendable { final class RoomModel: Model, @unchecked Sendable, Validatable {
static let schema = "room" static let schema = "room"
@@ -267,4 +214,38 @@ final class RoomModel: Model, @unchecked Sendable {
} }
var body: some Validation<RoomModel> {
Validator.accumulating {
Validator.validate(\.name, with: .notEmpty())
.errorLabel("Name", inline: true)
Validator.validate(\.heatingLoad, with: .greaterThanOrEquals(0))
.errorLabel("Heating Load", inline: true)
Validator.validate(\.coolingTotal, with: .greaterThanOrEquals(0))
.errorLabel("Cooling Total", inline: true)
Validator.validate(\.coolingSensible, with: Double.greaterThanOrEquals(0).optional())
.errorLabel("Cooling Sensible", inline: true)
Validator.validate(\.registerCount, with: .greaterThanOrEquals(1))
.errorLabel("Register Count", inline: true)
Validator.validate(\.rectangularSizes)
}
}
}
extension Room.RectangularSize: Validatable {
public var body: some Validation<Self> {
Validator.accumulating {
Validator.validate(\.register, with: Int.greaterThanOrEquals(1).optional())
.errorLabel("Register", inline: true)
Validator.validate(\.height, with: Int.greaterThanOrEquals(1))
.errorLabel("Height", inline: true)
}
}
} }

View File

@@ -3,6 +3,7 @@ import DependenciesMacros
import Fluent import Fluent
import Foundation import Foundation
import ManualDCore import ManualDCore
import Validations
extension DatabaseClient.TrunkSizes: TestDependencyKey { extension DatabaseClient.TrunkSizes: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()
@@ -10,12 +11,12 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
public static func live(database: any Database) -> Self { public static func live(database: any Database) -> Self {
.init( .init(
create: { request in create: { request in
try request.validate() // try request.validate()
let trunk = request.toModel() let trunk = request.toModel()
var roomProxies = [TrunkSize.RoomProxy]() var roomProxies = [TrunkSize.RoomProxy]()
try await trunk.save(on: database) try await trunk.validateAndSave(on: database)
for (roomID, registers) in request.rooms { for (roomID, registers) in request.rooms {
guard let room = try await RoomModel.find(roomID, on: database) else { guard let room = try await RoomModel.find(roomID, on: database) else {
@@ -27,7 +28,7 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
registers: registers, registers: registers,
type: request.type type: request.type
) )
try await model.save(on: database) try await model.validateAndSave(on: database)
roomProxies.append( roomProxies.append(
.init(room: try room.toDTO(), registers: registers) .init(room: try room.toDTO(), registers: registers)
) )
@@ -80,7 +81,7 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
else { else {
throw NotFoundError() throw NotFoundError()
} }
try updates.validate() // try updates.validate()
try await model.applyUpdates(updates, on: database) try await model.applyUpdates(updates, on: database)
return try model.toDTO() return try model.toDTO()
} }
@@ -90,17 +91,6 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
extension TrunkSize.Create { extension TrunkSize.Create {
func validate() throws(ValidationError) {
guard rooms.count > 0 else {
throw ValidationError("Trunk size should have associated rooms / registers.")
}
if let height {
guard height > 0 else {
throw ValidationError("Trunk size height should be greater than 0.")
}
}
}
func toModel() -> TrunkModel { func toModel() -> TrunkModel {
.init( .init(
projectID: projectID, projectID: projectID,
@@ -111,21 +101,6 @@ extension TrunkSize.Create {
} }
} }
extension TrunkSize.Update {
func validate() throws(ValidationError) {
if let rooms {
guard rooms.count > 0 else {
throw ValidationError("Trunk size should have associated rooms / registers.")
}
}
if let height {
guard height > 0 else {
throw ValidationError("Trunk size height should be greater than 0.")
}
}
}
}
extension TrunkSize { extension TrunkSize {
struct Migrate: AsyncMigration { struct Migrate: AsyncMigration {
@@ -205,7 +180,17 @@ final class TrunkRoomModel: Model, @unchecked Sendable {
registers: registers registers: registers
) )
} }
}
extension TrunkRoomModel: Validatable {
var body: some Validation<TrunkRoomModel> {
Validator.validate(\.registers) {
[Int].notEmpty()
ForEachValidator {
Int.greaterThanOrEquals(1)
}
}
}
} }
final class TrunkModel: Model, @unchecked Sendable { final class TrunkModel: Model, @unchecked Sendable {
@@ -276,7 +261,7 @@ final class TrunkModel: Model, @unchecked Sendable {
self.name = name self.name = name
} }
if hasChanges { if hasChanges {
try await self.save(on: database) try await self.validateAndSave(on: database)
} }
guard let updateRooms = updates.rooms else { guard let updateRooms = updates.rooms else {
@@ -297,7 +282,7 @@ final class TrunkModel: Model, @unchecked Sendable {
currRoom.registers = registers currRoom.registers = registers
} }
if currRoom.hasChanges { if currRoom.hasChanges {
try await currRoom.save(on: database) try await currRoom.validateAndSave(on: database)
} }
} else { } else {
database.logger.debug("CREATING NEW TrunkRoomModel") database.logger.debug("CREATING NEW TrunkRoomModel")
@@ -324,6 +309,21 @@ final class TrunkModel: Model, @unchecked Sendable {
} }
} }
extension TrunkModel: Validatable {
var body: some Validation<TrunkModel> {
Validator.accumulating {
Validator.validate(\.height, with: Int.greaterThan(0).optional())
.errorLabel("Height", inline: true)
Validator.validate(\.name, with: String.notEmpty().optional())
.errorLabel("Name", inline: true)
}
}
}
extension Array where Element == TrunkModel { extension Array where Element == TrunkModel {
func toDTO() throws -> [TrunkSize] { func toDTO() throws -> [TrunkSize] {

View File

@@ -0,0 +1,20 @@
import ManualDCore
import Validations
// Declaring this in seperate file because some Vapor imports
// have same name's and this was easiest solution.
extension User.Create: Validatable {
public var body: some Validation<Self> {
Validator.accumulating {
Validator.validate(\.email, with: .email())
.errorLabel("Email", inline: true)
Validator.validate(\.password.count, with: .greaterThanOrEquals(8))
.errorLabel("Password Count", inline: true)
Validator.validate(\.confirmPassword, with: .equals(password))
.mapError(ValidationError("Confirm password does not match."))
.errorLabel("Confirm Password", inline: true)
}
}
}

View File

@@ -1,8 +1,9 @@
import Dependencies import Dependencies
import DependenciesMacros import DependenciesMacros
import Fluent import Fluent
import Foundation
import ManualDCore import ManualDCore
import Vapor import Validations
extension DatabaseClient.UserProfiles: TestDependencyKey { extension DatabaseClient.UserProfiles: TestDependencyKey {
@@ -11,9 +12,8 @@ extension DatabaseClient.UserProfiles: TestDependencyKey {
public static func live(database: any Database) -> Self { public static func live(database: any Database) -> Self {
.init( .init(
create: { profile in create: { profile in
try profile.validate()
let model = profile.toModel() let model = profile.toModel()
try await model.save(on: database) try await model.validateAndSave(on: database)
return try model.toDTO() return try model.toDTO()
}, },
delete: { id in delete: { id in
@@ -37,10 +37,9 @@ extension DatabaseClient.UserProfiles: TestDependencyKey {
guard let model = try await UserProfileModel.find(id, on: database) else { guard let model = try await UserProfileModel.find(id, on: database) else {
throw NotFoundError() throw NotFoundError()
} }
try updates.validate()
model.applyUpdates(updates) model.applyUpdates(updates)
if model.hasChanges { if model.hasChanges {
try await model.save(on: database) try await model.validateAndSave(on: database)
} }
return try model.toDTO() return try model.toDTO()
} }
@@ -50,30 +49,6 @@ extension DatabaseClient.UserProfiles: TestDependencyKey {
extension User.Profile.Create { extension User.Profile.Create {
func validate() throws(ValidationError) {
guard !firstName.isEmpty else {
throw ValidationError("User first name should not be empty.")
}
guard !lastName.isEmpty else {
throw ValidationError("User last name should not be empty.")
}
guard !companyName.isEmpty else {
throw ValidationError("User company name should not be empty.")
}
guard !streetAddress.isEmpty else {
throw ValidationError("User street address should not be empty.")
}
guard !city.isEmpty else {
throw ValidationError("User city should not be empty.")
}
guard !state.isEmpty else {
throw ValidationError("User state should not be empty.")
}
guard !zipCode.isEmpty else {
throw ValidationError("User zip code should not be empty.")
}
}
func toModel() -> UserProfileModel { func toModel() -> UserProfileModel {
.init( .init(
userID: userID, userID: userID,
@@ -89,47 +64,6 @@ extension User.Profile.Create {
} }
} }
extension User.Profile.Update {
func validate() throws(ValidationError) {
if let firstName {
guard !firstName.isEmpty else {
throw ValidationError("User first name should not be empty.")
}
}
if let lastName {
guard !lastName.isEmpty else {
throw ValidationError("User last name should not be empty.")
}
}
if let companyName {
guard !companyName.isEmpty else {
throw ValidationError("User company name should not be empty.")
}
}
if let streetAddress {
guard !streetAddress.isEmpty else {
throw ValidationError("User street address should not be empty.")
}
}
if let city {
guard !city.isEmpty else {
throw ValidationError("User city should not be empty.")
}
}
if let state {
guard !state.isEmpty else {
throw ValidationError("User state should not be empty.")
}
}
if let zipCode {
guard !zipCode.isEmpty else {
throw ValidationError("User zip code should not be empty.")
}
}
}
}
extension User.Profile { extension User.Profile {
struct Migrate: AsyncMigration { struct Migrate: AsyncMigration {
@@ -270,3 +204,31 @@ final class UserProfileModel: Model, @unchecked Sendable {
} }
} }
extension UserProfileModel: Validatable {
var body: some Validation<UserProfileModel> {
Validator.accumulating {
Validator.validate(\.firstName, with: .notEmpty())
.errorLabel("First Name", inline: true)
Validator.validate(\.lastName, with: .notEmpty())
.errorLabel("Last Name", inline: true)
Validator.validate(\.companyName, with: .notEmpty())
.errorLabel("Company", inline: true)
Validator.validate(\.streetAddress, with: .notEmpty())
.errorLabel("Address", inline: true)
Validator.validate(\.city, with: .notEmpty())
.errorLabel("City", inline: true)
Validator.validate(\.state, with: .notEmpty())
.errorLabel("State", inline: true)
Validator.validate(\.zipCode, with: .notEmpty())
.errorLabel("Zip", inline: true)
}
}
}

View File

@@ -1,6 +1,7 @@
import Dependencies import Dependencies
import DependenciesMacros import DependenciesMacros
import Fluent import Fluent
import Foundation
import ManualDCore import ManualDCore
import Vapor import Vapor
@@ -10,6 +11,7 @@ extension DatabaseClient.Users: TestDependencyKey {
public static func live(database: any Database) -> Self { public static func live(database: any Database) -> Self {
.init( .init(
create: { request in create: { request in
try request.validate()
let model = try request.toModel() let model = try request.toModel()
try await model.save(on: database) try await model.save(on: database)
return try model.toDTO() return try model.toDTO()
@@ -118,21 +120,8 @@ extension User {
extension User.Create { extension User.Create {
func toModel() throws -> UserModel { func toModel() throws -> UserModel {
try validate()
return try .init(email: email, passwordHash: User.hashPassword(password)) return try .init(email: email, passwordHash: User.hashPassword(password))
} }
func validate() throws {
guard !email.isEmpty else {
throw ValidationError("Email should not be empty")
}
guard password.count > 8 else {
throw ValidationError("Password should be more than 8 characters long.")
}
guard password == confirmPassword else {
throw ValidationError("Passwords do not match.")
}
}
} }
final class UserModel: Model, @unchecked Sendable { final class UserModel: Model, @unchecked Sendable {

View File

@@ -1,9 +1,10 @@
import DatabaseClient
import Dependencies import Dependencies
import Foundation import Foundation
import ManualDCore import ManualDCore
import Testing import Testing
@testable import DatabaseClient
@Suite @Suite
struct ComponentLossTests { struct ComponentLossTests {
@@ -50,4 +51,18 @@ struct ComponentLossTests {
} }
} }
} }
@Test(
arguments: [
ComponentLossModel(name: "", value: 0.2, projectID: UUID(0)),
ComponentLossModel(name: "Foo", value: -0.2, projectID: UUID(0)),
ComponentLossModel(name: "Foo", value: 1.2, projectID: UUID(0)),
ComponentLossModel(name: "", value: -0.2, projectID: UUID(0)),
]
)
func validations(model: ComponentLossModel) {
#expect(throws: (any Error).self) {
try model.validate()
}
}
} }

View File

@@ -1,9 +1,10 @@
import DatabaseClient
import Dependencies import Dependencies
import Foundation import Foundation
import ManualDCore import ManualDCore
import Testing import Testing
@testable import DatabaseClient
@Suite @Suite
struct EquipmentTests { struct EquipmentTests {
@@ -51,4 +52,18 @@ struct EquipmentTests {
} }
} }
@Test(
arguments: [
EquipmentModel(staticPressure: -1, heatingCFM: 1000, coolingCFM: 1000, projectID: UUID(0)),
EquipmentModel(staticPressure: 0.5, heatingCFM: -1, coolingCFM: 1000, projectID: UUID(0)),
EquipmentModel(staticPressure: 0.5, heatingCFM: 1000, coolingCFM: -1000, projectID: UUID(0)),
EquipmentModel(staticPressure: 1.1, heatingCFM: 1000, coolingCFM: -1000, projectID: UUID(0)),
]
)
func validations(model: EquipmentModel) {
#expect(throws: (any Error).self) {
try model.validate()
}
}
} }

View File

@@ -1,9 +1,10 @@
import DatabaseClient
import Dependencies import Dependencies
import Foundation import Foundation
import ManualDCore import ManualDCore
import Testing import Testing
@testable import DatabaseClient
@Suite @Suite
struct EquivalentLengthTests { struct EquivalentLengthTests {
@@ -76,4 +77,47 @@ struct EquivalentLengthTests {
} }
} }
} }
@Test(
arguments: [
EquivalentLength.Create(
projectID: UUID(0), name: "", type: .return, straightLengths: [], groups: []
),
EquivalentLength.Create(
projectID: UUID(0), name: "Testy", type: .return, straightLengths: [-1, 1], groups: []
),
EquivalentLength.Create(
projectID: UUID(0), name: "Testy", type: .return, straightLengths: [1, -1], groups: []
),
EquivalentLength.Create(
projectID: UUID(0), name: "Testy", type: .return, straightLengths: [1, 1],
groups: [
.init(group: -1, letter: "a", value: 1.0, quantity: 1)
]
),
EquivalentLength.Create(
projectID: UUID(0), name: "Testy", type: .return, straightLengths: [1, 1],
groups: [
.init(group: 1, letter: "1", value: 1.0, quantity: 1)
]
),
EquivalentLength.Create(
projectID: UUID(0), name: "Testy", type: .return, straightLengths: [1, 1],
groups: [
.init(group: 1, letter: "a", value: -1.0, quantity: 1)
]
),
EquivalentLength.Create(
projectID: UUID(0), name: "Testy", type: .return, straightLengths: [1, 1],
groups: [
.init(group: 1, letter: "a", value: 1.0, quantity: -1)
]
),
]
)
func validations(model: EquivalentLength.Create) {
#expect(throws: (any Error).self) {
try model.toModel().validate()
}
}
} }

View File

@@ -1,5 +1,4 @@
import Dependencies import Dependencies
import DependenciesTestSupport
import Fluent import Fluent
import FluentSQLiteDriver import FluentSQLiteDriver
import ManualDCore import ManualDCore
@@ -151,4 +150,55 @@ struct ProjectTests {
} }
} }
@Test(
arguments: [
ProjectModel(
name: "", streetAddress: "1234 Sesame St", city: "Nowhere", state: "OH", zipCode: "55555",
sensibleHeatRatio: nil, userID: UUID(0)
),
ProjectModel(
name: "Testy", streetAddress: "", city: "Nowhere", state: "OH", zipCode: "55555",
sensibleHeatRatio: nil, userID: UUID(0)
),
ProjectModel(
name: "Testy", streetAddress: "1234 Sesame St", city: "", state: "OH", zipCode: "55555",
sensibleHeatRatio: nil, userID: UUID(0)
),
ProjectModel(
name: "Testy", streetAddress: "1234 Sesame St", city: "Nowhere", state: "",
zipCode: "55555",
sensibleHeatRatio: nil, userID: UUID(0)
),
ProjectModel(
name: "Testy", streetAddress: "1234 Sesame St", city: "Nowhere", state: "OH",
zipCode: "",
sensibleHeatRatio: nil, userID: UUID(0)
),
ProjectModel(
name: "Testy", streetAddress: "1234 Sesame St", city: "Nowhere", state: "OH",
zipCode: "55555",
sensibleHeatRatio: -1, userID: UUID(0)
),
ProjectModel(
name: "Testy", streetAddress: "1234 Sesame St", city: "Nowhere", state: "OH",
zipCode: "55555",
sensibleHeatRatio: 1.1, userID: UUID(0)
),
]
)
func validations(model: ProjectModel) {
var errors = [String]()
#expect(throws: (any Error).self) {
do {
try model.validate()
} catch {
// Just checking to make sure I'm not testing the same error over and over /
// making sure I've reset to good values / only testing one property at a time.
#expect(!errors.contains("\(error)"))
errors.append("\(error)")
throw error
}
}
}
} }

View File

@@ -1,8 +1,10 @@
import DatabaseClient
import Dependencies import Dependencies
import Foundation import Foundation
import ManualDCore import ManualDCore
import Testing import Testing
import Validations
@testable import DatabaseClient
@Suite @Suite
struct RoomTests { struct RoomTests {
@@ -63,4 +65,124 @@ struct RoomTests {
} }
} }
} }
@Test(
arguments: [
Room.Create(
projectID: UUID(0),
name: "",
heatingLoad: 12345,
coolingTotal: 12344,
coolingSensible: nil,
registerCount: 1
),
Room.Create(
projectID: UUID(0),
name: "Test",
heatingLoad: -12345,
coolingTotal: 12344,
coolingSensible: nil,
registerCount: 1
),
Room.Create(
projectID: UUID(0),
name: "Test",
heatingLoad: 12345,
coolingTotal: -12344,
coolingSensible: nil,
registerCount: 1
),
Room.Create(
projectID: UUID(0),
name: "Test",
heatingLoad: 12345,
coolingTotal: 12344,
coolingSensible: -123,
registerCount: 1
),
Room.Create(
projectID: UUID(0),
name: "Test",
heatingLoad: 12345,
coolingTotal: 12344,
coolingSensible: nil,
registerCount: -1
),
Room.Create(
projectID: UUID(0),
name: "",
heatingLoad: -12345,
coolingTotal: -12344,
coolingSensible: -1,
registerCount: -1
),
]
)
func validations(room: Room.Create) throws {
#expect(throws: (any Error).self) {
// do {
try room.toModel().validate()
// } catch {
// print("\(error)")
// throw error
// }
}
}
// @Test(
// arguments: [
// Room.Update(
// name: "",
// heatingLoad: 12345,
// coolingTotal: 12344,
// coolingSensible: nil,
// registerCount: 1
// ),
// Room.Update(
// name: "Test",
// heatingLoad: -12345,
// coolingTotal: 12344,
// coolingSensible: nil,
// registerCount: 1
// ),
// Room.Update(
// name: "Test",
// heatingLoad: 12345,
// coolingTotal: -12344,
// coolingSensible: nil,
// registerCount: 1
// ),
// Room.Update(
// name: "Test",
// heatingLoad: 12345,
// coolingTotal: 12344,
// coolingSensible: -123,
// registerCount: 1
// ),
// Room.Update(
// name: "Test",
// heatingLoad: 12345,
// coolingTotal: 12344,
// coolingSensible: nil,
// registerCount: -1
// ),
// Room.Update(
// name: "",
// heatingLoad: -12345,
// coolingTotal: -12344,
// coolingSensible: -1,
// registerCount: -1
// ),
// ]
// )
// func updateValidations(room: Room.Update) throws {
// #expect(throws: (any Error).self) {
// // do {
// try room.validate()
// // } catch {
// // print("\(error)")
// // throw error
// // }
// }
// }
} }

View File

@@ -1,9 +1,10 @@
import DatabaseClient
import Dependencies import Dependencies
import Foundation import Foundation
import ManualDCore import ManualDCore
import Testing import Testing
@testable import DatabaseClient
@Suite @Suite
struct TrunkSizeTests { struct TrunkSizeTests {
@@ -64,4 +65,29 @@ struct TrunkSizeTests {
} }
} }
} }
@Test(
arguments: [
TrunkModel(projectID: UUID(0), type: .return, height: 8, name: ""),
TrunkModel(projectID: UUID(0), type: .return, height: -8, name: "Test"),
]
)
func validations(model: TrunkModel) {
#expect(throws: (any Error).self) {
try model.validate()
}
}
@Test(
arguments: [
TrunkRoomModel(trunkID: UUID(0), roomID: UUID(0), registers: [-1, 1], type: .return),
TrunkRoomModel(trunkID: UUID(0), roomID: UUID(0), registers: [1, -1], type: .return),
TrunkRoomModel(trunkID: UUID(0), roomID: UUID(0), registers: [], type: .return),
]
)
func trunkRoomModelValidations(model: TrunkRoomModel) {
#expect(throws: (any Error).self) {
try model.validate()
}
}
} }

View File

@@ -1,4 +1,3 @@
import DatabaseClient
import Dependencies import Dependencies
import Foundation import Foundation
import ManualDCore import ManualDCore
@@ -41,26 +40,6 @@ struct UserDatabaseTests {
} }
} }
@Test
func createUserFails() async throws {
try await withDatabase {
@Dependency(\.database.users) var users
await #expect(throws: ValidationError.self) {
try await users.create(.init(email: "", password: "", confirmPassword: ""))
}
await #expect(throws: ValidationError.self) {
try await users.create(.init(email: "testy@example.com", password: "", confirmPassword: ""))
}
await #expect(throws: ValidationError.self) {
try await users.create(
.init(email: "testy@example.com", password: "super-secret", confirmPassword: ""))
}
}
}
@Test @Test
func deleteFailsWithInvalidUserID() async throws { func deleteFailsWithInvalidUserID() async throws {
try await withDatabase { try await withDatabase {
@@ -148,4 +127,64 @@ struct UserDatabaseTests {
} }
} }
@Test(
arguments: [
UserProfileModel(
userID: UUID(0), firstName: "", lastName: "McTestface", companyName: "Acme Co.",
streetAddress: "1234 Sesame St", city: "Nowhere", state: "CA", zipCode: "55555"
),
UserProfileModel(
userID: UUID(0), firstName: "Testy", lastName: "", companyName: "Acme Co.",
streetAddress: "1234 Sesame St", city: "Nowhere", state: "CA", zipCode: "55555"
),
UserProfileModel(
userID: UUID(0), firstName: "Testy", lastName: "McTestface", companyName: "",
streetAddress: "1234 Sesame St", city: "Nowhere", state: "CA", zipCode: "55555"
),
UserProfileModel(
userID: UUID(0), firstName: "Testy", lastName: "McTestface", companyName: "Acme Co.",
streetAddress: "", city: "Nowhere", state: "CA", zipCode: "55555"
),
UserProfileModel(
userID: UUID(0), firstName: "Testy", lastName: "McTestface", companyName: "Acme Co.",
streetAddress: "1234 Sesame St", city: "", state: "CA", zipCode: "55555"
),
UserProfileModel(
userID: UUID(0), firstName: "Testy", lastName: "McTestface", companyName: "Acme Co.",
streetAddress: "1234 Sesame St", city: "Nowhere", state: "", zipCode: "55555"
),
UserProfileModel(
userID: UUID(0), firstName: "Testy", lastName: "McTestface", companyName: "Acme Co.",
streetAddress: "1234 Sesame St", city: "Nowhere", state: "CA", zipCode: ""
),
]
)
func profileValidations(model: UserProfileModel) {
var errors = [String]()
#expect(throws: (any Error).self) {
do {
try model.validate()
} catch {
// Just checking to make sure I'm not testing the same error over and over /
// making sure I've reset to good values / only testing one property at a time.
#expect(!errors.contains("\(error)"))
errors.append("\(error)")
throw error
}
}
}
@Test(
arguments: [
User.Create(email: "", password: "super-secret", confirmPassword: "super-secret"),
User.Create(email: "testy@example.com", password: "", confirmPassword: "super-secret"),
User.Create(email: "testy@example.com", password: "super-secret", confirmPassword: ""),
User.Create(email: "testy@example.com", password: "super", confirmPassword: "super"),
]
)
func userValidations(model: User.Create) {
#expect(throws: (any Error).self) {
try model.validate()
}
}
} }