Input Validation Done Right. A Swift DSL for Validating User Input using Allow/Deny Rules

Last update: May 7, 2022

Syntax

Swift Package Manager Twitter: @nerdsupremacist

Valid

Input Validation Done Right. Have you ever struggled with a website with strange password requirements. Especially those crazy weird ones where they tell you whats wrong with your password one step at a time. And it like takes forever. Well I have. And to prove a point, and, to be honest, mainly as a joke, I coded a DSL for password requirements. After a while I decided to make it more generic, and here is the version that can validate any input. And I called it Valid.

Valid is a Swift DSL (much like SwiftUI) for validating inputs. It follows Allow or Deny rules, a concept commonly used in access control systems.

Installation

Swift Package Manager

You can install Valid via Swift Package Manager by adding the following line to your Package.swift:

import PackageDescription

let package = Package(
    [...]
    dependencies: [
        .package(url: "https://github.com/nerdsupremacist/Valid.git", from: "1.0.0")
    ]
)

Usage

So what can you validate with Valid? Well pretty much anything you'd like. You can use it to:

  • Validate Password Requirements
  • Privacy and Access Control Checks
  • well, I honestly haven't thought of more easy to explain examples, but the possibilities are endless...

Let's start with an example. Let's start validating some passwords. For that we create a Validator, with a set of rules:

struct PasswordValidator: Validator {
    var rules: ValidationRules<String> {
        AlwaysAllow<String>()
    }
}

Right now our validator, just allows every password to be set. That's what AlwaysAllow will do. That will be our fallback. Next we can start with a simple check for the length. Let's say we want it to be at least 8 characters long:

struct PasswordValidator: Validator {
    var rules: ValidationRules<String> {
        DenyIf("Must contain at least 8 characters") { $0.count < 8 }
        
        AlwaysAllow<String>()
    }
}

We just used the DenyIf rule. This rule says that we will deny the input, when our closure evaluates to true. So for any password with 8 characters or longer, the DenyIf won't deny it, and we will continue to our next rule on the list, which is AlwaysAllow. While we're at it, a fun aspect of the DSL is that in Valid you can write composable and reusable rules. And you can reuse rules for the values of properties. So for example another way of writing the 8 Characters rule would be to validate the value of count:

struct PasswordValidator: Validator {
    var rules: ValidationRules<String> {
        Property(\String.count) {
            DenyIfTooSmall(minimum: 8)
                .message("Must be at least 8 characters long")
        }
        
        AlwaysAllow<String>()
    }
}

The Property struct let's you inline rules for the value of a keypath. And since Valid already has a DenyIfTooSmall rule, we can just reuse it here. Let's keep going. How about validating against invalid characters. Well we already included a rule for that:

struct PasswordValidator: Validator {
    var rules: ValidationRules<String> {
        DenyIfContainsInvalidCharacters(allowedCharacters: .letters.union(.decimalDigits).union(.punctuationCharacters))
        
        Property(\String.count) {
            DenyIfTooSmall(minimum: 8)
                .message("Must be at least 8 characters long")
        }
        
        AlwaysAllow<String>()
    }
}

We can even tell the user which characters are wrong:

struct PasswordValidator: Validator {
    var rules: ValidationRules<String> {
        DenyIfContainsInvalidCharacters(allowedCharacters: .letters.union(.decimalDigits).union(.punctuationCharacters))
            .message { invalidCharacters in
                let listed = invalidCharacters.map { "\"\($0)\"" }.joined(separator: ", ")
                // Feel free to do better localization and plural handling
                return "Character(s) \(listed) is/are not allowed"
            }
        
        Property(\String.count) {
            DenyIfTooSmall(minimum: 8, message: "Must be at least 8 characters long")
        }
        
        AlwaysAllow<String>()
    }
}

Or the classic, your password must contain a number:

struct PasswordValidator: Validator {
    var rules: ValidationRules<String> {
        DenyIfContainsInvalidCharacters(allowedCharacters: .letters.union(.decimalDigits).union(.punctuationCharacters))
            .message { invalidCharacters in
                let listed = invalidCharacters.map { "\"\($0)\"" }.joined(separator: ", ")
                // Feel free to do better localization and plural handling
                return "Character(s) \(listed) is/are not allowed"
            }
            
        DenyIfContainsTooFewCharactersFromSet(.decimalDigits, minimum: 1, message: "Must contain a number")
        
        Property(\String.count) {
            DenyIfTooSmall(minimum: 8, message: "Must contain a number")
        }
        
        AlwaysAllow<String>()
    }
}

In order to use the validator we can just use the function validate:

// We set lazy to false, which will run all rules to give us more detailed results
let validation = await PasswordValidator().validate(input: "h⚠️llo", lazy: false)
print(validation.verdict) 
// .deny(message: "Character(s) @ is/are not allowed")

let errors = validation.all.errors.map(\.message) 
// ["Character(s) ⚠️ is/are not allowed", "Must contain a number", "Must contain a number"]

A couple of details you might have gotten from this:

  • Validation works using async/await. This is to enable these rules to perform complex logic such as accessing a database if needed
  • Validation is lazy by default. Meaning it will evaluate the rules from top to bottom until it reaches a decision. With the lazy flag set to false, it will evaluate every rule regardless of any final results that came before and report all errors that could occurr. A Password Validator is a perfect opportunity for using this.
  • The validation result will include every message that passed or failed during validation

If all you care about is the true or false there's also isValid:

let isValid = await PasswordValidator().isValid(input: password)

Implementing Rules

So Validators use Rules. These Rules in general can be any of the following:

  • Maybe Allow: it will either allow the input or skip to the next rule on the list
  • Maybe Deny: it will either deny the input or skip.
  • Warning Emmitter: it can add a warning to the results, but will not affect the outcome
  • Final Rule: it will either allow or deny. There can't be a rule afer that

There's a protocol for each of these kinds of rules. For example, if you were using the Password validator in a Vapor App, and wanted to stop validating passwords during development:

struct AllowIfInDevelopmentEnvironment<Input>: MaybeAllowValidationRule {
    let app: App

    func evaluate(on input: Input) async -> MaybeAllow {
        if case .development = app.environment {
            return .allow(message: "No checks during development")
        }

        return .skip
    }
}

struct PasswordValidator: Validator {
    let app: App
    
    var rules: ValidationRules<String> {
       AllowIfInDevelopmentEnvironment<String>(app: app)
       
       ...
    }
}

The process is very similar for all other kinds of rules. And if you don't feel like writing a struct for your rules, you will always have our defaults:

  • AllowIf: Allow if the closure you give it evaluates to true
  • DenyIf: Deny if the closure you give it evaluates to true
  • WarnIf: Emit a warning if the closure you give it evaluates to true
  • AlwaysAllow: Finish the validation by allowing
  • AlwaysDeny: Finish the validation by denying

Validators vs Partial Validators

For the sake of reusability, there's two kinds of validators:

  • Regular Validators: Validate the input using the rules. They are guaranteed to finish with a result, either allow or deny. This is enforced at compile time.
  • Partial Validators: They are not guaranteed to have a final result.

What does that mean? Well it means that a Validator, needs to have a allow or deny decision at the end. No exceptions. This means that the last rule, needs to be either:

  • AlwaysAllow
  • AlwaysDeny
  • Some implementation of FinalRule
  • Another Validator that is guaranteed to finish

On the other hand, Partial Validators are not allowed to include these rules inside. This effectively means:

  • You can inline a partial validator inside any other partial validator or validator. No problem
  • You can only inline a validator at the very end of another validator

Did that make sense? No worries, just try it out, you'll get it.

Debugging and nerdy details

Do you have a tricky input you want to debug? No problem. There's a checks function that will tell you exactly all the steps taken by your validator. Every allow, deny, skip decision including the location in code where that decision was made:

let checks = await PasswordValidator().checks(input: "hello", lazy: true)
// [
//    Check(type: DenyIfContainsInvalidCharacters, kind: .validation(.skip), location: ...), 
//    Check(type: DenyIfContainsTooFewCharactersFromSet, kind: .validation(.deny(message: "Must contain a number"), location: ...),
// ]

Contributions

Contributions are welcome and encouraged!

License

Valid is available under the MIT license. See the LICENSE file for more info.

GitHub

https://github.com/nerdsupremacist/Valid
You might also like...

Drop in user input validation for your iOS apps.

Drop in user input validation for your iOS apps.

Validator Validator is a user input validation library written in Swift. It's comprehensive, designed for extension, and leaves the UI up to you. Here

Apr 14, 2022

iOS validation framework with form validation support

ATGValidator ATGValidator is a validation framework written to address most common issues faced while verifying user input data. You can use it to val

Mar 27, 2022

iOS validation framework with form validation support

ATGValidator ATGValidator is a validation framework written to address most common issues faced while verifying user input data. You can use it to val

Mar 27, 2022

Input Mask is an Android & iOS native library allowing to format user input on the fly.

Input Mask is an Android & iOS native library allowing to format user input on the fly.

Migration Guide: v.6 This update brings breaking changes. Namely, the autocomplete flag is now a part of the CaretGravity enum, thus the Mask::apply c

May 9, 2022

Swift plugin which allow add mask to input field

Swift plugin which allow add mask to input field

AKMaskField AKMaskField is UITextField subclass which allows enter data in the fixed quantity and in the certain format (credit cards, telephone numbe

May 15, 2022

Type-based input validation.

Ensure Type-based input validation try EnsurePackageIsCool(wrappedValue: packages.ensure) Validators A Validator is a type that validates an input.

Jan 22, 2022

A modal passcode input and validation view controller for iOS

A modal passcode input and validation view controller for iOS

TOPasscodeViewController A modal passcode input and validation view controller for iOS. TOPasscodeViewController is an open-source UIViewController su

May 4, 2022

VKPinCodeView is simple and elegant UI component for input PIN. You can easily customise appearance and get auto fill (OTP) iOS 12 feature right from the box.

VKPinCodeView is simple and elegant UI component for input PIN. You can easily customise appearance and get auto fill (OTP) iOS 12 feature right from the box.

Features Variable PIN length Underline, border and custom styles The error status with / without shake animation Resetting the error status manually,

May 16, 2022

A Swift framework for parsing, formatting and validating international phone numbers. Inspired by Google's libphonenumber.

A Swift framework for parsing, formatting and validating international phone numbers. Inspired by Google's libphonenumber.

PhoneNumberKit Swift 5.3 framework for parsing, formatting and validating international phone numbers. Inspired by Google's libphonenumber. Features F

May 21, 2022

TPInAppReceipt is a lightweight, pure-Swift library for reading and validating Apple In App Purchase Receipt locally.

TPInAppReceipt is a lightweight, pure-Swift library for reading and validating Apple In App Purchase Receipt locally.

TPInAppReceipt is a lightweight, pure-Swift library for reading and validating Apple In App Purchase Receipt locally. Features Read all

May 19, 2022

A Swift framework for parsing, formatting and validating international phone numbers. Inspired by Google's libphonenumber.

A Swift framework for parsing, formatting and validating international phone numbers. Inspired by Google's libphonenumber.

PhoneNumberKit Swift 5.3 framework for parsing, formatting and validating international phone numbers. Inspired by Google's libphonenumber. Features F

May 21, 2022

OysterKit is a framework that provides a native Swift scanning, lexical analysis, and parsing capabilities. In addition it provides a language that can be used to rapidly define the rules used by OysterKit called STLR

OysterKit A Swift Framework for Tokenizing, Parsing, and Interpreting Languages OysterKit enables native Swift scanning, lexical analysis, and parsing

Apr 15, 2022

A lightweight Swift date library for parsing, validating, manipulating, and formatting dates based on moment.js.

A lightweight Swift date library for parsing, validating, manipulating, and formatting dates based on moment.js.

Jan 28, 2022

A simple wrapper for the iOS Keychain to allow you to use it in a similar fashion to User Defaults. Written in Swift.

SwiftKeychainWrapper A simple wrapper for the iOS / tvOS Keychain to allow you to use it in a similar fashion to User Defaults. Written in Swift. Prov

May 12, 2022

A simple wrapper for the iOS Keychain to allow you to use it in a similar fashion to User Defaults. Written in Swift.

SwiftKeychainWrapper A simple wrapper for the iOS / tvOS Keychain to allow you to use it in a similar fashion to User Defaults. Written in Swift. Prov

May 20, 2022

Utility functions for validating IBOutlet and IBAction connections

Utility functions for validating IBOutlet and IBAction connections

Outlets Utility functions for validating IBOutlet and IBAction connections. About Outlets provides a set of functions which validate that IBOutlets ar

May 2, 2022

Simulates cellular automata patterns according to rules of Wolfram Alpha.

Simulates cellular automata patterns according to rules of Wolfram Alpha.

Cellular Automata App Simulates cellular automata patterns according to rules of Wolfram Alpha. What can I do with this? This app is designed with the

Jan 15, 2022

TextFormation - Rules system for live typing completions

TextFormation TextFormation is simple rule system that can be used to implement

May 19, 2022

This repository contains rules for Bazel that can be used to generate Xcode projects

rules_xcodeproj This repository contains rules for Bazel that can be used to generate Xcode projects. If you run into any problems with these rules, p

May 19, 2022
Related tags
iOS validation framework with form validation support

ATGValidator ATGValidator is a validation framework written to address most common issues faced while verifying user input data. You can use it to val

Mar 27, 2022
Input Mask is an Android & iOS native library allowing to format user input on the fly.
Input Mask is an Android & iOS native library allowing to format user input on the fly.

Migration Guide: v.6 This update brings breaking changes. Namely, the autocomplete flag is now a part of the CaretGravity enum, thus the Mask::apply c

May 9, 2022
Swift Validator is a rule-based validation library for Swift.
Swift Validator is a rule-based validation library for Swift.

Swift Validator is a rule-based validation library for Swift. Core Concepts UITextField + [Rule] + (and optional error UILabel) go into

May 10, 2022
RxValidator Easy to Use, Read, Extensible, Flexible Validation Checker.

RxValidator Easy to Use, Read, Extensible, Flexible Validation Checker. It can use without Rx. Requirements RxValidator is written in Swift 4.

Feb 4, 2022
String (and more) validation for iOS

Swift Validators ?? String validation for iOS. Contents Installation Walkthrough Usage Available validators License ReactiveSwift + SwiftValidators Wa

Apr 7, 2022
🚦 Validation library depends on SwiftUI & Combine. Reactive and fully customizable.

?? Validation library depends on SwiftUI & Combine. Reactive and fully customizable.

Mar 23, 2022
Validation plugin for Moya.Result

MoyaResultValidate Why? Sometimes we need to verify that the data returned by the server is reasonable, when Moya returns Result.success. JSON returne

Dec 15, 2021
SwiftEmailValidator - A Swift implementation of an international email address syntax validator based on RFC5321 & RFC5322

SwiftEmailValidator A Swift implementation of an international email address syn

May 16, 2022
🧭 SwiftUI navigation done right

?? NavigationKit NavigationKit is a lightweight library which makes SwiftUI navigation super easy to use. ?? Installation ?? Swift Package Manager Usi

May 10, 2022
iOS routing done right. Handles both URL recognition and controller displaying with parsed parameters. All in one line, controller stack preserved automatically!
iOS routing done right. Handles both URL recognition and controller displaying with parsed parameters. All in one line, controller stack preserved automatically!

Developed and Maintained by Ipodishima Founder & CTO at Wasappli Inc. (If you need to develop an app, get in touch with our team!) So what is this lib

Apr 6, 2022