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

add support for generics in CRD generation #130

Merged
merged 3 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
euberseder-hubspot marked this conversation as resolved.
Show resolved Hide resolved
}
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
Loading