SwiftVerify

Swift Tests GitHub release codecov

一个灵活的状态验证解决方案。

特性

用法

创建验证器

从谓词创建简单的验证器

let validateEmail = Verify<String>.that({ $0.contains("@") }, otherwise: .myError)

扩展和重用验证器

你可以通过扩展轻松地在任何类型上创建验证器

extension Verify where Subject == Int {
    public static func greaterThanZero(otherwise error: Error) -> Validator_<Subject> {
        Verify<Int>.that({ $0  >  0}, otherwise: error)
    }
}

extension Verify where Subject == String {
    public static func minLength(_ value: Int, otherwise error: Error) -> Validator_<Subject> {
    Verify.that({ (string: String) in string.count >= value }, otherwise: error)
    }
}

创建这些扩展后,它们将像这样可用

Verify<String>.minLength(10, otherwise: .myError)
Verify<Int>.greaterThanZero(otherwise: .myOtherError)

组合

Verify 有两种组合方式,一种是顺序组合,另一种是并行组合。

顺序组合

在顺序组合中,一次只运行一个验证器,并且最多累积一个错误,因为链中的下一个验证器只有在前一个验证器成功时才会应用。

let emailValidator = Verify<String>.inOrder {
    Verify.that({ $0.contains("@")}, otherwise: invalidEmail)
    Verify.minLength(5, otherwise: invalidEmail)
}

let input = "1"
emailValidator.errors(input).count == 1

请注意,即使输入 "1" 未通过两个验证,也只会累积一个错误。 这通常是期望的行为,因为我们希望一次验证一个条件。

也可以写成

let emailValidator = Verify<String>
    .that({ $0.contains("@")}, otherwise: invalidEmail)
    .andThat({ $0.count >= 5}, otherwise: invalidEmail)

let input = "1"
emailValidator.errors(input).count == 1

并行组合

在并行组合中,我们同时运行所有验证器并累积所有错误。

let emailValidator = Verify<String>.atOnce {
    Verify.that({ $0.contains("@")}, otherwise: invalidEmail)
    Verify.minLength(5, otherwise: invalidEmail)
}

let input = "1"
emailValidator.errors(input).count == 2

前一个示例将累积两个错误。

速查表

工厂方法

方法 签名 描述
Verify.that (Predicate<S>) -> Validator<S> 使用谓词验证
Verify.at (KeyPath<S, P>, Predicate<P>) -> Validator<S> 验证由键路径聚焦的属性
Verify.error (Error) -> Validator<S> 始终以指定的错误失败

组合

方法 签名 累积错误
andThen (Validator<S>) -> Validator<S>
andThat / thenCheck (Predicate) -> Validator
add (Validator<S>) -> Validator<S>
addCheck (Predicate<S>) -> Validator<S>

实用程序

方法 签名 描述
ignore (Predicate<S>) -> Validator<S> 当提供的谓词为真时,绕过验证器

示例

字段验证

给定一个模型,例如 UserRegistration 结构体

struct UserRegistration {
    let email: String
    let password: String
    let passwordConfirmation: String
}

我们可以使用键路径将验证应用于特定属性。

let invalidEmail = UserRegistrationError.invalidEmail
let invalidPassword = UserRegistrationError.invalidPassword

let emailValidator = Verify<String>.inOrder {
    Verify.minLength(5, otherwise: invalidEmail)
    Verify.that({ $0.contains("@")}, otherwise: invalidEmail)
}

let password = Verify<String>.inOrder {
    Verify<String>.that({ $0.count > 5}, otherwise: invalidPassword)
    Verify.containsSomeOf(CharacterSet.symbols, otherwise: invalidPassword)
}

let registrationValidator = Verify<UserRegistration>.atOnce {
    Verify<UserRegistration>.at(\.email, validator: emailValidator)
    Verify<UserRegistration>.at(\.password, validator: password)
    Verify<UserRegistration>.that({ $0.password == $0.passwordConfirmation  }, otherwise: UserRegistrationError.passwordsDontMatch)
}

let errors = registrationValidator.errors(UserRegistration(email: "", password: "19d", passwordConfirmation: "12d"))

运行验证器

运行验证器就像传入一个参数一样简单,因为它只是一个函数。 为了更简洁,提供了一个 verify 方法,这个方法很特别,因为它除了将参数转发给调用的验证器之外,还可以用于过滤错误列表并将其强制转换为特定的错误类型。 只需提供一个特定的类型参数。

表单验证

通常,你会将你的错误类型建模为类似于

struct FormError<FieldType>: Error {
    enum Reason {
        case invalidFormat, required
    }

    let reason: Reason
    let field:  FieldType
}

enum LoginField {
    case email, password
}

在这些情况下,能够按字段对错误进行分组是很方便的。

typealias LoginFormError = FormError<LoginField>

let validator = Verify<Int>.atOnce {
    Verify<Int>.error(LoginFormError(reason: .invalidFormat, field: .email))
    Verify<Int>.error(LoginFormError(reason: .required, field: .password))
}

let groupedErrors: [LoginField: [LoginFormError]] = validator.groupedErrors(0, by: { (error:  LoginFormError) in error.field })

//  Or even

let fieldErrors: [LoginField: [LoginFormError.Reason]] = groupedErrors.mapValues({  $0.map({ $0.reason })})