Skip to content

Commit

Permalink
Merge pull request #130 from HubSpot/crd-gen-generics
Browse files Browse the repository at this point in the history
add support for generics in CRD generation
  • Loading branch information
euberseder-hubspot authored Jan 10, 2024
2 parents 1810322 + 893f39e commit 59a355d
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public boolean isPreserveUnknownFields() {
*/
protected T internalFrom(TypeDef definition, String... ignore) {
List<InternalSchemaSwap> schemaSwaps = new ArrayList<>();
T ret = internalFromImpl(definition, new HashSet<>(), schemaSwaps, ignore);
T ret = internalFromImpl(definition, new HashSet<>(), schemaSwaps, new ParameterMap(new HashMap<>()), ignore);
validateRemainingSchemaSwaps("unmatched class", schemaSwaps);
return ret;
}
Expand Down Expand Up @@ -277,7 +277,7 @@ private void validateRemainingSchemaSwaps(String error, List<InternalSchemaSwap>
}
}

private T internalFromImpl(TypeDef definition, Set<String> visited, List<InternalSchemaSwap> schemaSwaps, String... ignore) {
private T internalFromImpl(TypeDef definition, Set<String> visited, List<InternalSchemaSwap> schemaSwaps, ParameterMap parameterMap, String... ignore) {
final B builder = newBuilder();
Set<String> ignores =
ignore.length > 0 ? new LinkedHashSet<>(Arrays.asList(ignore)) : Collections
Expand Down Expand Up @@ -312,7 +312,7 @@ private T internalFromImpl(TypeDef definition, Set<String> visited, List<Interna
continue;
}

final PropertyFacade facade = new PropertyFacade(property, accessors, currentSchemaSwaps);
final PropertyFacade facade = new PropertyFacade(property, accessors, currentSchemaSwaps, parameterMap);
final Property possiblyRenamedProperty = facade.process();
final Set<InternalSchemaSwap> matchedSchemaSwaps = facade.getMatchedSchemaSwaps();
currentSchemaSwaps.removeAll(matchedSchemaSwaps);
Expand All @@ -324,7 +324,7 @@ private T internalFromImpl(TypeDef definition, Set<String> visited, List<Interna
} else if (facade.ignored) {
continue;
}
final T schema = internalFromImpl(name, possiblyRenamedProperty.getTypeRef(), visited, schemaSwaps);
final T schema = internalFromImpl(name, possiblyRenamedProperty.getTypeRef(), visited, schemaSwaps, parameterMap);
if (facade.preserveUnknownFields) {
preserveUnknownFields = true;
}
Expand Down Expand Up @@ -363,6 +363,45 @@ private Map<String, Method> indexPotentialAccessors(TypeDef definition) {
return accessors;
}

private static class ParameterMap {
final Map<String, TypeRef> mappings;

ParameterMap(Map<String, TypeRef> mappings) {
this.mappings = mappings;
}

TypeRef exchange(TypeRef original){
return exchange(original, true);
}

TypeRef exchange(TypeRef original, boolean throwOnFailedLookup){
if (original instanceof TypeParamRef) {
String name = ((TypeParamRef) original).getName();
TypeRef ref = mappings.get(name);
if(ref != null) {
return ref;
}
if(throwOnFailedLookup) {
throw new RuntimeException(String.format("Could not find type mapping for parametrized type %s", name));
}
}
return original;
}

static ParameterMap from(ClassRef classRef, ParameterMap parentMappings) {
TypeDef def = Types.typeDefFrom(classRef);

Map<String, TypeRef> mappings = new HashMap<>();
for(int i=0; i<def.getParameters().size(); i++) {
mappings.put(
def.getParameters().get(i).getName(),
parentMappings.exchange(classRef.getArguments().get(i))
);
}

return new ParameterMap(mappings);
}
}

private static class PropertyOrAccessor {
private final Collection<AnnotationRef> annotations;
Expand Down Expand Up @@ -515,6 +554,7 @@ private static class PropertyFacade {
private final List<PropertyOrAccessor> propertyOrAccessors = new ArrayList<>(4);
private final Set<InternalSchemaSwap> schemaSwaps;
private final Set<InternalSchemaSwap> matchedSchemaSwaps;
private final ParameterMap parameterMap;
private String renamedTo;
private String description;
private String defaultValue;
Expand All @@ -530,9 +570,10 @@ private static class PropertyFacade {
private String descriptionContributedBy;
private TypeRef schemaFrom;

public PropertyFacade(Property property, Map<String, Method> potentialAccessors, Set<InternalSchemaSwap> schemaSwaps) {
public PropertyFacade(Property property, Map<String, Method> potentialAccessors, Set<InternalSchemaSwap> schemaSwaps, ParameterMap parameterMap) {
original = property;
this.schemaSwaps = schemaSwaps;
this.parameterMap = parameterMap;
this.matchedSchemaSwaps = new HashSet<>();
final String capitalized = property.getNameCapitalized();
final String name = property.getName();
Expand Down Expand Up @@ -610,7 +651,7 @@ public Property process() {
}
});

TypeRef typeRef = schemaFrom != null ? schemaFrom : original.getTypeRef();
TypeRef typeRef = schemaFrom != null ? schemaFrom : parameterMap.exchange(original.getTypeRef());
String finalName = renamedTo != null ? renamedTo : original.getName();

return new Property(original.getAnnotations(), typeRef, finalName,
Expand Down Expand Up @@ -685,16 +726,16 @@ private String extractUpdatedNameFromJacksonPropertyIfPresent(Property property)
* @return the structural schema associated with the specified property
*/
public T internalFrom(String name, TypeRef typeRef) {
return internalFromImpl(name, typeRef, new HashSet<>(), new ArrayList<>());
return internalFromImpl(name, typeRef, new HashSet<>(), new ArrayList<>(), new ParameterMap(new HashMap<>()));
}

private T internalFromImpl(String name, TypeRef typeRef, Set<String> visited, List<InternalSchemaSwap> schemaSwaps) {
private T internalFromImpl(String name, TypeRef typeRef, Set<String> visited, List<InternalSchemaSwap> schemaSwaps, ParameterMap parameterMap) {
// Note that ordering of the checks here is meaningful: we need to check for complex types last
// in case some "complex" types are handled specifically
if (typeRef.getDimensions() > 0 || io.sundr.model.utils.Collections.isCollection(typeRef)) { // Handle Collections & Arrays
final TypeRef collectionType = TypeAs.combine(TypeAs.UNWRAP_ARRAY_OF, TypeAs.UNWRAP_COLLECTION_OF)
.apply(typeRef);
final T schema = internalFromImpl(name, collectionType, visited, schemaSwaps);
final T schema = internalFromImpl(name, parameterMap.exchange(collectionType), visited, schemaSwaps, parameterMap);
return arrayLikeProperty(schema);
} else if (io.sundr.model.utils.Collections.IS_MAP.apply(typeRef)) { // Handle Maps
final TypeRef keyType = TypeAs.UNWRAP_MAP_KEY_OF.apply(typeRef);
Expand All @@ -704,15 +745,15 @@ private T internalFromImpl(String name, TypeRef typeRef, Set<String> visited, Li
}

final TypeRef valueType = TypeAs.UNWRAP_MAP_VALUE_OF.apply(typeRef);
T schema = internalFromImpl(name, valueType, visited, schemaSwaps);
T schema = internalFromImpl(name, parameterMap.exchange(valueType, false), visited, schemaSwaps, parameterMap);
if (schema == null) {
LOGGER.warn("Property '{}' with '{}' value type is mapped to 'object' because its CRD representation cannot be extracted.", name, typeRef);
schema = internalFromImpl(name, OBJECT_REF, visited, schemaSwaps);
schema = internalFromImpl(name, OBJECT_REF, visited, schemaSwaps, parameterMap);
}

return mapLikeProperty(schema);
} else if (io.sundr.model.utils.Optionals.isOptional(typeRef)) { // Handle Optionals
return internalFromImpl(name, TypeAs.UNWRAP_OPTIONAL_OF.apply(typeRef), visited, schemaSwaps);
return internalFromImpl(name, parameterMap.exchange(TypeAs.UNWRAP_OPTIONAL_OF.apply(typeRef)), visited, schemaSwaps, parameterMap);
} else {
final String typeName = COMMON_MAPPINGS.get(typeRef);
if (typeName != null) { // we have a type that we handle specifically
Expand All @@ -724,9 +765,9 @@ private T internalFromImpl(String name, TypeRef typeRef, Set<String> visited, Li
} else {
if (typeRef instanceof ClassRef) { // Handle complex types
ClassRef classRef = (ClassRef) typeRef;
TypeDef def = Types.typeDefFrom(classRef);

// check if we're dealing with an enum
TypeDef def = Types.typeDefFrom(classRef);
if (def.isEnum()) {
final JsonNode[] enumValues = def.getProperties().stream()
.map(this::extractUpdatedNameFromJacksonPropertyIfPresent)
Expand All @@ -735,7 +776,7 @@ private T internalFromImpl(String name, TypeRef typeRef, Set<String> visited, Li
.toArray(JsonNode[]::new);
return enumProperty(enumValues);
} else {
return resolveNestedClass(name, def, visited, schemaSwaps);
return resolveNestedClass(name, classRef, visited, schemaSwaps, parameterMap);
}

}
Expand All @@ -747,7 +788,8 @@ private T internalFromImpl(String name, TypeRef typeRef, Set<String> visited, Li
// Flag to detect cycles
private boolean resolving = false;

private T resolveNestedClass(String name, TypeDef def, Set<String> visited, List<InternalSchemaSwap> schemaSwaps) {
private T resolveNestedClass(String name, ClassRef classRef, Set<String> visited, List<InternalSchemaSwap> schemaSwaps, ParameterMap parameterMap) {
TypeDef def = Types.typeDefFrom(classRef);
if (!resolving) {
visited.clear();
resolving = true;
Expand All @@ -759,7 +801,7 @@ private T resolveNestedClass(String name, TypeDef def, Set<String> visited, List
visited.add(visitedName);
}

T res = internalFromImpl(def, visited, schemaSwaps);
T res = internalFromImpl(def, visited, schemaSwaps, ParameterMap.from(classRef, parameterMap));
resolving = false;
return res;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.fabric8.crd.example.generic;

public class Generic<T> {
T bar;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.fabric8.crd.example.generic;

import java.util.List;

public class NestedGeneric<P> {
Generic<P> quux;
List<P> corge;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.fabric8.crd.example.generic;

import io.fabric8.kubernetes.client.CustomResource;

public class ResourceWithGeneric extends CustomResource<ResourceWithGenericSpec, Void> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.fabric8.crd.example.generic;

public class ResourceWithGenericSpec {
Generic<String> foo;
Generic<Integer> baz;
NestedGeneric<String> qux;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
import io.fabric8.crd.example.extraction.Extraction;
import io.fabric8.crd.example.extraction.IncorrectExtraction;
import io.fabric8.crd.example.extraction.IncorrectExtraction2;
import io.fabric8.crd.example.generic.ResourceWithGeneric;
import io.fabric8.crd.example.json.ContainingJson;
import io.fabric8.crd.example.person.Person;
import io.fabric8.crd.generator.utils.Types;
import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps;
import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaPropsOrArray;
import io.sundr.model.TypeDef;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -222,6 +224,51 @@ void shouldExtractPropertiesSchemaFromExtractValueAnnotation() {
assertNull(barProps.get("baz"));
}

@Test
void shouldProcessGenericClasses() {
TypeDef resourceWithGeneric = Types.typeDefFrom(ResourceWithGeneric.class);
JSONSchemaProps schema = JsonSchema.from(resourceWithGeneric);
assertNotNull(schema);

Map<String, JSONSchemaProps> properties = schema.getProperties();
assertEquals(2, properties.size());

final JSONSchemaProps specSchema = properties.get("spec");
Map<String, JSONSchemaProps> spec = specSchema.getProperties();
assertEquals(3, spec.size());

JSONSchemaProps foo = spec.get("foo");
assertNotNull(foo);
Map<String, JSONSchemaProps> fooProps = foo.getProperties();
assertNotNull(fooProps);
assertEquals("string", fooProps.get("bar").getType());

JSONSchemaProps baz = spec.get("baz");
assertNotNull(baz);
Map<String, JSONSchemaProps> bazProps = baz.getProperties();
assertNotNull(bazProps);
assertEquals("integer", bazProps.get("bar").getType());

JSONSchemaProps qux = spec.get("qux");
assertNotNull(qux);
Map<String, JSONSchemaProps> quxProps = qux.getProperties();
assertEquals(2, quxProps.size());

JSONSchemaProps quux = quxProps.get("quux");
assertNotNull(quux);
Map<String, JSONSchemaProps> quuxProps = quux.getProperties();
assertNotNull(quuxProps);
assertEquals("string", quuxProps.get("bar").getType());

JSONSchemaProps corge = quxProps.get("corge");
assertNotNull(corge);
JSONSchemaPropsOrArray corgeItems = corge.getItems();
assertNotNull(corgeItems);
JSONSchemaProps corgeItemsProps = corgeItems.getSchema();
assertNotNull(corgeItemsProps);
assertEquals("string", corgeItemsProps.getType());
}

@Test
void shouldThrowIfSchemaSwapHasUnmatchedField() {
TypeDef incorrectExtraction = Types.typeDefFrom(IncorrectExtraction.class);
Expand Down

0 comments on commit 59a355d

Please sign in to comment.