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

Related tags

Validation Valid
Overview

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.

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

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

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

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

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

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

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

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

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.

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

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,

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

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

TextFormation - Rules system for live typing completions

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

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

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

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

Type-based input validation.

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

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

Releases(2.0.0)
Owner
Mathias Quintero
Developer, Student, Spaghetti Code Enthusiast and Professional Swear Word Sayer.
Mathias Quintero
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

null 51 Oct 19, 2022
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

red_mad_robot 548 Dec 20, 2022
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

null 1.4k Dec 29, 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.

GeumSang Yoo 153 Nov 17, 2022
String (and more) validation for iOS

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

George Kaimakas 241 Nov 13, 2022
🚦 Validation library depends on SwiftUI & Combine. Reactive and fully customizable.

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

Alexo 14 Dec 30, 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

Insect_QY 1 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

Dave Poirier 21 Oct 24, 2022
🧭 SwiftUI navigation done right

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

Alex Nagy 152 Dec 27, 2022
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

null 589 Dec 24, 2022