Last active
October 16, 2021 17:57
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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