-
Notifications
You must be signed in to change notification settings - Fork 15
Refactor validator creation to support various configurations #283
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
Conversation
@@ -51,6 +51,7 @@ class EvaluatorBuilder { | |||
|
|||
private final Env env; | |||
private final boolean disableLazy; | |||
private final List<Descriptor> descriptors; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This only looks to be referenced in the constructor. We should be able to remove this field and just iterate over the constructor arg below.
* be created with a list of seed descriptors to warmup the validator cache ahead of time as well as | ||
* an indicator to lazily-load any descriptors not provided into the cache. | ||
*/ | ||
public class ValidatorFactory { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public class ValidatorFactory { | |
public final class ValidatorFactory { |
public class ValidatorFactory { | ||
// Prevent instantiation | ||
private ValidatorFactory() {} | ||
; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
; |
* @throws IllegalStateException If disableLazy is set to true and no descriptors are passed. | ||
*/ | ||
public Validator build() throws CompilationException, IllegalStateException { | ||
if (disableLazy && this.descriptors.size() == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (disableLazy && this.descriptors.size() == 0) { | |
if (disableLazy && this.descriptors.isEmpty()) { |
} | ||
|
||
/** An abstract class that all validator builders should extend. */ | ||
abstract class BaseBuilder<T extends BaseBuilder<T>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be made static and lifted into the ValidatorFactory class or moved to a separate .java file.
* makes the entire cache read-only, eliminating thread contention. | ||
* @return A Validator builder | ||
*/ | ||
public static EagerBuilder newBuilder(List<Descriptor> descriptors, boolean disableLazy) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps a simpler design would be to have a single builder class, with two build methods:
public Validator build() {}
public Validator buildWithDescriptors(List<Descriptor> descriptors, boolean disableLazy) throws CompilationException, IllegalStateException;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair. To do that, we'd have to create the EvaluatorBuilder
in the builder and keep it in state so that it can be passed to the ValidatorImpl
constructor. Not sure if that's desirable or not. It definitely simplifies the design but at the cost of having more logic in the actual builder itself. Wdyt?
* @return A Validator instance | ||
*/ | ||
public static Validator defaultInstance() { | ||
return newBuilder().build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should leave this out - it isn't that much more to type to have someone just do ValidatorFactory.newBuilder().build()
. We could always add it back if desired.
This method as written feels like it is returning the same instance each time (similar to the global validator in pv-go: https://pkg.go.dev/buf.build/go/protovalidate#Validator), not a new instance each time w/ the default config.
this.disableLazy = disableLazy; | ||
this.rules = new RuleCache(env, config); | ||
|
||
for (Descriptor descriptor : this.descriptors) { | ||
this.build(descriptor); | ||
if (descriptors != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nil check isn't needed here - if anything we could add an Objects.requireNonNull(descriptors)
above if we wanted with an error message. Doesn't hurt but typically you never pass around null collections.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without the check, it throws an NPE though: java.lang.NullPointerException: Cannot invoke "java.util.List.iterator()"
. I think we should do something here to catch that. Seems like an easy thing to prevent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would only happen though if someone passes null
to the buildDescriptors
method, right?
Typically if you wanted a nicer error you'd do Objects.requireNonNull(descriptors, "descriptors must not be null")
as the first line in the constructor (or the equivalent Preconditions.checkNotNull from Guava).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, removed the null check and added the requireNonNull
check instead.
Co-authored-by: Philip K. Warren <[email protected]>
Co-authored-by: Philip K. Warren <[email protected]>
Fixes #51
This makes a series of changes to the way that a validator is created. Doing so enables more flexible ways to configure a validator and also provides a bit more safety when attempting to create a validator with invalid state. The impetus behind this change was to allow for the ability to specify a list of seed descriptors when building a validator and for the optional ability to disable lazy-loading of any descriptors not provided.
Previously, a validator could be created either with a default constructor (
new Validator()
) or via aConfig
object (new Validator(config)
). However, theConfig
object had no way to provide a list of descriptors, it only had adisableLazy
field. Simply adding a list of descriptors to theConfig
presented a few problems though:disableLazy
to true and not specify any descriptors. Doing so would return an error at validation time with a validation message about an unknown evaluator.CompilationException
. This meant that if the seed descriptors were added to the config, all users creating a validator would now have to catch a checked exception even if they weren't using the seed descriptors.The solution in this PR converts validator creation from using constructors into a builder pattern with the following changes:
disableLazy
and the newdescriptors
list have been moved from the config and are now parameters at instantiation time.build()
buildWithDescriptors(List<Descriptor>, boolean disableLazy) throws CompilationException, InvalidStateException
The overload that accepts seed descriptors is the only one that throws a
CompilationException
. If users choose to usebuild
with no arguments, no checked exception needs to be handled.Note that the above also allows us to provide a helpful check at instantiation time if a user sets
disableLazy
to true but doesn't provide any descriptors. In this case, anIllegalStateException
is thrown. It is still possible to provide an incorrect list of descriptors, but that is impossible to catch at instantiation time (but hopefully is now a bit harder to do).The new API now adds a
ValidatorFactory
class with static methods for creating a validator. In addition, the type returned is now an interface (still namedValidator
) which provides us with a bit more flexibility both for testing and for making non-breaking changes in the future.Examples of creating a validator with no seed descriptors:
Creating a validator with seed descriptors is similar. You would still call
newBuilder
, but at build time, you callbuildWithDescriptors
and pass the descriptors and setdisableLazy
accordingly. Using this approach requires catching exceptions at build time:Note: THIS IS A BREAKING CHANGE, but the hope is that this makes it easier and more intuitive for creating a validator while providing future-proof flexibility. To migrate, users should make the following changes:
With no config
With config
In addition, the
loadMessages
andloadDescriptors
methods from the validator have been removed. Users should instead use thenewBuilder(List<Descriptor> descriptors, boolean disableLazy)
approach to load descriptors ahead of time.