Skip to content

Commit

Permalink
feat(crd-generator): add support for more validation constraints (6447)
Browse files Browse the repository at this point in the history
Add support for exclusiveMinimum and exclusiveMaximum (#5868)
---
Fix annotation description
---
Add support for minLength and maxLength (#5836)
---
Add support for minItems and maxItems (#5836)
---
Add support for minProperties and maxProperties (#5836)
---
Add tests for type annotations and cleanup
---
Add type support for strings Lists and Maps using `@Size`
---
Fix javadoc in Size annotation
---
Fix approval test
---
Rename group in approvaltest
---
Add default value handling again
---
Updated docs to describe inclusive/exclusive and `@Size` annotation
---
Fix typo
---
Cleanup SpecReplicasPathTest
---
Add unit tests
---
Add changelog entries
  • Loading branch information
baloo42 authored Dec 3, 2024
1 parent 5cab66c commit b3a5fea
Show file tree
Hide file tree
Showing 13 changed files with 1,190 additions and 52 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#### Improvements
* Fix #3069: (crd-generator) Add `@AdditionalPrinterColumn` to specify a printer column by JSON path.
* Fix #6392: (crd-generator) Add `@AdditionalSelectableField` and `@SelectableField` to specify selectable fields.
* Fix #5836: (crd-generator) Add `@Size` annotation to limit the size of strings, lists/arrays or maps
* Fix #5868: (crd-generator) Add `exlusiveMinimum` / `exclusiveMaximum` support to `@Min` and `@Max`
* Fix #5264: Remove deprecated `Config.errorMessages` field
* Fix #6008: removing the optional dependency on bouncy castle
* Fix #6407: sundrio builder-annotations is not available via bom import
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema;
import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema.Items;
import com.fasterxml.jackson.module.jsonSchema.types.IntegerSchema;
import com.fasterxml.jackson.module.jsonSchema.types.NumberSchema;
import com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema;
import com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema.SchemaAdditionalProperties;
import com.fasterxml.jackson.module.jsonSchema.types.ReferenceSchema;
Expand All @@ -43,6 +46,7 @@
import io.fabric8.generator.annotation.Nullable;
import io.fabric8.generator.annotation.Pattern;
import io.fabric8.generator.annotation.Required;
import io.fabric8.generator.annotation.Size;
import io.fabric8.generator.annotation.ValidationRule;
import io.fabric8.generator.annotation.ValidationRules;
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
Expand Down Expand Up @@ -230,8 +234,16 @@ class PropertyMetadata {
private final String description;
private final Object defaultValue;
private Double min;
private Boolean exclusiveMinimum;
private Double max;
private Boolean exclusiveMaximum;
private String pattern;
private Long minLength;
private Long maxLength;
private Long minItems;
private Long maxItems;
private Long minProperties;
private Long maxProperties;
private boolean nullable;
private String format;
private List<V> validationRules = new ArrayList<>();
Expand All @@ -254,15 +266,55 @@ public PropertyMetadata(JsonSchema value, BeanProperty beanProperty) {
if (value.isStringSchema()) {
StringSchema stringSchema = value.asStringSchema();
// only set if ValidationSchemaFactoryWrapper is used
this.pattern = stringSchema.getPattern();
//this.maxLength = ofNullable(stringSchema.getMaxLength()).map(Integer::doubleValue).orElse(null);
//this.minLength = ofNullable(stringSchema.getMinLength()).map(Integer::doubleValue).orElse(null);
} else {
// TODO: process the other schema types for validation values
pattern = ofNullable(beanProperty.getAnnotation(Pattern.class)).map(Pattern::value)
.or(() -> ofNullable(stringSchema.getPattern()))
.orElse(null);
minLength = findMinInSizeAnnotation(beanProperty)
.or(() -> ofNullable(stringSchema.getMinLength()).map(Integer::longValue))
.orElse(null);
maxLength = findMaxInSizeAnnotation(beanProperty)
.or(() -> ofNullable(stringSchema.getMaxLength()).map(Integer::longValue))
.orElse(null);
} else if (value.isIntegerSchema()) {
// integerschema extends numberschema and must handled first
IntegerSchema integerSchema = value.asIntegerSchema();
setMinMax(beanProperty,
integerSchema.getMinimum(),
integerSchema.getExclusiveMinimum(),
integerSchema.getMaximum(),
integerSchema.getExclusiveMaximum());
} else if (value.isNumberSchema()) {
NumberSchema numberSchema = value.asNumberSchema();
setMinMax(beanProperty,
numberSchema.getMinimum(),
numberSchema.getExclusiveMinimum(),
numberSchema.getMaximum(),
numberSchema.getExclusiveMaximum());
} else if (value.isArraySchema()) {
ArraySchema arraySchema = value.asArraySchema();
minItems = findMinInSizeAnnotation(beanProperty)
.or(() -> ofNullable(arraySchema.getMinItems()).map(Integer::longValue))
.orElse(null);
maxItems = findMaxInSizeAnnotation(beanProperty)
.or(() -> ofNullable(arraySchema.getMaxItems()).map(Integer::longValue))
.orElse(null);
} else if (value.isObjectSchema()) {
// TODO: Could be also applied only on Maps instead of "all the rest"
minProperties = findMinInSizeAnnotation(beanProperty)
.orElse(null);
maxProperties = findMaxInSizeAnnotation(beanProperty)
.orElse(null);
}

collectValidationRules(beanProperty, validationRules);

// TODO: should probably move to a standard annotations
// see ValidationSchemaFactoryWrapper
nullable = beanProperty.getAnnotation(Nullable.class) != null;

// TODO: should the following be deprecated?
required = beanProperty.getAnnotation(Required.class) != null;

if (beanProperty.getMetadata().getDefaultValue() != null) {
defaultValue = toTargetType(beanProperty.getType(), beanProperty.getMetadata().getDefaultValue());
} else if (ofNullable(beanProperty.getAnnotation(Default.class)).map(Default::value).isPresent()) {
Expand All @@ -271,16 +323,34 @@ public PropertyMetadata(JsonSchema value, BeanProperty beanProperty) {
} else {
defaultValue = null;
}
}

// TODO: should probably move to a standard annotations
// see ValidationSchemaFactoryWrapper
nullable = beanProperty.getAnnotation(Nullable.class) != null;
max = ofNullable(beanProperty.getAnnotation(Max.class)).map(Max::value).orElse(max);
min = ofNullable(beanProperty.getAnnotation(Min.class)).map(Min::value).orElse(min);

// TODO: should the following be deprecated?
required = beanProperty.getAnnotation(Required.class) != null;
pattern = ofNullable(beanProperty.getAnnotation(Pattern.class)).map(Pattern::value).orElse(pattern);
private void setMinMax(BeanProperty beanProperty,
Double minimum, Boolean exclusiveMinimum, Double maximum, Boolean exclusiveMaximum) {
ofNullable(minimum).ifPresent(v -> {
this.min = v;
if (Boolean.TRUE.equals(exclusiveMinimum)) {
this.exclusiveMinimum = true;
}
});
ofNullable(beanProperty.getAnnotation(Min.class)).ifPresent(a -> {
min = a.value();
if (!a.inclusive()) {
this.exclusiveMinimum = true;
}
});
ofNullable(maximum).ifPresent(v -> {
this.max = v;
if (Boolean.TRUE.equals(exclusiveMaximum)) {
this.exclusiveMaximum = true;
}
});
ofNullable(beanProperty.getAnnotation(Max.class)).ifPresent(a -> {
this.max = a.value();
if (!a.inclusive()) {
this.exclusiveMaximum = true;
}
});
}

public void updateSchema(T schema) {
Expand All @@ -294,10 +364,22 @@ public void updateSchema(T schema) {
}
}
if (nullable) {
schema.setNullable(nullable);
schema.setNullable(true);
}
schema.setMaximum(max);
schema.setExclusiveMaximum(exclusiveMaximum);
schema.setMinimum(min);
schema.setExclusiveMinimum(exclusiveMinimum);

schema.setMinLength(minLength);
schema.setMaxLength(maxLength);

schema.setMinItems(minItems);
schema.setMaxItems(maxItems);

schema.setMinProperties(minProperties);
schema.setMaxProperties(maxProperties);

schema.setPattern(pattern);
schema.setFormat(format);
if (preserveUnknownFields) {
Expand All @@ -306,6 +388,18 @@ public void updateSchema(T schema) {

addToValidationRules(schema, validationRules);
}

private Optional<Long> findMinInSizeAnnotation(BeanProperty beanProperty) {
return ofNullable(beanProperty.getAnnotation(Size.class))
.map(Size::min)
.filter(v -> v > 0);
}

private Optional<Long> findMaxInSizeAnnotation(BeanProperty beanProperty) {
return ofNullable(beanProperty.getAnnotation(Size.class))
.map(Size::max)
.filter(v -> v < Long.MAX_VALUE);
}
}

private T resolveObject(LinkedHashMap<String, String> visited, InternalSchemaSwaps schemaSwaps, JsonSchema jacksonSchema,
Expand Down Expand Up @@ -514,24 +608,49 @@ private void handleTypeAnnotations(final T schema, BeanProperty beanProperty, Cl

AnnotatedElement member = beanProperty.getMember().getAnnotated();
AnnotatedType fieldType = null;
AnnotatedType type = null;
AnnotatedType methodType = null;
if (member instanceof Field) {
fieldType = ((Field) member).getAnnotatedType();
} else if (member instanceof Method) {
fieldType = getFieldForMethod(beanProperty).map(Field::getAnnotatedType).orElse(null);
type = ((Method) member).getAnnotatedReceiverType();
methodType = ((Method) member).getAnnotatedReceiverType();
}

Stream.of(fieldType, type)
Stream.of(fieldType, methodType)
.filter(o -> !Objects.isNull(o))
.filter(AnnotatedParameterizedType.class::isInstance)
.map(AnnotatedParameterizedType.class::cast)
.map(AnnotatedParameterizedType::getAnnotatedActualTypeArguments)
.map(a -> a[typeIndex])
.forEach(at -> {
Optional.ofNullable(at.getAnnotation(Pattern.class)).ifPresent(a -> schema.setPattern(a.value()));
Optional.ofNullable(at.getAnnotation(Min.class)).ifPresent(a -> schema.setMinimum(a.value()));
Optional.ofNullable(at.getAnnotation(Max.class)).ifPresent(a -> schema.setMaximum(a.value()));
if ("string".equals(schema.getType())) {
ofNullable(at.getAnnotation(Pattern.class))
.ifPresent(a -> schema.setPattern(a.value()));

ofNullable(at.getAnnotation(Size.class))
.map(Size::min)
.filter(v -> v > 0)
.ifPresent(schema::setMinLength);

ofNullable(at.getAnnotation(Size.class))
.map(Size::max)
.filter(v -> v < Long.MAX_VALUE)
.ifPresent(schema::setMaxLength);

} else if ("number".equals(schema.getType()) || "integer".equals(schema.getType())) {
ofNullable(at.getAnnotation(Min.class)).ifPresent(a -> {
schema.setMinimum(a.value());
if (!a.inclusive()) {
schema.setExclusiveMinimum(true);
}
});
ofNullable(at.getAnnotation(Max.class)).ifPresent(a -> {
schema.setMaximum(a.value());
if (!a.inclusive()) {
schema.setExclusiveMaximum(true);
}
});
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,24 @@ public interface KubernetesJSONSchemaProps {

void setMaximum(Double max);

void setExclusiveMaximum(Boolean b);

void setMinimum(Double min);

void setExclusiveMinimum(Boolean b);

void setMinLength(Long min);

void setMaxLength(Long max);

void setMinItems(Long min);

void setMaxItems(Long max);

void setMinProperties(Long min);

void setMaxProperties(Long max);

void setPattern(String pattern);

void setFormat(String format);
Expand Down
Loading

0 comments on commit b3a5fea

Please sign in to comment.