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

Simple validation annotations that don't rely on Jakarta Bean Validation #1006

Open
yrodiere opened this issue Sep 22, 2023 · 2 comments
Open
Labels
enhancement New feature or request

Comments

@yrodiere
Copy link
Contributor

yrodiere commented Sep 22, 2023

Use case

In Quarkus we use Smallrye Config and @ConfigMapping for framework configuration.

This works great, but in that context we can't require additional dependencies (such as Jakarta Bean Validation and implementation), since that would force every single Quarkus application to depend on that.

Feature

It would be nice to provide some very simple validation annotations for such situation. We don't need a full-blown validation framework, just some simple validations: ranges for comparables types (integers, ...), maybe patterns for some edge cases (though those could be handled with a custom converter).

Implementation idea

The backing converter wrappers are already available in Smallrye Config:

/**
* Get a wrapping converter which verifies that the configuration value is greater than, or optionally equal to,
* the given minimum value.
*
* @param delegate the delegate converter (must not be {@code null})
* @param minimumValue the minimum value (must not be {@code null})
* @param inclusive {@code true} if the minimum value is inclusive, {@code false} otherwise
* @param <T> the converter target type
* @return a range-validating converter
*/
public static <T extends Comparable<T>> Converter<T> minimumValueConverter(Converter<? extends T> delegate, T minimumValue,
boolean inclusive) {
return new RangeCheckConverter<>(Comparator.naturalOrder(), delegate, minimumValue, inclusive, null, false);
}
/**
* Get a wrapping converter which verifies that the configuration value is greater than, or optionally equal to,
* the given minimum value.
*
* @param comparator the comparator to use (must not be {@code null})
* @param delegate the delegate converter (must not be {@code null})
* @param minimumValue the minimum value (must not be {@code null})
* @param inclusive {@code true} if the minimum value is inclusive, {@code false} otherwise
* @param <T> the converter target type
* @return a range-validating converter
*/
public static <T> Converter<T> minimumValueConverter(Comparator<? super T> comparator, Converter<? extends T> delegate,
T minimumValue, boolean inclusive) {
return new RangeCheckConverter<>(comparator, delegate, minimumValue, inclusive, null, false);
}
/**
* Get a wrapping converter which verifies that the configuration value is greater than, or optionally equal to,
* the given minimum value (in string form).
*
* @param delegate the delegate converter (must not be {@code null})
* @param minimumValue the minimum value (must not be {@code null})
* @param inclusive {@code true} if the minimum value is inclusive, {@code false} otherwise
* @param <T> the converter target type
* @return a range-validating converter
* @throws IllegalArgumentException if the given minimum value fails conversion
*/
public static <T extends Comparable<T>> Converter<T> minimumValueStringConverter(Converter<? extends T> delegate,
String minimumValue, boolean inclusive) {
return minimumValueConverter(delegate, delegate.convert(minimumValue), inclusive);
}
/**
* Get a wrapping converter which verifies that the configuration value is greater than, or optionally equal to,
* the given minimum value (in string form).
*
* @param comparator the comparator to use (must not be {@code null})
* @param delegate the delegate converter (must not be {@code null})
* @param minimumValue the minimum value (must not be {@code null})
* @param inclusive {@code true} if the minimum value is inclusive, {@code false} otherwise
* @param <T> the converter target type
* @return a range-validating converter
* @throws IllegalArgumentException if the given minimum value fails conversion
*/
public static <T> Converter<T> minimumValueStringConverter(Comparator<? super T> comparator,
Converter<? extends T> delegate, String minimumValue, boolean inclusive) {
return minimumValueConverter(comparator, delegate, delegate.convert(minimumValue), inclusive);
}
/**
* Get a wrapping converter which verifies that the configuration value is less than, or optionally equal to,
* the given maximum value.
*
* @param delegate the delegate converter (must not be {@code null})
* @param maximumValue the maximum value (must not be {@code null})
* @param inclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
* @param <T> the converter target type
* @return a range-validating converter
*/
public static <T extends Comparable<T>> Converter<T> maximumValueConverter(Converter<? extends T> delegate, T maximumValue,
boolean inclusive) {
return new RangeCheckConverter<>(Comparator.naturalOrder(), delegate, null, false, maximumValue, inclusive);
}
/**
* Get a wrapping converter which verifies that the configuration value is less than, or optionally equal to,
* the given maximum value.
*
* @param comparator the comparator to use (must not be {@code null})
* @param delegate the delegate converter (must not be {@code null})
* @param maximumValue the maximum value (must not be {@code null})
* @param inclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
* @param <T> the converter target type
* @return a range-validating converter
*/
public static <T> Converter<T> maximumValueConverter(Comparator<? super T> comparator, Converter<? extends T> delegate,
T maximumValue, boolean inclusive) {
return new RangeCheckConverter<>(comparator, delegate, null, false, maximumValue, inclusive);
}
/**
* Get a wrapping converter which verifies that the configuration value is less than, or optionally equal to,
* the given maximum value (in string form).
*
* @param delegate the delegate converter (must not be {@code null})
* @param maximumValue the maximum value (must not be {@code null})
* @param inclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
* @param <T> the converter target type
* @return a range-validating converter
* @throws IllegalArgumentException if the given maximum value fails conversion
*/
public static <T extends Comparable<T>> Converter<T> maximumValueStringConverter(Converter<? extends T> delegate,
String maximumValue, boolean inclusive) {
return maximumValueConverter(delegate, delegate.convert(maximumValue), inclusive);
}
/**
* Get a wrapping converter which verifies that the configuration value is less than, or optionally equal to,
* the given maximum value (in string form).
*
* @param comparator the comparator to use (must not be {@code null})
* @param delegate the delegate converter (must not be {@code null})
* @param maximumValue the maximum value (must not be {@code null})
* @param inclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
* @param <T> the converter target type
* @return a range-validating converter
* @throws IllegalArgumentException if the given maximum value fails conversion
*/
public static <T> Converter<T> maximumValueStringConverter(Comparator<? super T> comparator,
Converter<? extends T> delegate, String maximumValue, boolean inclusive) {
return maximumValueConverter(comparator, delegate, delegate.convert(maximumValue), inclusive);
}
/**
* Get a wrapping converter which verifies that the configuration value is within the given range.
*
* @param delegate the delegate converter (must not be {@code null})
* @param maximumValue the maximum value (must not be {@code null})
* @param maxInclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
* @param <T> the converter target type
* @return a range-validating converter
*/
public static <T extends Comparable<T>> Converter<T> rangeValueConverter(Converter<? extends T> delegate, T minimumValue,
boolean minInclusive, T maximumValue, boolean maxInclusive) {
return new RangeCheckConverter<>(Comparator.naturalOrder(), delegate, minimumValue, minInclusive, maximumValue,
maxInclusive);
}
/**
* Get a wrapping converter which verifies that the configuration value is within the given range.
*
* @param comparator the comparator to use (must not be {@code null})
* @param delegate the delegate converter (must not be {@code null})
* @param maximumValue the maximum value (must not be {@code null})
* @param maxInclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
* @param <T> the converter target type
* @return a range-validating converter
*/
public static <T> Converter<T> rangeValueConverter(Comparator<? super T> comparator, Converter<? extends T> delegate,
T minimumValue, boolean minInclusive, T maximumValue, boolean maxInclusive) {
return new RangeCheckConverter<>(comparator, delegate, minimumValue, minInclusive, maximumValue, maxInclusive);
}
/**
* Get a wrapping converter which verifies that the configuration value is within the given range (in string form).
*
* @param delegate the delegate converter (must not be {@code null})
* @param maximumValue the maximum value (must not be {@code null})
* @param maxInclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
* @param <T> the converter target type
* @return a range-validating converter
* @throws IllegalArgumentException if the given minimum or maximum value fails conversion
*/
public static <T extends Comparable<T>> Converter<T> rangeValueStringConverter(Converter<? extends T> delegate,
String minimumValue, boolean minInclusive, String maximumValue, boolean maxInclusive) {
return rangeValueConverter(delegate, delegate.convert(minimumValue), minInclusive, delegate.convert(maximumValue),
maxInclusive);
}
/**
* Get a wrapping converter which verifies that the configuration value is within the given range (in string form).
*
* @param comparator the comparator to use (must not be {@code null})
* @param delegate the delegate converter (must not be {@code null})
* @param maximumValue the maximum value (must not be {@code null})
* @param maxInclusive {@code true} if the maximum value is inclusive, {@code false} otherwise
* @param <T> the converter target type
* @return a range-validating converter
* @throws IllegalArgumentException if the given minimum or maximum value fails conversion
*/
public static <T> Converter<T> rangeValueStringConverter(Comparator<? super T> comparator, Converter<? extends T> delegate,
String minimumValue, boolean minInclusive, String maximumValue, boolean maxInclusive) {
return rangeValueConverter(comparator, delegate, delegate.convert(minimumValue), minInclusive,
delegate.convert(maximumValue), maxInclusive);
}
/**
* Get a wrapping converter which verifies that the configuration value matches the given pattern.
*
* @param delegate the delegate converter (must not be {@code null})
* @param pattern the pattern to match (must not be {@code null})
* @param <T> the converter target type
* @return a pattern-validating converter
*/
public static <T> Converter<T> patternValidatingConverter(Converter<? extends T> delegate, Pattern pattern) {
return new PatternCheckConverter<>(delegate, pattern);
}
/**
* Get a wrapping converter which verifies that the configuration value matches the given pattern.
*
* @param delegate the delegate converter (must not be {@code null})
* @param pattern the pattern string to match (must not be {@code null})
* @param <T> the converter target type
* @return a pattern-validating converter
* @throws PatternSyntaxException if the given pattern has invalid syntax
*/
public static <T> Converter<T> patternValidatingConverter(Converter<? extends T> delegate, String pattern) {
return patternValidatingConverter(delegate, Pattern.compile(pattern));
}

One possibility would be to provide annotations such as these:

public @interface WithMin {

    String value();

    // Maybe... Jakarta Validation itself doesn't provide the option,
    // so we could just assume everything is inclusive too.
    boolean inclusive default true;

    // Defaults to natural order for types implementing Comparable,
    // but might require customization for other types?
    Class<? extends Comparator<?>> comparator() default ComparableComparator.class;

}

public @interface WithMax {

    String value();

    boolean inclusive() default true;

    Class<? extends Comparator<?>> comparator() default ComparableComparator.class;

}

public @interface WithRange {

    String min();

    boolean minInclusive() default true;

    String max();

    boolean maxInclusive() default true;

    Class<? extends Comparator<?>> comparator() default ComparableComparator.class;

}

// Maybe... like I mentioned above, this would be much less useful
public @interface WithPattern {

    String regexp();

}
@yrodiere yrodiere changed the title Simple validation annotations that don't rely on Jakarta Validation Simple validation annotations that don't rely on Jakarta Bean Validation Sep 22, 2023
@dmlloyd
Copy link
Contributor

dmlloyd commented Sep 22, 2023

An important feature of this proposal, and an important limitation of using the BV annotations, is that by giving our min/max values as a string, we can apply the same converter to the min/max values to ensure that we support any min/max values.

+1 to allowing a custom comparator for the comparison, +1 to this proposal overall.

@dmlloyd
Copy link
Contributor

dmlloyd commented Sep 22, 2023

The annotations should apply at the same scope as the @WithConverter to appropriately decorate it, so for example I could have a property like:

    List<@WithConverter(MyHexConverter.class) @WithMin("0x01") Integer> myPositiveIntegerList();

@radcortez radcortez added the enhancement New feature or request label Jan 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants