Skip to content

Commit

Permalink
fix fabric8io#5233: allowing schemaswap to have a configurable level
Browse files Browse the repository at this point in the history
  • Loading branch information
shawkins committed Jun 13, 2023
1 parent 3edfdbb commit ff1d4e7
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -227,15 +244,15 @@ 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)) {
Map<String, Object> params = ((AnnotationRef) annotation).getParameters();
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);
Expand All @@ -256,7 +273,8 @@ private T internalFromImpl(TypeDef definition, Set<String> 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<String, Method> accessors = indexPotentialAccessors(definition);
Expand All @@ -268,7 +286,8 @@ private T internalFromImpl(TypeDef definition, Set<String> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Key, Value> swaps = new HashMap<>();
private final Map<Key, Value> swaps;
private final Map<Key, Integer> 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<Key, Value> 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<ClassRef> 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() {
Expand All @@ -51,7 +75,7 @@ public void throwIfUnmatchedSwaps() {
}
}

private static final class Key {
static final class Key {
private final ClassRef originalType;
private final String fieldName;

Expand Down Expand Up @@ -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;
}

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<CollectionCyclicSchemaSwap.Spec, Void> {

public static class Spec {
private MyObject myObject;
private List<Level> levels;
}

public static class Level {
private MyObject myObject;
private List<Level> levels;
}

public static class MyObject {
private int value;
}
}
Original file line number Diff line number Diff line change
@@ -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<CyclicSchemaSwap.Spec, Void> {

public static class Spec {
private MyObject myObject;
private Level level;
private List<Level> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -259,6 +266,50 @@ void shouldApplySchemaSwapsMultipleTimesInDeepClassHierarchy() {
}
}

@Test
void shouldApplyCyclicSchemaSwaps() {
TypeDef extraction = Types.typeDefFrom(CyclicSchemaSwap.class);
JSONSchemaProps schema = JsonSchema.from(extraction);
assertNotNull(schema);
Map<String, JSONSchemaProps> properties = assertSchemaHasNumberOfProperties(schema, 2);
Map<String, JSONSchemaProps> spec = assertSchemaHasNumberOfProperties(properties.get("spec"), 3);

assertPropertyHasType(spec.get("myObject"), "value", "integer");
Map<String, JSONSchemaProps> level1 = assertSchemaHasNumberOfProperties(spec.get("level"), 2);

assertPropertyHasType(level1.get("myObject"), "value", "integer");
Map<String, JSONSchemaProps> level2 = assertSchemaHasNumberOfProperties(level1.get("level"), 2);

assertPropertyHasType(level2.get("myObject"), "value", "integer");
Map<String, JSONSchemaProps> 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<String, JSONSchemaProps> properties = assertSchemaHasNumberOfProperties(schema, 2);
Map<String, JSONSchemaProps> spec = assertSchemaHasNumberOfProperties(properties.get("spec"), 2);

assertPropertyHasType(spec.get("myObject"), "value", "integer");
Map<String, JSONSchemaProps> level1 = assertSchemaHasNumberOfProperties(spec.get("levels").getItems().getSchema(), 2);

assertPropertyHasType(level1.get("myObject"), "value", "integer");
Map<String, JSONSchemaProps> level2 = assertSchemaHasNumberOfProperties(level1.get("levels").getItems().getSchema(), 2);

assertPropertyHasType(level2.get("myObject"), "value", "integer");
Map<String, JSONSchemaProps> 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);
Expand Down

0 comments on commit ff1d4e7

Please sign in to comment.