Skip to content

Instantly share code, notes, and snippets.

@lukeredpath
Last active October 16, 2021 17:57
Show Gist options
  • Save lukeredpath/6eb646c2338e4f0f59a64ceb69162e3e to your computer and use it in GitHub Desktop.
Save lukeredpath/6eb646c2338e4f0f59a64ceb69162e3e to your computer and use it in GitHub Desktop.
A little experiment with functional Rails-style validators built on top of pointfreeco/Validated
import UIKit
import Validated
extension Validated {
func mapErrors<LocalError>(_ transform: (Error) -> LocalError) -> Validated<Value, LocalError> {
switch self {
case let .valid(value):
return .valid(value)
case let .invalid(errors):
return .invalid(errors.map(transform))
}
}
}
struct ValidatorOf<Value> {
let validate: (Value) -> Validated<Value, String>
func pullback<LocalValue>(_ transform: @escaping (LocalValue) -> Value) -> ValidatorOf<LocalValue> {
return ValidatorOf<LocalValue> { localValue in
self.validate(transform(localValue)).map { _ in localValue }
}
}
func mapErrors(_ transform: @escaping (String) -> String) -> ValidatorOf<Value> {
return ValidatorOf<Value> { value in
self.validate(value).mapErrors(transform)
}
}
}
extension ValidatorOf where Value == Int {
static func atLeast(_ minimum: Int) -> Self {
Self { value in
if value >= minimum {
return .valid(value)
}
return .error("must be a minimum length of \(minimum)")
}
}
static func atMost(_ maximum: Int) -> Self {
Self { value in
if value <= maximum {
return .valid(value)
}
return .error("must be a maximum length of \(maximum)")
}
}
static func exactly(_ count: Int) -> Self {
Self { value in
if value == count {
return .valid(value)
}
return .error("must equal \(count)")
}
}
static func lessThan(_ upperBound: Int) -> Self {
Self { value in
if value < upperBound {
return .valid(value)
}
return .error("must be less than \(upperBound)")
}
}
static func greaterThan(_ lowerBound: Int) -> Self {
Self { value in
if value > lowerBound {
return .valid(value)
}
return .error("must be less than \(lowerBound)")
}
}
static let odd = Self { value in
if value % 2 == 1 {
return .valid(value)
}
return .error("must be odd")
}
static let even = Self { value in
if value % 2 == 0 {
return .valid(value)
}
return .error("must be even")
}
}
extension ValidatorOf where Value == String {
static func beginsWith(_ prefix: String) -> Self {
Self { value in
if value.hasPrefix(prefix) {
return .valid(value)
}
return .error("must begin with \(prefix)")
}
}
static func hasLengthOf(_ validator: ValidatorOf<Int>) -> Self {
return validator.pullback(\.count).mapErrors { "length \($0)" }
}
}
extension ValidatorOf where Value: Collection {
static func hasLengthOf(_ validator: ValidatorOf<Int>) -> Self {
return validator.pullback(\.count)
}
}
extension ValidatorOf where Value: Collection, Value.Element: Equatable {
static func contains(_ element: Value.Element) -> Self {
Self { value in
if value.contains(element) {
return .valid(value)
}
return .error("must contain \(element)")
}
}
}
func validate<Value>(_ value: Value, _ validator: ValidatorOf<Value>) -> Validated<Value, String> {
validator.validate(value)
}
func combine<Value>(_ validators: ValidatorOf<Value>...) -> ValidatorOf<Value> {
return ValidatorOf { value in
validators.reduce(.valid(value)) { validated, validator in
return zip(validated, validator.validate(value)).map { _ in value }
}
}
}
@dynamicMemberLookup
struct Validating<Value> {
var value: Value {
didSet {
validatedValue = validator.validate(value)
}
}
let validator: ValidatorOf<Value>
private var validatedValue: Validated<Value, String>
init(initialValue: Value, validator: ValidatorOf<Value>) {
self.value = initialValue
self.validator = validator
self.validatedValue = validator.validate(value)
}
subscript<T>(dynamicMember keyPath: KeyPath<Validated<Value, String>, T>) -> T {
return validatedValue[keyPath: keyPath]
}
}
validate("blo", combine(
.hasLengthOf(.exactly(3)),
.hasLengthOf(.odd),
.beginsWith("blo")
))
validate("blob", .hasLengthOf(.exactly(4)))
validate([1, 2, 3], .hasLengthOf(.exactly(4)))
//var blob = Validating(initialValue: "", validator: combine(
// .hasLengthOf(.exactly(3)),
// .hasLengthOf(.odd),
// .beginsWith("blo")
//))
//
//print("value:", blob.value)
//print("is valid:", blob.isValid)
//print("errors:", blob.errors ?? [])
//
//blob.value = "blo"
//
//print("value:", blob.value)
//print("is valid:", blob.isValid)
//print("errors:", blob.errors ?? [])
var numbers = Validating(initialValue: [], validator: combine(
.hasLengthOf(.atLeast(2)),
.hasLengthOf(.even),
.contains(7)
))
numbers.value = [1, 2, 3, 7]
print("value:", numbers.value)
print("is valid:", numbers.isValid)
print("errors:", numbers.errors ?? [])
import PlaygroundSupport
import SwiftUI
struct FormView: View {
@State var name = Validating(initialValue: "", validator: .hasLengthOf(.atLeast(3)))
var body: some View {
Form {
TextField("Name:", text: $name.value)
if !name.isValid {
Text("Error: \(name.errors!.first)")
.font(.system(.footnote))
.foregroundColor(.red)
}
}
}
}
PlaygroundPage.current.liveView = UIHostingController(rootView: FormView())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment