Preconfigured validators
Akkurate is currently missing global configurations, this could be brought with preconfigured validators:
// Declare as many preconfigured validators you need, instead of a single global configuration.
val BooksValidator = Validator.preconfigured(Configuration {
rootPath("books")
})
val OrdersValidator = Validator.preconfigured(Configuration {
rootPath("orders")
})
// For each preconfigured validator, you can override its configuration, provide
// contextual values, and even use suspendable code.
fun main() {
// Simple validator
BooksValidator<String> {
isNotEmpty()
}
// Configuration overridden
BooksValidator<String>(Configuration { defaultViolationMessage = "This is wrong" }) {
isNotEmpty()
}
// With contextual values
BooksValidator<SomeDao, String> { dao ->
isNotEmpty()
constrain { /* use the dao */ }
}
// Suspendable validator
BooksValidator.suspendable<String> {
isNotEmpty()
// call some suspendable code
}
}
Here's the implementation details:
The factory methods should be extracted to an interface, let's name it ValidatorFactory. Then the Validator.Companion object extends this interface, but the implementation can be probably stay the same than nowadays.
We must add a preconfigured(Configuration): ValidatorFactory method to the Validator.Companion object. The returned value is an instance of some class (PreconfiguredValidatorFactory?) that implements ValidatorFactory and stores the Configuration provided to the preconfigured method.
A next step could be to allow partial configuration overriding of the preconfigured validators, but let's ignore this for the moment, we'll think about this later.
Discussed in https://github.com/nesk/akkurate/discussions/49
Originally posted by bejibx November 29, 2024
Hi! I notice that right now the only way to change validation config is to explicitly pass configuration object during Validator creation. This could be cumbersome if you want to change your configuration globally. One example might be to collect violations in debug version of your app but fail on first one in release version for better performance. Right now to do something like this you have to either pass config into every validator or to declare some wrapper method to create validators inside your code. I don't like both solutions so I'm curious if you see as an reasonable solution to instead add to the library some way to specify default globall configuration? If you ok with the idea, I'm ready to implement it.
Oh, by the way another bonus of keeping default configuration instance somewhere would be reduced number of object creation. If I'm not mistaken right now every Validator creation also creates 2 instances of Configuration (one for default value in Validator.invoke(...), another for default value in Configuration.invoke(...)). Instead we can just point to our default same instance. This might sound silly for some people, but I worked on some mobile projects where I had to do some evil black magic to decrease number of creating objects to reduce garbage collector load.
Hi! Sounds pretty much how I imagined this and I already had some thoughts about it. This implementation has one problem - it is not possible to override default method values in kotlin. So right now we have default configuration instances in every factory method. it seems like we need to change this value to be configuration passed during instantiation for preconfigured factory but as I said, kotlin not allowing this. Is this interface really needed? I can just copy-paste factory methods into new class. Not pretty but this way I can chage default configuration values.
You're right, I thought it would be possible for the implementation to override the default value provided by the interface, but it's not.
The reason I want an interface here is because someone might someday need create a function like this:
fun createValidator(factory: ValidatorFactory = Validator.Companion)
This allows to treat the default factory Validator.Companion the same way than preconfigured validators.
Another option would be to give up on the default parameter and create method overloads:
public interface ValidatorFactory {
public operator fun <ContextType, ValueType> invoke(
block: Validatable<ValueType>.(context: ContextType) -> Unit,
): Runner.WithContext<ContextType, ValueType>
public operator fun <ContextType, ValueType> invoke(
configuration: Configuration,
block: Validatable<ValueType>.(context: ContextType) -> Unit,
): Runner.WithContext<ContextType, ValueType>
public operator fun <ValueType> invoke(
block: Validatable<ValueType>.() -> Unit,
): Runner<ValueType>
public operator fun <ValueType> invoke(
configuration: Configuration,
block: Validatable<ValueType>.() -> Unit,
): Runner<ValueType>
public fun <ContextType, ValueType> suspendable(
block: suspend Validatable<ValueType>.(context: ContextType) -> Unit,
): SuspendableRunner.WithContext<ContextType, ValueType>
public fun <ContextType, ValueType> suspendable(
configuration: Configuration,
block: suspend Validatable<ValueType>.(context: ContextType) -> Unit,
): SuspendableRunner.WithContext<ContextType, ValueType>
public fun <ValueType> suspendable(
block: suspend Validatable<ValueType>.() -> Unit,
): SuspendableRunner<ValueType>
public fun <ValueType> suspendable(
configuration: Configuration,
block: suspend Validatable<ValueType>.() -> Unit,
): SuspendableRunner<ValueType>
}
I have another option in mind. After https://github.com/nesk/akkurate/issues/52 will be implemented we can do something like this
class PreconfiguredValidatorFactory(
private val userConfiguration: Configuration,
) : ValidatorFactory {
public override operator fun <ContextType, ValueType> invoke(
configuration: Configuration = defaultConfiguration,
block: Validatable<ValueType>.(context: ContextType) -> Unit,
): Runner.WithContext<ContextType, ValueType> {
val configurationToUse = configuration.takeIf { it !== defaultConfiguration } ?: userConfiguration
TODO()
}
}
This doesn't seem to be an option because the default value is part of the method signature. Here's an example:
It would be weird for the users (and the maintainers) to see the default value being defaultConfiguration in a preconfigured validator.