CLOVIS
03/28/2023, 7:58 AMvalue class Password(val text: String) {
init {
require(text.length > 8) { "A password should have at least 8 characters, found ${text.length}" }
}
}
I know about ensure
& co, however the primary constructor cannot return Either
, so how do you recommend exposing validation?
I guess another pattern could be to provide a factory method?
fun String.asPassword() = Either.catch { Password(this@asPassword) }
simon.vergauwen
03/28/2023, 8:09 AMrequire
is that it fails fast, but so does your other example using context
.
Otherwise you would need Raise<NonEmptyList<PasswordError>>
or PasswordValidationError
that has all errors inside.
If that is not a concern, then using require
and Either.catch
is a good solution.Marko Novakovic
03/28/2023, 9:07 AMvalue class
so it looks like you are calling the constructorsimon.vergauwen
03/28/2023, 9:08 AMMarko Novakovic
03/28/2023, 9:09 AMif used with KotlinX, there is no way to use it with factory methods instead of constructor.can you elaborate a bit more? I don’t understand the issue there
simon.vergauwen
03/28/2023, 9:14 AM@Serializable
it will use primary constructor, not factory methods.Marko Novakovic
03/28/2023, 9:14 AMCLOVIS
03/28/2023, 9:26 AMso it looks like you are calling the constructorBut then it's unclear whether you're calling the overload that throws or the overload that raises.
Marko Novakovic
03/28/2023, 9:26 AMcompanion object
or somethingCLOVIS
03/28/2023, 9:27 AMMarko Novakovic
03/28/2023, 9:28 AMEither.catch
that that’s fine imoCLOVIS
03/28/2023, 9:34 AMsealed class PasswordValidatorError { … }
value class Password(val text: String) {
init {
validate(text).orElse { throw IllegalArgumentException(it.toString()) }
}
companion object {
fun validate(text: String) = either {
ensure(text.length > 8) { PasswordValidatorError.TooShort }
}
}
}
fun String.asPassword() = either {
validate(this).bind()
Password(this)
}
It's more verbose, and the validation is always executed twice, but it doesn't use exceptions for error managementMarko Novakovic
03/28/2023, 9:43 AMCLOVIS
03/28/2023, 9:44 AMMarko Novakovic
03/28/2023, 9:45 AMphldavies
03/28/2023, 1:10 PMcreate(text: String): Validated<A, B>
and createUnsafe(text: String): B = create(text).getOrThrow()
simon.vergauwen
03/28/2023, 1:24 PMAnd used a custom serializer in kotlinx to handle validation and propagationDo you have an example of this?
phldavies
03/28/2023, 3:07 PMSerializationException
subclass (so it plays nicely with the rest of kotlinx-serialization)simon.vergauwen
03/28/2023, 3:10 PMCLOVIS
04/15/2023, 2:43 PM// Utilities
fun requireSuccess(block: Raise<*, *>.() -> Unit) {
val value = either { block() }
require(value is Either.Right) { (value as Either.Left).value.toString() }
}
// Actual code
data class User(
val username: String,
val age: Int,
) {
init {
requireSuccess(validateUsername(username))
requireSuccess(validateAge(age))
}
companion object {
context(Raise<ValidationError>)
fun validateUsername(username: String) {
ensure(username.length > 4) { ValidatorError.UsernameTooShort }
ensure(username.length < 100) { ValidationError.UsernameTooLong }
}
context(Raise<ValidationError>)
fun validateAge(age: Int) {
ensure(age >= 13) { ValidationError.TooYoung }
}
}
sealed class ValidationError { … }
}
context(Raise<ValidationError>)
fun user(username: String, age: Int): User {
validateUsername(username)
validateAge(age)
return User(username, age)
}
Pros:
• The constructor throws IllegalArgumentException (idiomatic)
• The factory requires a Raise context and raises a custom sealed class (idiomatic)
• UI code can validate values independently in forms
Cons:
• The validation is executed twice when using the factory (should be fine?)