Skip to content

Repository files navigation

GitHub License GitHub Tag Swift Package Manager Swift 6 Supported platforms CI

XCEValidatableValue

XCEValidatableValue defines reusable value specifications and applies them to editable model properties with Swift property wrappers. Raw input stays easy to edit, while validated and converted values are available only through an explicit throwing API.

It keeps domain-specific validation rules centralized, declarative, and reusable instead of scattering checks throughout an application.

Version 6 is a new major release with a deliberately redesigned API. See Migrating from 5.x before updating an existing project.

Requirements

  • Swift 6.0 or later
  • macOS 12+, Mac Catalyst 15+, iOS 15+, tvOS 15+, watchOS 8+, or visionOS 1+

Installation

Add the package to Package.swift:

dependencies: [
    .package(
        url: "https://github.com/XCEssentials/XCEValidatableValue",
        from: "6.0.0"
    )
]

Then add the product to your target:

.target(
    name: "YourTarget",
    dependencies: [
        .product(
            name: "XCEValidatableValue",
            package: "XCEValidatableValue"
        )
    ]
)

Define value specifications

A ValueSpecification describes the raw editable type, the validated output type, field metadata, and conditions evaluated before and after conversion.

import XCERequirement
import XCEValidatableValue

enum Email: ValueSpecification {
    typealias Raw = String

    static let field = ValidationField(
        name: "Email",
        placeholder: "name@example.com",
        hint: "Used to sign in"
    )

    static let conditionsOnRaw: [Condition<String, Never>] = [
        Condition("Contains @") { $0.contains("@") }
    ]
}

Declaring a specification as an enum creates a non-instantiable namespace for its static validation rules.

When Raw and Valid are the same type, conversion is supplied automatically. Collections are considered empty when their isEmpty property is true; other types are non-empty by default. Override isEmpty(_:) when the domain needs different behavior.

Convert raw input

Declare Valid and a typed ConversionFailure when validated output has a different type:

enum Age: ValueSpecification {
    typealias Raw = String
    typealias Valid = Int

    enum ConversionFailure: Error, Sendable {
        case notAnInteger
    }

    static let conditionsOnValid: [Condition<Int, Never>] = [
        Condition("At least eighteen") { $0 >= 18 }
    ]

    static func convert(_ rawValue: String) throws(ConversionFailure) -> Int {
        guard let age = Int(rawValue) else { throw .notAnInteger }
        return age
    }
}

If Valid conforms to RawRepresentable and its RawValue matches Raw, conversion is also supplied automatically.

Add values to a model

Use Required and NonRequired as property wrappers. Both expose Raw? as the wrapped property so a draft model can represent missing input.

struct RegistrationForm: ValidatableEntity, Codable {
    @Required<Email> var email: String? = nil
    @Required<Age> var age: String? = nil
    @NonRequired<Nickname> var nickname: String? = nil
}

enum Nickname: ValueSpecification {
    typealias Raw = String

    static let conditionsOnRaw: [Condition<String, Never>] = [
        Condition("At most thirty characters") { $0.count <= 30 }
    ]
}

Edit the raw values through the ordinary properties:

var form = RegistrationForm()
form.email = "ada@example.com"
form.age = "37"
form.nickname = "Countess"

Validate the entire entity, then obtain converted values from the projected wrappers:

try form.validate()

let email: String = try form.$email.validatedValue()
let age: Int = try form.$age.validatedValue()
let nickname: String? = try form.$nickname.validatedValue()

Requiredness has a direct effect on validation and return types:

Wrapper Missing or empty input validatedValue()
Required<Spec> Produces a presence issue Returns Spec.Valid or throws
NonRequired<Spec> Represents valid absence Returns Spec.Valid? or throws

Wrappers encode and decode as their raw single value rather than as an implementation-specific container.

Validation pipeline

Every non-empty value passes through the same ordered pipeline:

  1. conditionsOnRaw checks editable input.
  2. convert(_:) creates the valid representation.
  3. conditionsOnValid checks the converted value.

Validation stops before conversion when a raw condition fails, and stops before valid-value conditions when conversion fails. All failed conditions in the active phase are reported together.

Structured diagnostics

Call validationIssues() when errors should be displayed without throwing:

for issue in form.validationIssues() {
    print(issue.field.name)
    print(issue.phase)
    print(issue.message)
}

validate() and validatedValue() throw ValidationError, whose issues property contains the same [ValidationIssue]. Each issue includes:

  • its ValidationField;
  • the ValidationPhase (presence, rawValue, conversion, or validValue);
  • a displayable message and optional requirement description;
  • the RequirementContext call site.

Diagnostics and errors conform to Codable, Hashable, and Sendable.

Entity and plan composition

ValidatableEntity automatically discovers stored Validatable members, including property-wrapper storage, nested entities, and stored ValidationPlan values. Issues retain declaration order.

Use a plan to compose explicit validation:

let shouldValidateNickname = true
let plan = ValidationPlan {
    form.$email
    form.$age

    if shouldValidateNickname {
        form.$nickname
    }
}

try plan.validate()

An entity can implement additionalValidation for rules not represented by stored members, such as a cross-field comparison. The result builder accepts individual validatable values, conditionals, and loops.

Secret values

Mark a specification as SecretValue to prevent conversion error details from exposing sensitive input:

enum Password: ValueSpecification, SecretValue {
    typealias Raw = String

    static let conditionsOnRaw: [Condition<String, Never>] = [
        Condition("At least eight characters") { $0.count >= 8 }
    ]
}

The wrapper's metadata.isSecret value is also true, allowing a UI to select secure input controls.

Migrating from 5.x

Version 6 intentionally removes the legacy API rather than carrying deprecated aliases. The principal migrations are:

5.x 6.x
SomeValidatableValue ValueSpecification
SomeValidatable Validatable
SomeValidatableEntity ValidatableEntity
IsSecretValue SecretValue
DisplayNamed / DisplayNamedInfo ValidationField
Optional Required<Spec>? wrapper storage @Required<Spec> var value: Spec.Raw?
Optional NonRequired<Spec>? wrapper storage @NonRequired<Spec> var value: Spec.Raw?
rawValue on a wrapper The wrapped property
validValue or vv $property.validatedValue()
convert(rawValue:) -> Valid? convert(_:) throws(ConversionFailure) -> Valid
Case-based ValidationError ValidationError.issues with structured phases
ValidatationMetadata ValidationMetadata

The 5.x convenience protocols and global helpers for Boolean flags, checkmarks, email checks, wrapper construction, optionals, and wrapper comparison are no longer part of the package. Model those behaviors explicitly in a ValueSpecification or in application code.

Development

Run the test suite with:

swift test

The project is licensed under the MIT License.

About

Generic value wrapper with built-in validation.

Resources

Stars

1 star

Watchers

1 watching

Forks

Used by

Contributors

Languages