From 2403614ae288434bbedb179fd7f52043c9f966f1 Mon Sep 17 00:00:00 2001 From: Dusan Jakub Date: Thu, 25 Aug 2022 17:39:39 +0200 Subject: [PATCH] @SchemaSwaps annotation can now be repeated (WIP). fixes fabric8io/kubernetes-client#4350 --- .../crd/generator/AbstractJsonSchema.java | 106 ++++++++-------- .../crd/generator/InternalSchemaSwaps.java | 118 ++++++++++++++++++ .../extraction/MultipleSchemaSwaps.java | 25 ++++ .../example/extraction/SchemaSwapSpec.java | 30 +++++ .../crd/generator/v1/JsonSchemaTest.java | 43 ++++++- 5 files changed, 265 insertions(+), 57 deletions(-) create mode 100644 crd-generator/api/src/main/java/io/fabric8/crd/generator/InternalSchemaSwaps.java create mode 100644 crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/MultipleSchemaSwaps.java create mode 100644 crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/SchemaSwapSpec.java 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 59fbfd70d6b..80e4cb1e290 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 @@ -17,6 +17,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; + import io.fabric8.crd.generator.annotation.SchemaSwap; import io.fabric8.crd.generator.utils.Types; import io.fabric8.kubernetes.api.model.Duration; @@ -81,6 +82,7 @@ public abstract class AbstractJsonSchema { public static final String ANNOTATION_NOT_NULL = "javax.validation.constraints.NotNull"; public static final String ANNOTATION_SCHEMA_FROM = "io.fabric8.crd.generator.annotation.SchemaFrom"; public static final String ANNOTATION_SCHEMA_SWAP = "io.fabric8.crd.generator.annotation.SchemaSwap"; + public static final String ANNOTATION_SCHEMA_SWAPS = "io.fabric8.crd.generator.annotation.SchemaSwaps"; public static final String JSON_NODE_TYPE = "com.fasterxml.jackson.databind.JsonNode"; @@ -119,9 +121,9 @@ public static String getSchemaTypeFor(TypeRef typeRef) { * @return The schema. */ protected T internalFrom(TypeDef definition, String... ignore) { - List schemaSwaps = new ArrayList<>(); + InternalSchemaSwaps schemaSwaps = new InternalSchemaSwaps(); T ret = internalFromImpl(definition, new HashSet<>(), schemaSwaps, ignore); - validateRemainingSchemaSwaps("unmatched class", schemaSwaps); + validateRemainingSchemaSwaps(schemaSwaps); return ret; } @@ -187,25 +189,51 @@ private static ClassRef extractClassRef(Object type) { } } - private InternalSchemaSwap extractSchemaSwap(AnnotationRef annotation) { + private void extractSchemaSwaps(ClassRef definitionType, AnnotationRef annotation, InternalSchemaSwaps schemaSwaps) { + String fullyQualifiedName = annotation.getClassRef().getFullyQualifiedName(); + switch (fullyQualifiedName) { + case ANNOTATION_SCHEMA_SWAP: + extractSchemaSwap(definitionType, annotation, schemaSwaps); + break; + case ANNOTATION_SCHEMA_SWAPS: + Map params = annotation.getParameters(); + Object[] values = (Object[]) params.get("value"); + if (values instanceof SchemaSwap[]) { + for (SchemaSwap value : (SchemaSwap[]) values) { + extractSchemaSwap(definitionType, value, schemaSwaps); + } + } +// for (AnnotationRef value : values) { +// extractSchemaSwap(definitionType, value, schemaSwaps); +// } + break; + } + } + + private void extractSchemaSwap(ClassRef definitionType, AnnotationRef annotation, InternalSchemaSwaps schemaSwaps) { Map params = annotation.getParameters(); - return new InternalSchemaSwap( - extractClassRef(params.get("originalType")), - (String) params.get("fieldName"), - extractClassRef(params.get("targetType"))); + schemaSwaps.registerSwap(definitionType, + extractClassRef(params.get("originalType")), + (String) params.get("fieldName"), + extractClassRef(params.get("targetType"))); + } + private void extractSchemaSwap(ClassRef definitionType, SchemaSwap annotation, InternalSchemaSwaps schemaSwaps) { + schemaSwaps.registerSwap(definitionType, + extractClassRef(annotation.originalType()), + annotation.fieldName(), + extractClassRef(annotation.targetType())); } - private void validateRemainingSchemaSwaps(String error, List schemaSwaps) { - if (!schemaSwaps.isEmpty()) { - String umatchedSchemaSwaps = schemaSwaps - .stream() - .map(InternalSchemaSwap::toString) - .collect(Collectors.joining(",", "[", "]")); - throw new IllegalArgumentException("SchemaSwap annotation error " + error + ": " + umatchedSchemaSwaps); + private void validateRemainingSchemaSwaps(InternalSchemaSwaps schemaSwaps) { + String unmatchedSchemaSwaps = schemaSwaps.getUnusedSwaps() + .map(Object::toString) + .collect(Collectors.joining(",")); + if (!unmatchedSchemaSwaps.isEmpty()) { + throw new IllegalArgumentException("Unmatched SchemaSwaps: " + unmatchedSchemaSwaps); } } - private T internalFromImpl(TypeDef definition, Set visited, List schemaSwaps, String... ignore) { + private T internalFromImpl(TypeDef definition, Set visited, InternalSchemaSwaps schemaSwaps, String... ignore) { final B builder = newBuilder(); Set ignores = ignore.length > 0 ? new LinkedHashSet<>(Arrays.asList(ignore)) : Collections @@ -215,19 +243,7 @@ private T internalFromImpl(TypeDef definition, Set visited, List newSchemaSwaps = definition - .getAnnotations() - .stream() - .filter(a -> a.getClassRef().getFullyQualifiedName().equals(ANNOTATION_SCHEMA_SWAP)) - .map(this::extractSchemaSwap) - .collect(Collectors.toList()); - - schemaSwaps.addAll(newSchemaSwaps); - - final Set currentSchemaSwaps = schemaSwaps - .stream() - .filter(iss -> iss.getOriginalType().getFullyQualifiedName().equals(definition.getFullyQualifiedName())) - .collect(Collectors.toSet()); + definition.getAnnotations().forEach(annotation -> extractSchemaSwaps(definition.toReference(), annotation, schemaSwaps)); // index potential accessors by name for faster lookup final Map accessors = indexPotentialAccessors(definition); @@ -239,11 +255,9 @@ private T internalFromImpl(TypeDef definition, Set visited, List matchedSchemaSwaps = facade.getMatchedSchemaSwaps(); - currentSchemaSwaps.removeAll(matchedSchemaSwaps); - schemaSwaps.removeAll(matchedSchemaSwaps); name = possiblyRenamedProperty.getName(); if (facade.required) { @@ -267,7 +281,6 @@ private T internalFromImpl(TypeDef definition, Set visited, List propertyOrAccessors = new ArrayList<>(4); - private final Set schemaSwaps; - private final Set matchedSchemaSwaps; private String renamedTo; private String description; private boolean required; @@ -395,10 +406,8 @@ private static class PropertyFacade { private String descriptionContributedBy; private TypeRef schemaFrom; - public PropertyFacade(Property property, Map potentialAccessors, Set schemaSwaps) { + public PropertyFacade(Property property, Map potentialAccessors, ClassRef schemaSwap) { original = property; - this.schemaSwaps = schemaSwaps; - this.matchedSchemaSwaps = new HashSet<>(); final String capitalized = property.getNameCapitalized(); final String name = property.getName(); propertyOrAccessors.add(PropertyOrAccessor.fromProperty(property)); @@ -414,21 +423,12 @@ public PropertyFacade(Property property, Map potentialAccessors, if (method != null) { propertyOrAccessors.add(PropertyOrAccessor.fromMethod(method, name)); } + schemaFrom = schemaSwap; } public Property process() { final String name = original.getName(); - Optional currentSchemaSwap = schemaSwaps - .stream() - .filter(iss -> iss.getFieldName().equals(name)) - .findFirst(); - - currentSchemaSwap.ifPresent(iss -> { - schemaFrom = iss.targetType; - matchedSchemaSwaps.add(iss); - }); - propertyOrAccessors.forEach(p -> { p.process(); final String contributorName = p.toString(); @@ -471,10 +471,6 @@ public Property process() { return new Property(original.getAnnotations(), typeRef, finalName, original.getComments(), original.getModifiers(), original.getAttributes()); } - - public Set getMatchedSchemaSwaps() { - return this.matchedSchemaSwaps; - } } private boolean isPotentialAccessor(Method method) { @@ -547,10 +543,10 @@ 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 InternalSchemaSwaps()); } - private T internalFromImpl(String name, TypeRef typeRef, Set visited, List schemaSwaps) { + private T internalFromImpl(String name, TypeRef typeRef, Set visited, InternalSchemaSwaps schemaSwaps) { // 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 @@ -611,7 +607,7 @@ private T internalFromImpl(String name, TypeRef typeRef, Set visited, Li // Flag to detect cycles private boolean resolving = false; - private T resolveNestedClass(String name, TypeDef def, Set visited, List schemaSwaps) { + private T resolveNestedClass(String name, TypeDef def, Set visited, InternalSchemaSwaps schemaSwaps) { if (!resolving) { visited.clear(); resolving = true; 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 new file mode 100644 index 00000000000..87b42a701d4 --- /dev/null +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/InternalSchemaSwaps.java @@ -0,0 +1,118 @@ +package io.fabric8.crd.generator; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.stream.Stream; + +import io.sundr.model.ClassRef; + +public class InternalSchemaSwaps { + private final Map swaps = new HashMap<>(); + + public void registerSwap(ClassRef definitionType, ClassRef originalType, String fieldName, ClassRef targetType) { + Value value = new Value(definitionType, originalType, fieldName, targetType); + swaps.put(new Key(originalType, fieldName), value); + } + + public Stream getUnusedSwaps() { + return swaps.values().stream().filter(value -> !value.used); + } + + public Optional lookupAndMark(ClassRef originalType, String name) { + Value value = swaps.get(new Key(originalType, name)); + if (value != null) { + value.markUsed(); + return Optional.of(value.getTargetType()); + } else { + return Optional.empty(); + } + } + + private static class Key { + private final ClassRef originalType; + private final String fieldName; + + + public Key(ClassRef originalType, String fieldName) { + this.originalType = originalType; + this.fieldName = fieldName; + } + + public ClassRef getOriginalType() { + return originalType; + } + + public String getFieldName() { + return fieldName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Key key = (Key) o; + return Objects.equals(originalType, key.originalType) && Objects.equals(fieldName, key.fieldName); + } + + @Override + public int hashCode() { + return Objects.hash(originalType, fieldName); + } + + @Override + public String toString() { + return new StringJoiner(", ", Key.class.getSimpleName() + "[", "]") + .add("originalType=" + originalType) + .add("fieldName='" + fieldName + "'") + .toString(); + } + } + + public static class Value { + private final ClassRef originalType; + private final String fieldName; + private final ClassRef targetType; + private boolean used; + private final ClassRef definitionType; + + public Value(ClassRef definitionType, ClassRef originalType, String fieldName, ClassRef targetType) { + this.definitionType = definitionType; + this.originalType = originalType; + this.fieldName = fieldName; + this.targetType = targetType; + this.used = false; + } + + private void markUsed() { + this.used = true; + } + + public ClassRef getOriginalType() { + return originalType; + } + + public String getFieldName() { + return fieldName; + } + + public ClassRef getTargetType() { + return targetType; + } + + public boolean isUsed() { + return used; + } + + @Override + public String toString() { + return "@SchemaSwap(originalType=" + originalType + ", fieldName=\"" + fieldName + "\", targetType="+targetType + ") on " + definitionType; + } + } +} diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/MultipleSchemaSwaps.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/MultipleSchemaSwaps.java new file mode 100644 index 00000000000..f8143927cfa --- /dev/null +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/MultipleSchemaSwaps.java @@ -0,0 +1,25 @@ +/** + * 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.client.CustomResource; + +@SchemaSwap(originalType = SchemaSwapSpec.SomeObject.class, fieldName = "shouldBeString", targetType = String.class) +@SchemaSwap(originalType = SchemaSwapSpec.AnotherObject.class, fieldName = "shouldBeInt", targetType = Integer.class) +public class MultipleSchemaSwaps extends CustomResource { + +} diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/SchemaSwapSpec.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/SchemaSwapSpec.java new file mode 100644 index 00000000000..2f98fa315bb --- /dev/null +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/extraction/SchemaSwapSpec.java @@ -0,0 +1,30 @@ +/** + * 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; + +public class SchemaSwapSpec { + private SomeObject first; + private SomeObject second; + private AnotherObject third; + + static class SomeObject { + private int shouldBeString; + } + + static class AnotherObject { + private String shouldBeInt; + } +} 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 a0715d3cd44..87ee85e6a41 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 @@ -20,6 +20,7 @@ import io.fabric8.crd.example.basic.Basic; import io.fabric8.crd.example.extraction.IncorrectExtraction; import io.fabric8.crd.example.extraction.IncorrectExtraction2; +import io.fabric8.crd.example.extraction.MultipleSchemaSwaps; import io.fabric8.crd.example.json.ContainingJson; import io.fabric8.crd.example.extraction.Extraction; import io.fabric8.crd.example.person.Person; @@ -178,15 +179,53 @@ void shouldExtractPropertiesSchemaFromExtractValueAnnotation() { assertNull(barProps.get("baz")); } + + @Test + void shouldExtractPropertiesSchemaFromSchemaSwapAnnotations() { + TypeDef extraction = Types.typeDefFrom(MultipleSchemaSwaps.class); + JSONSchemaProps schema = JsonSchema.from(extraction); + assertNotNull(schema); + Map properties = schema.getProperties(); + assertEquals(2, properties.size()); + final JSONSchemaProps specSchema = properties.get("spec"); + Map spec = specSchema.getProperties(); + assertEquals(3, spec.size()); + + // 'first' is replaced by SchemaSwap from int to string + JSONSchemaProps first = spec.get("first"); + Map firstProps = first.getProperties(); + assertNotNull(firstProps); + JSONSchemaProps firstProperty = firstProps.get("shouldBeString"); + assertEquals("string", firstProperty.getType()); + + // 'second' is replaced by the same SchemaSwap that is applied multiple times + JSONSchemaProps second = spec.get("second"); + Map secondProps = second.getProperties(); + assertNotNull(secondProps); + JSONSchemaProps secondProperty = secondProps.get("shouldBeString"); + assertEquals("string", secondProperty.getType()); + + // 'third' is replaced by the another SchemaSwap + JSONSchemaProps third = spec.get("third"); + Map thirdProps = third.getProperties(); + assertNotNull(thirdProps); + JSONSchemaProps thirdProperty = thirdProps.get("shouldBeInt"); + assertEquals("integer", thirdProperty.getType()); + } + @Test void shouldThrowIfSchemaSwapHasUnmatchedField() { TypeDef incorrectExtraction = Types.typeDefFrom(IncorrectExtraction.class); - assertThrows(IllegalArgumentException.class, () -> JsonSchema.from(incorrectExtraction)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> JsonSchema.from(incorrectExtraction)); + assertEquals("Unmatched SchemaSwaps: @SchemaSwap(originalType=io.fabric8.crd.example.extraction.ExtractionSpec, fieldName=\"FOO\", targetType=io" + + ".fabric8.crd.example.extraction.FooExtractor) on io.fabric8.crd.example.extraction.IncorrectExtraction", exception.getMessage()); } @Test void shouldThrowIfSchemaSwapHasUnmatchedClass() { TypeDef incorrectExtraction2 = Types.typeDefFrom(IncorrectExtraction2.class); - assertThrows(IllegalArgumentException.class, () -> JsonSchema.from(incorrectExtraction2)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> JsonSchema.from(incorrectExtraction2)); + assertEquals("Unmatched SchemaSwaps: @SchemaSwap(originalType=io.fabric8.crd.example.basic.BasicSpec, fieldName=\"bar\", targetType=io.fabric8.crd" + + ".example.extraction.FooExtractor) on io.fabric8.crd.example.extraction.IncorrectExtraction2", exception.getMessage()); } }