Skip to content

Commit

Permalink
fix(crd-generator): allow limited use of type annotations (6322)
Browse files Browse the repository at this point in the history
fix: allow limited use of type annotations

closes: #6282

Signed-off-by: Steve Hawkins <[email protected]>
  • Loading branch information
shawkins authored Sep 17, 2024
1 parent ab5fdae commit cc33729
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Fix #6008: removing the optional dependency on bouncy castle
* Fix #6230: introduced Quantity.multiply(int) to allow for Quantity multiplication by an integer
* Fix #6281: use GitHub binary repo for Kube API Tests
* Fix #6282: Allow annotated types with Pattern, Min, and Max with Lists and Maps and CRD generation
* Fix #5480: Move `io.fabric8:zjsonpatch` to KubernetesClient project

#### Dependency Upgrade
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema.Items;
Expand Down Expand Up @@ -56,6 +55,9 @@
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedParameterizedType;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
Expand All @@ -68,6 +70,7 @@
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
Expand Down Expand Up @@ -130,10 +133,9 @@ public Map<String, AnnotationMetadata> getAllPaths(Class<PrinterColumn> clazz) {

/**
* Creates the JSON schema for the class. This is template method where
* sub-classes are supposed to provide specific implementations of abstract methods.
* subclasses are supposed to provide specific implementations of abstract methods.
*
* @param definition The definition.
* @param ignore a potentially empty list of property names to ignore while generating the schema
* @return The schema.
*/
private T resolveRoot(Class<?> definition) {
Expand All @@ -143,7 +145,7 @@ private T resolveRoot(Class<?> definition) {
return resolveObject(new LinkedHashMap<>(), schemaSwaps, schema, "kind", "apiVersion", "metadata");
}
return resolveProperty(new LinkedHashMap<>(), schemaSwaps, null,
resolvingContext.objectMapper.getSerializationConfig().constructType(definition), schema);
resolvingContext.objectMapper.getSerializationConfig().constructType(definition), schema, null);
}

/**
Expand All @@ -157,32 +159,47 @@ private static <A extends Annotation> void consumeRepeatingAnnotation(Class<?> b
}
}

void collectValidationRules(BeanProperty beanProperty, List<V> validationRules) {
// TODO: the old logic allowed for picking up the annotation from both the getter and the field
// this requires a messy hack by convention because there doesn't seem to be a way to all annotations
// nor does jackson provide the field
if (beanProperty.getMember() instanceof AnnotatedMethod) {
Optional<Field> getFieldForMethod(BeanProperty beanProperty) {
AnnotatedElement annotated = beanProperty.getMember().getAnnotated();
if (annotated instanceof Method) {
// field first
Method m = ((AnnotatedMethod) beanProperty.getMember()).getMember();
Method m = (Method) annotated;
String name = m.getName();
if (name.startsWith("get") || name.startsWith("set")) {
name = name.substring(3);
} else if (name.startsWith("is")) {
name = name.substring(2);
}
if (name.length() > 0) {
if (!name.isEmpty()) {
name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
}

try {
return Optional.of(m.getDeclaringClass().getDeclaredField(name));
} catch (NoSuchFieldException | SecurityException e) {
// ignored
}
}
return Optional.empty();
}

void collectValidationRules(BeanProperty beanProperty, List<V> validationRules) {
// TODO: the old logic allowed for picking up the annotation from both the getter and the field
// this requires a messy hack by convention because there doesn't seem to be a way to all annotations
// nor does jackson provide the field
AnnotatedElement member = beanProperty.getMember().getAnnotated();
if (member instanceof Method) {
Optional<Field> field = getFieldForMethod(beanProperty);
try {
Field f = beanProperty.getMember().getDeclaringClass().getDeclaredField(name);
ofNullable(f.getAnnotation(ValidationRule.class)).map(this::from)
field.map(f -> f.getAnnotation(ValidationRule.class)).map(this::from)
.ifPresent(validationRules::add);
ofNullable(f.getAnnotation(ValidationRules.class))
field.map(f -> f.getAnnotation(ValidationRules.class))
.ifPresent(ann -> Stream.of(ann.value()).map(this::from).forEach(validationRules::add));
} catch (NoSuchFieldException | SecurityException e) {
} catch (SecurityException e) {
// ignored
}
// then method
Stream.of(m.getAnnotationsByType(ValidationRule.class)).map(this::from).forEach(validationRules::add);
Stream.of(member.getAnnotationsByType(ValidationRule.class)).map(this::from).forEach(validationRules::add);
return;
}

Expand Down Expand Up @@ -225,8 +242,8 @@ public PropertyMetadata(JsonSchema value, BeanProperty beanProperty) {
StringSchema stringSchema = value.asStringSchema();
// only set if ValidationSchemaFactoryWrapper is used
this.pattern = stringSchema.getPattern();
this.max = ofNullable(stringSchema.getMaxLength()).map(Integer::doubleValue).orElse(null);
this.min = ofNullable(stringSchema.getMinLength()).map(Integer::doubleValue).orElse(null);
//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
}
Expand Down Expand Up @@ -333,7 +350,7 @@ private T resolveObject(LinkedHashMap<String, String> visited, InternalSchemaSwa
type = resolvingContext.objectMapper.getSerializationConfig().constructType(propertyMetadata.schemaFrom);
}

T schema = resolveProperty(visited, schemaSwaps, name, type, propertySchema);
T schema = resolveProperty(visited, schemaSwaps, name, type, propertySchema, beanProperty);

propertyMetadata.updateSchema(schema);

Expand Down Expand Up @@ -378,15 +395,19 @@ static String toFQN(LinkedHashMap<String, String> visited, String name) {
}

private T resolveProperty(LinkedHashMap<String, String> visited, InternalSchemaSwaps schemaSwaps, String name,
JavaType type, JsonSchema jacksonSchema) {
JavaType type, JsonSchema jacksonSchema, BeanProperty beanProperty) {

if (jacksonSchema.isArraySchema()) {
Items items = jacksonSchema.asArraySchema().getItems();
if (items == null) { // raw collection
throw new IllegalStateException(String.format("Untyped collection %s", name));
}
if (items.isArrayItems()) {
throw new IllegalStateException("not yet supported");
}
JsonSchema arraySchema = jacksonSchema.asArraySchema().getItems().asSingleItems().getSchema();
final T schema = resolveProperty(visited, schemaSwaps, name, type.getContentType(), arraySchema);
final T schema = resolveProperty(visited, schemaSwaps, name, type.getContentType(), arraySchema, null);
handleTypeAnnotations(schema, beanProperty, List.class, 0);
return arrayLikeProperty(schema);
} else if (jacksonSchema.isIntegerSchema()) {
return singleProperty("integer");
Expand Down Expand Up @@ -440,7 +461,8 @@ private T resolveProperty(LinkedHashMap<String, String> visited, InternalSchemaS
final JavaType valueType = type.getContentType();
JsonSchema mapValueSchema = ((SchemaAdditionalProperties) ((ObjectSchema) jacksonSchema).getAdditionalProperties())
.getJsonSchema();
T component = resolveProperty(visited, schemaSwaps, name, valueType, mapValueSchema);
T component = resolveProperty(visited, schemaSwaps, name, valueType, mapValueSchema, null);
handleTypeAnnotations(component, beanProperty, Map.class, 1);
return mapLikeProperty(component);
}

Expand All @@ -464,8 +486,36 @@ private T resolveProperty(LinkedHashMap<String, String> visited, InternalSchemaS
return res;
}

private void handleTypeAnnotations(final T schema, BeanProperty beanProperty, Class<?> containerType, int typeIndex) {
if (beanProperty == null || !containerType.equals(beanProperty.getType().getRawClass())) {
return;
}

AnnotatedElement member = beanProperty.getMember().getAnnotated();
AnnotatedType fieldType = null;
AnnotatedType type = 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();
}

Stream.of(fieldType, type)
.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()));
});
}

/**
* we've added support for ignoring an enum values, which complicates this processing
* we've added support for ignoring enum values, which complicates this processing
* as that is something not supported directly by jackson
*/
private Set<String> findIgnoredEnumConstants(JavaType type) {
Expand All @@ -478,6 +528,7 @@ private Set<String> findIgnoredEnumConstants(JavaType type) {
Object value = field.get(null);
toIgnore.add(resolvingContext.objectMapper.convertValue(value, String.class));
} catch (IllegalArgumentException | IllegalAccessException e) {
// ignored
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import lombok.Data;

import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;

@Data
public class AnnotatedSpec {
Expand Down Expand Up @@ -58,6 +60,9 @@ public class AnnotatedSpec {
private String numFloat;
private ZonedDateTime issuedAt;

private List<@Pattern("[a-z].*") String> typeAnnotationCollection;
private Map<String, @Min(1) @Max(255) Integer> typeAnnotationMap;

@JsonIgnore
private int ignoredFoo;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.fabric8.crdv2.example.person;

import io.fabric8.generator.annotation.Pattern;

import java.util.List;
import java.util.Optional;

Expand All @@ -24,7 +26,7 @@ public class Person {
public Optional<String> middleName;
public String lastName;
public int birthYear;
public List<String> hobbies;
public List<@Pattern(".*ball") String> hobbies;
public AddressList addresses;
public Type type;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ void shouldCreateJsonSchemaFromClass() {
assertEquals(2, addressTypes.size());
assertTrue(addressTypes.contains("home"));
assertTrue(addressTypes.contains("work"));
assertEquals(".*ball", properties.get("hobbies").getItems()
.getSchema().getPattern());

schema = JsonSchema.from(Basic.class);
assertNotNull(schema);
Expand All @@ -116,7 +118,7 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti
assertNotNull(schema);
Map<String, JSONSchemaProps> properties = assertSchemaHasNumberOfProperties(schema, 2);
final JSONSchemaProps specSchema = properties.get("spec");
Map<String, JSONSchemaProps> spec = assertSchemaHasNumberOfProperties(specSchema, 20);
Map<String, JSONSchemaProps> spec = assertSchemaHasNumberOfProperties(specSchema, 22);

// check descriptions are present
assertTrue(spec.containsKey("from-field"));
Expand Down Expand Up @@ -155,6 +157,12 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti
assertTrue(required.contains("emptySetter2"));
assertTrue(required.contains("from-getter"));

assertEquals("[a-z].*", spec.get("typeAnnotationCollection").getItems()
.getSchema().getPattern());
JSONSchemaProps mapSchema = spec.get("typeAnnotationMap").getAdditionalProperties().getSchema();
assertEquals(255, mapSchema.getMaximum());
assertEquals(1.0, mapSchema.getMinimum());

// check ignored fields
assertFalse(spec.containsKey("ignoredFoo"));
assertFalse(spec.containsKey("ignoredBar"));
Expand Down Expand Up @@ -461,7 +469,7 @@ private static class Cyclic1 {

private static class Cyclic2 {

public Cyclic2 parent[];
public Cyclic2[] parent;

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
*/
package io.fabric8.generator.annotation;

import java.lang.annotation.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Java representation of the {@code maximum} field of JSONSchemaProps.
Expand All @@ -25,7 +28,7 @@
* Kubernetes Docs - API Reference - CRD v1 - JSONSchemaProps
* </a>
*/
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD })
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE_USE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Max {
double value();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
*/
package io.fabric8.generator.annotation;

import java.lang.annotation.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Java representation of the {@code minimum} field of JSONSchemaProps.
Expand All @@ -25,7 +28,7 @@
* Kubernetes Docs - API Reference - CRD v1 - JSONSchemaProps
* </a>
*/
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD })
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE_USE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Min {
double value();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
*/
package io.fabric8.generator.annotation;

import java.lang.annotation.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Java representation of the {@code pattern} field of JSONSchemaProps.
Expand All @@ -25,7 +28,7 @@
* Kubernetes Docs - API Reference - CRD v1 - JSONSchemaProps
* </a>
*/
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD })
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE_USE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Pattern {
String value();
Expand Down

0 comments on commit cc33729

Please sign in to comment.