Hi everyone! I’ve been working on a small Kotlin v...
# feed
t
Hi everyone! I’ve been working on a small Kotlin validation library and just released the first version. It provides: • A type-safe, composable DSL for defining validation rules • Built-in constraints for strings, numbers, collections, etc. • Rule composition (
and
,
or
,
map
,
chain
) • Clear, structured error messages with keys you can localize • No reflection, simple JVM-first design It’s still early days, but feedback is super welcome! 👉 GitHub: https://github.com/komapper/kova Thanks!
👍🏻 1
👍 6
y
This looks really cool! Why not make it multiplatform? Nothing about it looks JVM-specific to me For object schemas, you might wanna look into using
provideDelegate
to enforce that the ability to register a property validator can only happen inside the class body. Some integrations with Arrow could be nice to have! Arrow has
Raise
for signalling errors, and
Accumulate
for error accumulation. Interop back and forth between the 2 libraries may thus be a nice plus! (Ping/DM me or ask in #C5UPMM0A0 if you have any questions! I worked on the latest design for
Accumulate
, so I should be able to help!). The "Arrow approach" is to get away from having Validators as first-class objects that get passed around and such, and instead represent a Validator using a function. Validator chaining then becomes normal function composition. Validation `or`ing just becomes a simple
||
, etc. Error accumulation is done by only "forcing" an error to happen when a value is really needed.
tryValidate
et all then become DSL entry-points. This also means we can use
inline
lambdas everywhere, so it works well with
suspend
and even
@Composable
! It also makes the DSL compose nicely with other features, like `raise`ing an error in your validator if you encounter something fatal that should end validation, or
ResourceScope
for resource cleanup (although I struggle to see how that'd be relevant to validation, but it's possible nevertheless!). The difference here is analogous to the difference between a classic parser combinator library vs a
suspend
-based, function-based parsing library.
constrain
might be nicer as vararg parameters to the constructor, instead of a lambda. I don't see on the surface why type-specific validators are necessary, but you probably have a good reason for it. It just seems to me that
Validator<String>
might work just as well with extension functions, or even
Validator<T>
with
T: Number
as a replacement for
NumberValidator<T>
I don't really get
ObjectFactory
. It seems to me easier to make the
Person
first then validate. This might be where an Arrow-like approach shines.
👍 2
n
I would suggest to also either add a
.not()
or e.g. a
doesNotMatch(r: RegExp)
, because that makes it much easier to e.g. validate that a string does not contain "PO Box".
👍 1
t
Thank you for the insightful feedback. Every point you raised seems worth considering. I’ve known about Arrow for a while, but I never fully understood what features it provides, so this seems like a good opportunity for me to explore it.
I don’t see on the surface why type-specific validators are necessary,
The reason I’m creating type-specific validators is that the operations I want to provide differ by type. For example, Validator<String> offers operations like uppercase(), while Validator<Int> does not.
I don’t really get ObjectFactory. It seems to me easier to make the Person first then validate.
You’re absolutely right. Still, I have a motivation to keep domain models always in a valid state. For that reason, I’m experimenting with an approach where all arguments are validated before instantiating the object.
I would suggest to also either add a .not() or e.g. a doesNotMatch(r: RegExp)
Thank you for the great suggestion. I’ll give it some serious consideration.
y
For example, Validator<String> offers operations like uppercase(), while Validator<Int> does not.
but that's exactly my point. You can make extension functions on
Validator<String>
instead of having a special
StringValidator
type
t
I see. So extension functions defined for
Validator<String>
and
Validator<Int>
are treated separately. Indeed, in the example I mentioned earlier, it seems that
StringValidator
would no longer be necessary. However, in cases like the following where the signatures are identical, I believe a concrete class is needed to avoid a platform declaration clash.
Copy code
fun interface Validator<T> {
    fun execute(input: T): Boolean
}

fun Validator<String>.max(value: Int): Validator<String> = Validator {
    it.length <= value
}
fun Validator<Int>.max(value: Int): Validator<Int> = Validator {
    it <= value
}

val stringValidator = Validator<String> { true }

// platform declaration clash: The following declarations have the same JVM signature
stringValidator.max(10).execute("abc").let { println(it) }
y
You can always use
@JvmName("maxString")
to prevent such a clash. I think it can really cut down on the amount of classes you have, and hence make the library less complex and easier to maintain
t
I didn’t know about
@JvmName
. I’ll give it a try. Thanks for letting me know!
y
Oh also, if they're in separate files, then there's no issue anyway because each declaration is then unique in its "class" (because a file
foo.kt
generates a class
FooKt
)
t
I was able to express most of the validators using extension functions! Thank you for the advice. https://github.com/komapper/kova/pull/26
👀 1
y
Power assert integrations would be rather nice! It can make
constraint
writing incredibly simple, since there's no need to craft your own messages anymore, the compiler plugin takes care of it!
t
Thank you for the suggestion. I tried introducing Power Assert: https://github.com/komapper/kova/pull/29 However, I’m not quite sure how it actually makes things simpler. I don’t have a clear picture yet.
y
@Toshihiro Nakamura I had some fun over the weekend introducing Arrow to the library, and simplifying it down to be in direct style, as in, you don't pass `Validator`s around, but you call validation functions instead. I'm not opening a PR because I have no expectation that this'll be merged or anything lol. I figured it was an interesting exercise. Each commit is fully working, and so some of the intermediary commits could be useful for you since they're less overreaching, but still help simplify things (in particular, this is the commit right before the direct style refactoring, so it may be useful without changing the interface of the library too much) I really like the final result, though. Using it is a lot simpler IMO. There's also just less to maintain overall. My only gripe is that I couldn't get `or`ing to look elegant.
❤️ 1
t
Thanks for sharing this! The direct-style approach with Arrow is really interesting, and the incremental commits are especially helpful. The final result looks quite clean—definitely gives me something to think about.