Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: Smart constructor pattern / composable validations #96

Closed
jedesroches opened this issue Apr 2, 2024 · 3 comments
Closed

Question: Smart constructor pattern / composable validations #96

jedesroches opened this issue Apr 2, 2024 · 3 comments

Comments

@jedesroches
Copy link

Hello, and thank you for this nice library. Coming from the FP world, I am trying to use it with the smart constructor pattern and, not having a bind (or flatMap, or andThen, or anything you call (ValidationResult<T1> -> (T1 -> ValidationResult<T2>)) -> ValidationResult<T2>), am having a bit of trouble: I'd be happy to open a PR with a bind implementation, but first I'm thinking maybe I'm using this wrong.

Using Konform, I've managed to build the following inline class, who'se type represents a proof of being valid (I don't want to have Valid<x>'s all over my domain code), which works well enough:

@JvmInline
value class CustomerID private constructor(val value: String) {
    companion object {
        operator fun invoke(input: String): ValidationResult<CustomerID> = Validation {
            pattern("^[A-Z0-9]{32}$") hint "Customer ID <value> does not match `^[A-Z0-9]{32}$`"
        }(CustomerID(input))
    }
}

But then when I have a class that is using the above value class as a member, I cannot compose both validations. Given:

data class CustomerDto(
    val id: String,
    val name: String,
    val whatever: String,
)

data class Customer(
    val id: CustomerID,
    val name: CustomerName,
    val whatever: CustomerWhatever,
)

Going from the first above to the second, I cannot do something like CustomerID(id).andThen { id -> ... } to build the final object. But I have to validate the CustomerID before passing it to the Customer constructor (since the CustomerID type enforces validity of the data it contains), and so I am left with no choice but to validate by hand the different elements, and compose the errors manually.

What is the suggested usage pattern for Konform to have both type-level guarantees of data validity when passing values around and composability of validations to build more complex objects ?

I hope I've managed to explain my question correctly, and I'm looking forward to your answer! Have a great day 😃

@jedesroches
Copy link
Author

jedesroches commented Apr 2, 2024

In fact, thinking about it, there isn't even the need for bind (i.e. for ValidationResult to be a Monad), apply would be enough (i.e. for it to be an Applicative). Something along the lines of:

public fun <A, B> ValidationResult<(A) -> B>.apply(x: ValidationResult<A>): ValidationResult<B> =
    when (this) {
        is Invalid -> this
        is Valid -> x.map(value)
    }

Would allow one to compose various value class validations through smart constructors to build validated data classes, as so:

// If Foo and Bar are value classes with validation in their companion object invoke methods
data class FooBar(val foo: Foo, val bar: Bar) {
    // Does this exist somehow in Kotlin ?
    companion object { fun curried(foo: Foo) = { bar: Bar -> FooBar(foo, bar) } }
}

// And then this works:
val validatedFooBar: ValidationResult<FooBar> = Foo("abc").map(FooBar::curry).apply(Bar("cde"))

My kotlin is not good enough to make a DSL-y thing, but one could imagine a syntax like below to be rather nice to work with:

val validatedFooBar: ValidationResult<FooBar> = validateApply<FooBar> {
    Foo("abc")
    Bar("cde")
}

@dhoepelman dhoepelman modified the milestone: v0.5 May 10, 2024
@dhoepelman
Copy link
Collaborator

dhoepelman commented May 10, 2024

I'm not 100% sure I follow, but I think it would be good enough for ValidationResult to be a functor. Then you can do:

val customer: Customer = ...
val dto: ValidationResult<CustomerDto>= customerValidation(customer).map { it.toDto() }

The current (0.4.0) map doesn't properly work, fixed in 0.5.0. See #105 for when it will be released

To help you: In the JVM world functor is usually implemented by map and monadic bind by flatMap. Monadic unit is usually just a constructor. (Valid(...) in konform). I purposefully have left out flatMap so far since I think it's not suitable to the design of the library, but you can easily implement it as an extension function if you want.

Trying to find these in types and using this nomenclature for issues will make things more clear. You will probably also enjoy Arrow (altough I personally don't think Kotlin is suitable for going so hard on the FP)

@dhoepelman
Copy link
Collaborator

Closing this as it's not an issue per se, feel free to reply or open a new issue with a more directed question/problem

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants