diff --git a/CHANGELOG.md b/CHANGELOG.md index ee53e74a72b..8e7747222a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Fix #5224: Ensuring jetty sets the User-Agent header #### Improvements +* Fix #5233: Generalized SchemaSwap to allow for cycle expansion #### Dependency Upgrade diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java index 6a33da24cd9..4649b42db39 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java @@ -23,12 +23,29 @@ import io.fabric8.kubernetes.api.model.IntOrString; import io.fabric8.kubernetes.api.model.Quantity; import io.sundr.builder.internal.functions.TypeAs; -import io.sundr.model.*; +import io.sundr.model.AnnotationRef; +import io.sundr.model.ClassRef; +import io.sundr.model.Method; +import io.sundr.model.PrimitiveRefBuilder; +import io.sundr.model.Property; +import io.sundr.model.TypeDef; +import io.sundr.model.TypeRef; import io.sundr.utils.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import static io.sundr.model.utils.Types.BOOLEAN_REF; import static io.sundr.model.utils.Types.DOUBLE_REF; @@ -227,7 +244,7 @@ private void extractSchemaSwap(ClassRef definitionType, Object annotation, Inter schemaSwaps.registerSwap(definitionType, extractClassRef(schemaSwap.originalType()), schemaSwap.fieldName(), - extractClassRef(schemaSwap.targetType())); + extractClassRef(schemaSwap.targetType()), schemaSwap.cycleDepth()); } else if (annotation instanceof AnnotationRef && ((AnnotationRef) annotation).getClassRef().getFullyQualifiedName().equals(ANNOTATION_SCHEMA_SWAP)) { @@ -235,7 +252,7 @@ private void extractSchemaSwap(ClassRef definitionType, Object annotation, Inter schemaSwaps.registerSwap(definitionType, extractClassRef(params.get("originalType")), (String) params.get("fieldName"), - extractClassRef(params.getOrDefault("targetType", void.class))); + extractClassRef(params.getOrDefault("targetType", void.class)), (Integer) params.getOrDefault("cycleDepth", 1)); } else { throw new IllegalArgumentException("Unmanaged annotation type passed to the SchemaSwaps: " + annotation); @@ -256,7 +273,8 @@ private T internalFromImpl(TypeDef definition, Set visited, InternalSche boolean preserveUnknownFields = isJsonNode; - definition.getAnnotations().forEach(annotation -> extractSchemaSwaps(definition.toReference(), annotation, schemaSwaps)); + final InternalSchemaSwaps swaps = schemaSwaps; + definition.getAnnotations().forEach(annotation -> extractSchemaSwaps(definition.toReference(), annotation, swaps)); // index potential accessors by name for faster lookup final Map accessors = indexPotentialAccessors(definition); @@ -268,7 +286,8 @@ private T internalFromImpl(TypeDef definition, Set visited, InternalSche continue; } - ClassRef potentialSchemaSwap = schemaSwaps.lookupAndMark(definition.toReference(), name).orElse(null); + schemaSwaps = schemaSwaps.branchDepths(); + ClassRef potentialSchemaSwap = schemaSwaps.lookupAndMark(definition.toReference(), name, visited::clear); final PropertyFacade facade = new PropertyFacade(property, accessors, potentialSchemaSwap); final Property possiblyRenamedProperty = facade.process(); name = possiblyRenamedProperty.getName(); diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/InternalSchemaSwaps.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/InternalSchemaSwaps.java index 2c1a030aef4..6da34ec5c91 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/InternalSchemaSwaps.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/InternalSchemaSwaps.java @@ -20,26 +20,50 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.StringJoiner; import java.util.stream.Collectors; public class InternalSchemaSwaps { - private final Map swaps = new HashMap<>(); + private final Map swaps; + private final Map swapDepths = new HashMap<>(); - public void registerSwap(ClassRef definitionType, ClassRef originalType, String fieldName, ClassRef targetType) { - Value value = new Value(definitionType, originalType, fieldName, targetType); + public InternalSchemaSwaps() { + this(new HashMap<>()); + } + + private InternalSchemaSwaps(Map swaps) { + this.swaps = swaps; + } + + public InternalSchemaSwaps branchDepths() { + InternalSchemaSwaps result = new InternalSchemaSwaps(this.swaps); + result.swapDepths.putAll(this.swapDepths); + return result; + } + + public void registerSwap(ClassRef definitionType, ClassRef originalType, String fieldName, ClassRef targetType, + int cycleDepth) { + Value value = new Value(definitionType, originalType, fieldName, targetType, cycleDepth); swaps.put(new Key(originalType, fieldName), value); } - public Optional lookupAndMark(ClassRef originalType, String name) { - Value value = swaps.get(new Key(originalType, name)); + public ClassRef lookupAndMark(ClassRef originalType, String name, Runnable swapApplicableAction) { + Key key = new Key(originalType, name); + Value value = swaps.get(key); if (value != null) { + swapApplicableAction.run(); + int depth = swapDepths.compute(key, (k, v) -> { + if (v == null) { + return 1; + } + return v + 1; + }); value.markUsed(); - return Optional.of(value.getTargetType()); - } else { - return Optional.empty(); + if (depth >= value.cycleDepth) { + return value.getTargetType(); + } } + return null; } public void throwIfUnmatchedSwaps() { @@ -51,7 +75,7 @@ public void throwIfUnmatchedSwaps() { } } - private static final class Key { + static final class Key { private final ClassRef originalType; private final String fieldName; @@ -94,18 +118,20 @@ public String toString() { } } - private static class Value { + static class Value { private final ClassRef originalType; private final String fieldName; private final ClassRef targetType; private boolean used; private final ClassRef definitionType; + private final int cycleDepth; - public Value(ClassRef definitionType, ClassRef originalType, String fieldName, ClassRef targetType) { + public Value(ClassRef definitionType, ClassRef originalType, String fieldName, ClassRef targetType, int cycleDepth) { this.definitionType = definitionType; this.originalType = originalType; this.fieldName = fieldName; this.targetType = targetType; + this.cycleDepth = cycleDepth; this.used = false; } @@ -125,10 +151,6 @@ public ClassRef getTargetType() { return targetType; } - public boolean isUsed() { - return used; - } - @Override public String toString() { return "@SchemaSwap(originalType=" + originalType + ", fieldName=\"" + fieldName + "\", targetType=" + targetType diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/annotation/SchemaSwap.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/annotation/SchemaSwap.java index 4903d4e31c4..39bbabe7009 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/annotation/SchemaSwap.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/annotation/SchemaSwap.java @@ -15,7 +15,11 @@ */ package io.fabric8.crd.generator.annotation; -import java.lang.annotation.*; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** * Annotation that allows replacing a nested schema with one from another class. @@ -52,4 +56,11 @@ * The default value of {@code void.class} causes the field to be skipped */ Class targetType() default void.class; + + /** + * Perform the swap after seeing this many occurrences + * + * @return + */ + int cycleDepth() default 1; } diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CollectionCyclicSchemaSwap.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CollectionCyclicSchemaSwap.java new file mode 100644 index 00000000000..7f212638525 --- /dev/null +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CollectionCyclicSchemaSwap.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.fabric8.crd.example.extraction; + +import io.fabric8.crd.generator.annotation.SchemaSwap; +import io.fabric8.kubernetes.api.model.AnyType; +import io.fabric8.kubernetes.client.CustomResource; + +import java.util.List; + +@SchemaSwap(originalType = CollectionCyclicSchemaSwap.Level.class, fieldName = "levels", targetType = AnyType.class, cycleDepth = 3) +public class CollectionCyclicSchemaSwap extends CustomResource { + + public static class Spec { + private MyObject myObject; + private List levels; + } + + public static class Level { + private MyObject myObject; + private List levels; + } + + public static class MyObject { + private int value; + } +} diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CyclicSchemaSwap.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CyclicSchemaSwap.java new file mode 100644 index 00000000000..d5061cf175a --- /dev/null +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/CyclicSchemaSwap.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.fabric8.crd.example.extraction; + +import io.fabric8.crd.generator.annotation.SchemaSwap; +import io.fabric8.kubernetes.api.model.AnyType; +import io.fabric8.kubernetes.client.CustomResource; + +import java.util.List; + +@SchemaSwap(originalType = CyclicSchemaSwap.Level.class, fieldName = "level", targetType = AnyType.class, cycleDepth = 3) +public class CyclicSchemaSwap extends CustomResource { + + public static class Spec { + private MyObject myObject; + private Level level; + private List levels; // should not interfere with the rendering depth of level + } + + public static class Level { + private MyObject myObject; + private Level level; + } + + public static class MyObject { + private int value; + } +} diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java b/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java index 26d05c96cec..2111a98fb5c 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java @@ -18,6 +18,8 @@ import com.fasterxml.jackson.databind.JsonNode; import io.fabric8.crd.example.annotated.Annotated; import io.fabric8.crd.example.basic.Basic; +import io.fabric8.crd.example.extraction.CollectionCyclicSchemaSwap; +import io.fabric8.crd.example.extraction.CyclicSchemaSwap; import io.fabric8.crd.example.extraction.DeeplyNestedSchemaSwaps; import io.fabric8.crd.example.extraction.Extraction; import io.fabric8.crd.example.extraction.IncorrectExtraction; @@ -35,7 +37,12 @@ import java.util.Map; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class JsonSchemaTest { @@ -259,6 +266,50 @@ void shouldApplySchemaSwapsMultipleTimesInDeepClassHierarchy() { } } + @Test + void shouldApplyCyclicSchemaSwaps() { + TypeDef extraction = Types.typeDefFrom(CyclicSchemaSwap.class); + JSONSchemaProps schema = JsonSchema.from(extraction); + assertNotNull(schema); + Map properties = assertSchemaHasNumberOfProperties(schema, 2); + Map spec = assertSchemaHasNumberOfProperties(properties.get("spec"), 3); + + assertPropertyHasType(spec.get("myObject"), "value", "integer"); + Map level1 = assertSchemaHasNumberOfProperties(spec.get("level"), 2); + + assertPropertyHasType(level1.get("myObject"), "value", "integer"); + Map level2 = assertSchemaHasNumberOfProperties(level1.get("level"), 2); + + assertPropertyHasType(level2.get("myObject"), "value", "integer"); + Map level3 = assertSchemaHasNumberOfProperties(level2.get("level"), 2); + + assertPropertyHasType(level3.get("myObject"), "value", "integer"); + // should terminate at the 3rd level with object + assertPropertyHasType(level3.get("level"), "value", "object"); + } + + @Test + void shouldApplyCollectionCyclicSchemaSwaps() { + TypeDef extraction = Types.typeDefFrom(CollectionCyclicSchemaSwap.class); + JSONSchemaProps schema = JsonSchema.from(extraction); + assertNotNull(schema); + Map properties = assertSchemaHasNumberOfProperties(schema, 2); + Map spec = assertSchemaHasNumberOfProperties(properties.get("spec"), 2); + + assertPropertyHasType(spec.get("myObject"), "value", "integer"); + Map level1 = assertSchemaHasNumberOfProperties(spec.get("levels").getItems().getSchema(), 2); + + assertPropertyHasType(level1.get("myObject"), "value", "integer"); + Map level2 = assertSchemaHasNumberOfProperties(level1.get("levels").getItems().getSchema(), 2); + + assertPropertyHasType(level2.get("myObject"), "value", "integer"); + Map level3 = assertSchemaHasNumberOfProperties(level2.get("levels").getItems().getSchema(), 2); + + assertPropertyHasType(level3.get("myObject"), "value", "integer"); + // should terminate at the 3rd level with object + assertPropertyHasType(level3.get("levels"), "value", "object"); + } + @Test void shouldThrowIfSchemaSwapHasUnmatchedField() { TypeDef incorrectExtraction = Types.typeDefFrom(IncorrectExtraction.class);