diff --git a/Package.resolved b/Package.resolved index defa6a5..5b1505d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "300df15f5af26da69cfcf959d16ee1b9eada6101dc105a17fc01ddd244d476b4", + "originHash" : "ed354a8e92f6b810403986d192b495a0e8e67cc9577e8ec24bce4ba275c0513d", "pins" : [ { "identity" : "async-http-client", @@ -415,6 +415,15 @@ "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", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index b277b0d..225e92c 100644 --- a/Package.swift +++ b/Package.swift @@ -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-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/m-housh/swift-validations.git", from: "0.1.0"), ], targets: [ .executableTarget( @@ -67,6 +68,7 @@ let package = Package( .product(name: "DependenciesMacros", package: "swift-dependencies"), .product(name: "Fluent", package: "fluent"), .product(name: "Vapor", package: "vapor"), + .product(name: "Validations", package: "swift-validations"), ] ), .testTarget( diff --git a/Sources/DatabaseClient/Internal/Array+validator.swift b/Sources/DatabaseClient/Internal/Array+validator.swift new file mode 100644 index 0000000..4e2041a --- /dev/null +++ b/Sources/DatabaseClient/Internal/Array+validator.swift @@ -0,0 +1,48 @@ +import Validations + +extension Validator { + + static func validate( + _ toChild: KeyPath + ) + -> Self + { + self.mapValue({ $0[keyPath: toChild] }, with: ArrayValidator()) + } + + static func validate( + _ toChild: KeyPath + ) + -> Self + { + self.mapValue({ $0[keyPath: toChild] }, with: ArrayValidator().optional()) + } +} + +extension Array where Element: Validatable { + static func validator() -> some Validation { + ArrayValidator() + } +} + +struct ArrayValidator: Validation where Element: Validatable { + func validate(_ value: [Element]) throws { + for item in value { + try item.validate() + } + } +} + +struct ForEachValidator: Validation where T: Validation, T.Value == E { + let validator: T + + init(@ValidationBuilder builder: () -> T) { + self.validator = builder() + } + + func validate(_ value: [E]) throws { + for item in value { + try validator.validate(item) + } + } +} diff --git a/Sources/DatabaseClient/Internal/ComponentLosses.swift b/Sources/DatabaseClient/Internal/ComponentLosses.swift index 0eb58e7..f936802 100644 --- a/Sources/DatabaseClient/Internal/ComponentLosses.swift +++ b/Sources/DatabaseClient/Internal/ComponentLosses.swift @@ -4,6 +4,7 @@ import Fluent import Foundation import ManualDCore import SQLKit +import Validations extension DatabaseClient.ComponentLosses: TestDependencyKey { public static let testValue = Self() @@ -13,8 +14,8 @@ extension DatabaseClient.ComponentLosses { public static func live(database: any Database) -> Self { .init( create: { request in - let model = try request.toModel() - try await model.save(on: database) + let model = request.toModel() + try await model.validateAndSave(on: database) return try model.toDTO() }, delete: { id in @@ -35,13 +36,13 @@ extension DatabaseClient.ComponentLosses { try await ComponentLossModel.find(id, on: database).map { try $0.toDTO() } }, update: { id, updates in - try updates.validate() + // try updates.validate() guard let model = try await ComponentLossModel.find(id, on: database) else { throw NotFoundError() } model.applyUpdates(updates) if model.hasChanges { - try await model.save(on: database) + try await model.validateAndSave(on: database) } return try model.toDTO() } @@ -51,40 +52,9 @@ extension DatabaseClient.ComponentLosses { extension ComponentPressureLoss.Create { - func toModel() throws(ValidationError) -> ComponentLossModel { - try validate() + func toModel() -> ComponentLossModel { 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 { @@ -171,3 +141,19 @@ final class ComponentLossModel: Model, @unchecked Sendable { } } } + +extension ComponentLossModel: Validatable { + + var body: some Validation { + 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) + } + } +} diff --git a/Sources/DatabaseClient/Internal/Equipment.swift b/Sources/DatabaseClient/Internal/Equipment.swift index 1252cc1..3a927c9 100644 --- a/Sources/DatabaseClient/Internal/Equipment.swift +++ b/Sources/DatabaseClient/Internal/Equipment.swift @@ -3,6 +3,7 @@ import DependenciesMacros import Fluent import Foundation import ManualDCore +import Validations extension DatabaseClient.Equipment: TestDependencyKey { public static let testValue = Self() @@ -10,8 +11,8 @@ extension DatabaseClient.Equipment: TestDependencyKey { public static func live(database: any Database) -> Self { .init( create: { request in - let model = try request.toModel() - try await model.save(on: database) + let model = request.toModel() + try await model.validateAndSave(on: database) return try model.toDTO() }, delete: { id in @@ -37,10 +38,9 @@ extension DatabaseClient.Equipment: TestDependencyKey { guard let model = try await EquipmentModel.find(id, on: database) else { throw NotFoundError() } - try updates.validate() model.applyUpdates(updates) if model.hasChanges { - try await model.save(on: database) + try await model.validateAndSave(on: database) } return try model.toDTO() } @@ -50,8 +50,7 @@ extension DatabaseClient.Equipment: TestDependencyKey { extension EquipmentInfo.Create { - func toModel() throws(ValidationError) -> EquipmentModel { - try validate() + func toModel() -> EquipmentModel { return .init( staticPressure: staticPressure, 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 { @@ -197,3 +158,22 @@ final class EquipmentModel: Model, @unchecked Sendable { } } } + +extension EquipmentModel: Validatable { + + var body: some Validation { + 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) + } + } +} diff --git a/Sources/DatabaseClient/Internal/EquivalentLengths.swift b/Sources/DatabaseClient/Internal/EquivalentLengths.swift index d025081..f076436 100644 --- a/Sources/DatabaseClient/Internal/EquivalentLengths.swift +++ b/Sources/DatabaseClient/Internal/EquivalentLengths.swift @@ -3,6 +3,7 @@ import DependenciesMacros import Fluent import Foundation import ManualDCore +import Validations extension DatabaseClient.EquivalentLengths: TestDependencyKey { public static let testValue = Self() @@ -11,7 +12,7 @@ extension DatabaseClient.EquivalentLengths: TestDependencyKey { .init( create: { request in let model = try request.toModel() - try await model.save(on: database) + try await model.validateAndSave(on: database) return try model.toDTO() }, delete: { id in @@ -53,7 +54,7 @@ extension DatabaseClient.EquivalentLengths: TestDependencyKey { } try model.applyUpdates(updates) if model.hasChanges { - try await model.save(on: database) + try await model.validateAndSave(on: database) } return try model.toDTO() } @@ -64,7 +65,9 @@ extension DatabaseClient.EquivalentLengths: TestDependencyKey { extension EquivalentLength.Create { func toModel() throws -> EffectiveLengthModel { - try validate() + if groups.count > 0 { + try [EquivalentLength.FittingGroup].validator().validate(groups) + } return try .init( name: name, type: type.rawValue, @@ -73,12 +76,6 @@ extension EquivalentLength.Create { projectID: projectID ) } - - func validate() throws(ValidationError) { - guard !name.isEmpty else { - throw ValidationError("Effective length name can not be empty.") - } - } } extension EquivalentLength { @@ -184,7 +181,51 @@ final class EffectiveLengthModel: Model, @unchecked Sendable { self.straightLengths = straightLengths } if let groups = updates.groups { + if groups.count > 0 { + try [EquivalentLength.FittingGroup].validator().validate(groups) + } self.groups = try JSONEncoder().encode(groups) } } } + +extension EffectiveLengthModel: Validatable { + + var body: some Validation { + 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 { + 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) + } + } +} diff --git a/Sources/DatabaseClient/Internal/Model+validateAndSave.swift b/Sources/DatabaseClient/Internal/Model+validateAndSave.swift new file mode 100644 index 0000000..dca456d --- /dev/null +++ b/Sources/DatabaseClient/Internal/Model+validateAndSave.swift @@ -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) + } +} diff --git a/Sources/DatabaseClient/Internal/Projects.swift b/Sources/DatabaseClient/Internal/Projects.swift index 239cc25..f86cac5 100644 --- a/Sources/DatabaseClient/Internal/Projects.swift +++ b/Sources/DatabaseClient/Internal/Projects.swift @@ -3,6 +3,7 @@ import DependenciesMacros import Fluent import Foundation import ManualDCore +import Validations extension DatabaseClient.Projects: TestDependencyKey { public static let testValue = Self() @@ -10,8 +11,8 @@ extension DatabaseClient.Projects: TestDependencyKey { public static func live(database: any Database) -> Self { .init( create: { userID, request in - let model = try request.toModel(userID: userID) - try await model.save(on: database) + let model = request.toModel(userID: userID) + try await model.validateAndSave(on: database) return try model.toDTO() }, delete: { id in @@ -81,10 +82,9 @@ extension DatabaseClient.Projects: TestDependencyKey { guard let model = try await ProjectModel.find(id, on: database) else { throw NotFoundError() } - try updates.validate() model.applyUpdates(updates) if model.hasChanges { - try await model.save(on: database) + try await model.validateAndSave(on: database) } return try model.toDTO() } @@ -94,8 +94,7 @@ extension DatabaseClient.Projects: TestDependencyKey { extension Project.Create { - func toModel(userID: User.ID) throws -> ProjectModel { - try validate() + func toModel(userID: User.ID) -> ProjectModel { return .init( name: name, 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 { @@ -342,3 +277,35 @@ final class ProjectModel: Model, @unchecked Sendable { return model } } + +extension ProjectModel: Validatable { + + var body: some Validation { + 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) + + } + } +} diff --git a/Sources/DatabaseClient/Internal/Rooms.swift b/Sources/DatabaseClient/Internal/Rooms.swift index f673ddb..cfd8570 100644 --- a/Sources/DatabaseClient/Internal/Rooms.swift +++ b/Sources/DatabaseClient/Internal/Rooms.swift @@ -3,6 +3,7 @@ import DependenciesMacros import Fluent import Foundation import ManualDCore +import Validations extension DatabaseClient.Rooms: TestDependencyKey { public static let testValue = Self() @@ -11,7 +12,7 @@ extension DatabaseClient.Rooms: TestDependencyKey { .init( create: { request in let model = try request.toModel() - try await model.save(on: database) + try await model.validateAndSave(on: database) return try model.toDTO() }, delete: { id in @@ -31,7 +32,7 @@ extension DatabaseClient.Rooms: TestDependencyKey { model.rectangularSizes = nil } if model.hasChanges { - try await model.save(on: database) + try await model.validateAndSave(on: database) } return try model.toDTO() }, @@ -50,11 +51,9 @@ extension DatabaseClient.Rooms: TestDependencyKey { guard let model = try await RoomModel.find(id, on: database) else { throw NotFoundError() } - - try updates.validate() model.applyUpdates(updates) if model.hasChanges { - try await model.save(on: database) + try await model.validateAndSave(on: database) } return try model.toDTO() }, @@ -77,8 +76,7 @@ extension DatabaseClient.Rooms: TestDependencyKey { extension Room.Create { - func toModel() throws(ValidationError) -> RoomModel { - try validate() + func toModel() throws -> RoomModel { return .init( name: name, heatingLoad: heatingLoad, @@ -88,57 +86,6 @@ extension Room.Create { 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 { @@ -169,7 +116,7 @@ extension Room { } } -final class RoomModel: Model, @unchecked Sendable { +final class RoomModel: Model, @unchecked Sendable, Validatable { static let schema = "room" @@ -267,4 +214,38 @@ final class RoomModel: Model, @unchecked Sendable { } + var body: some Validation { + 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 { + 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) + } + } } diff --git a/Sources/DatabaseClient/Internal/TrunkSizes.swift b/Sources/DatabaseClient/Internal/TrunkSizes.swift index 4b67c72..15de062 100644 --- a/Sources/DatabaseClient/Internal/TrunkSizes.swift +++ b/Sources/DatabaseClient/Internal/TrunkSizes.swift @@ -3,6 +3,7 @@ import DependenciesMacros import Fluent import Foundation import ManualDCore +import Validations extension DatabaseClient.TrunkSizes: TestDependencyKey { public static let testValue = Self() @@ -10,12 +11,12 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey { public static func live(database: any Database) -> Self { .init( create: { request in - try request.validate() + // try request.validate() let trunk = request.toModel() var roomProxies = [TrunkSize.RoomProxy]() - try await trunk.save(on: database) + try await trunk.validateAndSave(on: database) for (roomID, registers) in request.rooms { guard let room = try await RoomModel.find(roomID, on: database) else { @@ -27,7 +28,7 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey { registers: registers, type: request.type ) - try await model.save(on: database) + try await model.validateAndSave(on: database) roomProxies.append( .init(room: try room.toDTO(), registers: registers) ) @@ -80,7 +81,7 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey { else { throw NotFoundError() } - try updates.validate() + // try updates.validate() try await model.applyUpdates(updates, on: database) return try model.toDTO() } @@ -90,17 +91,6 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey { 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 { .init( 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 { struct Migrate: AsyncMigration { @@ -205,7 +180,17 @@ final class TrunkRoomModel: Model, @unchecked Sendable { registers: registers ) } +} +extension TrunkRoomModel: Validatable { + var body: some Validation { + Validator.validate(\.registers) { + [Int].notEmpty() + ForEachValidator { + Int.greaterThanOrEquals(1) + } + } + } } final class TrunkModel: Model, @unchecked Sendable { @@ -276,7 +261,7 @@ final class TrunkModel: Model, @unchecked Sendable { self.name = name } if hasChanges { - try await self.save(on: database) + try await self.validateAndSave(on: database) } guard let updateRooms = updates.rooms else { @@ -297,7 +282,7 @@ final class TrunkModel: Model, @unchecked Sendable { currRoom.registers = registers } if currRoom.hasChanges { - try await currRoom.save(on: database) + try await currRoom.validateAndSave(on: database) } } else { database.logger.debug("CREATING NEW TrunkRoomModel") @@ -324,6 +309,21 @@ final class TrunkModel: Model, @unchecked Sendable { } } +extension TrunkModel: Validatable { + + var body: some Validation { + 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 { func toDTO() throws -> [TrunkSize] { diff --git a/Sources/DatabaseClient/Internal/User+validation.swift b/Sources/DatabaseClient/Internal/User+validation.swift new file mode 100644 index 0000000..5e2ef9d --- /dev/null +++ b/Sources/DatabaseClient/Internal/User+validation.swift @@ -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 { + 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) + } + } +} diff --git a/Sources/DatabaseClient/Internal/UserProfiles.swift b/Sources/DatabaseClient/Internal/UserProfiles.swift index ab450a7..1cbbc2c 100644 --- a/Sources/DatabaseClient/Internal/UserProfiles.swift +++ b/Sources/DatabaseClient/Internal/UserProfiles.swift @@ -1,8 +1,9 @@ import Dependencies import DependenciesMacros import Fluent +import Foundation import ManualDCore -import Vapor +import Validations extension DatabaseClient.UserProfiles: TestDependencyKey { @@ -11,9 +12,8 @@ extension DatabaseClient.UserProfiles: TestDependencyKey { public static func live(database: any Database) -> Self { .init( create: { profile in - try profile.validate() let model = profile.toModel() - try await model.save(on: database) + try await model.validateAndSave(on: database) return try model.toDTO() }, delete: { id in @@ -37,10 +37,9 @@ extension DatabaseClient.UserProfiles: TestDependencyKey { guard let model = try await UserProfileModel.find(id, on: database) else { throw NotFoundError() } - try updates.validate() model.applyUpdates(updates) if model.hasChanges { - try await model.save(on: database) + try await model.validateAndSave(on: database) } return try model.toDTO() } @@ -50,30 +49,6 @@ extension DatabaseClient.UserProfiles: TestDependencyKey { 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 { .init( 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 { struct Migrate: AsyncMigration { @@ -270,3 +204,31 @@ final class UserProfileModel: Model, @unchecked Sendable { } } + +extension UserProfileModel: Validatable { + + var body: some Validation { + 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) + } + } +} diff --git a/Sources/DatabaseClient/Internal/Users.swift b/Sources/DatabaseClient/Internal/Users.swift index efcaa48..bb539a2 100644 --- a/Sources/DatabaseClient/Internal/Users.swift +++ b/Sources/DatabaseClient/Internal/Users.swift @@ -1,6 +1,7 @@ import Dependencies import DependenciesMacros import Fluent +import Foundation import ManualDCore import Vapor @@ -10,6 +11,7 @@ extension DatabaseClient.Users: TestDependencyKey { public static func live(database: any Database) -> Self { .init( create: { request in + try request.validate() let model = try request.toModel() try await model.save(on: database) return try model.toDTO() @@ -118,21 +120,8 @@ extension User { extension User.Create { func toModel() throws -> UserModel { - try validate() 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 { diff --git a/Tests/DatabaseClientTests/ComponentLossTests.swift b/Tests/DatabaseClientTests/ComponentLossTests.swift index 60041ca..7262cd9 100644 --- a/Tests/DatabaseClientTests/ComponentLossTests.swift +++ b/Tests/DatabaseClientTests/ComponentLossTests.swift @@ -1,9 +1,10 @@ -import DatabaseClient import Dependencies import Foundation import ManualDCore import Testing +@testable import DatabaseClient + @Suite 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() + } + } } diff --git a/Tests/DatabaseClientTests/EquipmentTests.swift b/Tests/DatabaseClientTests/EquipmentTests.swift index 50568b2..852ed9c 100644 --- a/Tests/DatabaseClientTests/EquipmentTests.swift +++ b/Tests/DatabaseClientTests/EquipmentTests.swift @@ -1,9 +1,10 @@ -import DatabaseClient import Dependencies import Foundation import ManualDCore import Testing +@testable import DatabaseClient + @Suite 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() + } + } + } diff --git a/Tests/DatabaseClientTests/EquivalentLengthTests.swift b/Tests/DatabaseClientTests/EquivalentLengthTests.swift index 68861c5..7723961 100644 --- a/Tests/DatabaseClientTests/EquivalentLengthTests.swift +++ b/Tests/DatabaseClientTests/EquivalentLengthTests.swift @@ -1,9 +1,10 @@ -import DatabaseClient import Dependencies import Foundation import ManualDCore import Testing +@testable import DatabaseClient + @Suite 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() + } + } } diff --git a/Tests/DatabaseClientTests/ProjectTests.swift b/Tests/DatabaseClientTests/ProjectTests.swift index 37585a0..a377c41 100644 --- a/Tests/DatabaseClientTests/ProjectTests.swift +++ b/Tests/DatabaseClientTests/ProjectTests.swift @@ -1,5 +1,4 @@ import Dependencies -import DependenciesTestSupport import Fluent import FluentSQLiteDriver 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 + } + } + } } diff --git a/Tests/DatabaseClientTests/RoomTests.swift b/Tests/DatabaseClientTests/RoomTests.swift index 70446b1..55c1526 100644 --- a/Tests/DatabaseClientTests/RoomTests.swift +++ b/Tests/DatabaseClientTests/RoomTests.swift @@ -1,8 +1,10 @@ -import DatabaseClient import Dependencies import Foundation import ManualDCore import Testing +import Validations + +@testable import DatabaseClient @Suite 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 + // // } + // } + // } } diff --git a/Tests/DatabaseClientTests/TrunkSizeTests.swift b/Tests/DatabaseClientTests/TrunkSizeTests.swift index 7422f73..c80c017 100644 --- a/Tests/DatabaseClientTests/TrunkSizeTests.swift +++ b/Tests/DatabaseClientTests/TrunkSizeTests.swift @@ -1,9 +1,10 @@ -import DatabaseClient import Dependencies import Foundation import ManualDCore import Testing +@testable import DatabaseClient + @Suite 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() + } + } } diff --git a/Tests/DatabaseClientTests/UserTests.swift b/Tests/DatabaseClientTests/UserTests.swift index a47f3d4..4488f83 100644 --- a/Tests/DatabaseClientTests/UserTests.swift +++ b/Tests/DatabaseClientTests/UserTests.swift @@ -1,4 +1,3 @@ -import DatabaseClient import Dependencies import Foundation 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 func deleteFailsWithInvalidUserID() async throws { 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() + } + } }