Skip to content

Commit

Permalink
fix: rely upon jackson json schema for java types
Browse files Browse the repository at this point in the history
  • Loading branch information
shawkins committed Apr 17, 2024
1 parent bb3d0c0 commit dcf75ea
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### 6.13-SNAPSHOT

#### Bugs
* Fix #5866: Addressed cycle in crd generation with Java 19+ and ZonedDateTime

#### Improvements

Expand Down
5 changes: 5 additions & 0 deletions crd-generator/api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
<artifactId>kubernetes-client-api</artifactId>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jsonSchema</artifactId>
</dependency>

<dependency>
<groupId>io.fabric8</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@
package io.fabric8.crd.generator;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;
import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema.Items;
import io.fabric8.crd.generator.InternalSchemaSwaps.SwapResult;
import io.fabric8.crd.generator.annotation.SchemaSwap;
import io.fabric8.crd.generator.utils.Types;
import io.fabric8.generator.annotation.ValidationRule;
import io.fabric8.kubernetes.api.model.Duration;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.Quantity;
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
import io.sundr.builder.internal.functions.TypeAs;
import io.sundr.model.AnnotationRef;
import io.sundr.model.ClassRef;
Expand All @@ -43,6 +48,7 @@
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
Expand Down Expand Up @@ -122,6 +128,9 @@ public abstract class AbstractJsonSchema<T, B> {
public static final String JSON_NODE_TYPE = "com.fasterxml.jackson.databind.JsonNode";
public static final String ANY_TYPE = "io.fabric8.kubernetes.api.model.AnyType";

private static final JsonSchemaGenerator GENERATOR;
private static final Set<String> COMPLEX_JAVA_TYPES = new HashSet<>();

static {
COMMON_MAPPINGS.put(STRING_REF, STRING_MARKER);
COMMON_MAPPINGS.put(DATE_REF, STRING_MARKER);
Expand All @@ -138,6 +147,10 @@ public abstract class AbstractJsonSchema<T, B> {
COMMON_MAPPINGS.put(QUANTITY_REF, INT_OR_STRING_MARKER);
COMMON_MAPPINGS.put(INT_OR_STRING_REF, INT_OR_STRING_MARKER);
COMMON_MAPPINGS.put(DURATION_REF, STRING_MARKER);
ObjectMapper mapper = new ObjectMapper();
// initialize with client defaults
new KubernetesSerialization(mapper, false);
GENERATOR = new JsonSchemaGenerator(mapper);
}

public static String getSchemaTypeFor(TypeRef typeRef) {
Expand Down Expand Up @@ -853,18 +866,67 @@ private T internalFromImpl(String name, TypeRef typeRef, LinkedHashMap<String, S

private T resolveNestedClass(String name, TypeDef def, LinkedHashMap<String, String> visited,
InternalSchemaSwaps schemaSwaps) {
if (visited.put(def.getFullyQualifiedName(), name) != null) {
String fullyQualifiedName = def.getFullyQualifiedName();
T res = resolveJavaClass(fullyQualifiedName);
if (res != null) {
return res;
}
if (visited.put(fullyQualifiedName, name) != null) {
throw new IllegalArgumentException(
"Found a cyclic reference involving the field of type " + def.getFullyQualifiedName() + " starting a field "
"Found a cyclic reference involving the field of type " + fullyQualifiedName + " starting a field "
+ visited.entrySet().stream().map(e -> e.getValue() + " >>\n" + e.getKey()).collect(Collectors.joining(".")) + "."
+ name);
}

T res = internalFromImpl(def, visited, schemaSwaps);
visited.remove(def.getFullyQualifiedName());
res = internalFromImpl(def, visited, schemaSwaps);
visited.remove(fullyQualifiedName);
return res;
}

private T resolveJavaClass(String fullyQualifiedName) {
if ((!fullyQualifiedName.startsWith("java.") && !fullyQualifiedName.startsWith("javax."))
|| COMPLEX_JAVA_TYPES.contains(fullyQualifiedName)) {
return null;
}
String mapping = null;
boolean array = false;
try {
Class<?> clazz = Class.forName(fullyQualifiedName);
JsonSchema schema = GENERATOR.generateSchema(clazz);
if (schema.isArraySchema()) {
Items items = schema.asArraySchema().getItems();
if (items.isSingleItems()) {
array = true;
schema = items.asSingleItems().getSchema();
}
}
if (schema.isIntegerSchema()) {
mapping = INTEGER_MARKER;
} else if (schema.isNumberSchema()) {
mapping = NUMBER_MARKER;
} else if (schema.isBooleanSchema()) {
mapping = BOOLEAN_MARKER;
} else if (schema.isStringSchema()) {
mapping = STRING_MARKER;
}
} catch (Exception e) {
LOGGER.debug(
"Something went wrong with detecting java type schema for {}, will use full introspection instead",
fullyQualifiedName, e);
}
// cache the result for subsequent calls
if (mapping != null) {
if (array) {
return arrayLikeProperty(singleProperty(mapping));
}
COMMON_MAPPINGS.put(TypeDef.forName(fullyQualifiedName).toReference(), mapping);
return singleProperty(mapping);
}

COMPLEX_JAVA_TYPES.add(fullyQualifiedName);
return null;
}

/**
* Builds the schema for specifically handled property types (e.g. intOrString properties)
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.generator.jackson;

import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.Version;

@Group("org.example")
@Version("v1alpha1")
public class Example extends CustomResource<ExampleSpec, Void> implements Namespaced {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.generator.jackson;

import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;

public class ExampleSpec {
public ZonedDateTime timestamp;
public LocalDate local;
public UUID uuid;
public Short shortValue;
public AtomicBoolean ab;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.generator.jackson;

import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps;
import io.fabric8.kubernetes.client.utils.Serialization;
import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

class JacksonTypeTest {

@Test
void testCrd() {
CustomResourceDefinition d = Serialization.unmarshal(getClass().getClassLoader()
.getResourceAsStream("META-INF/fabric8/examples.org.example-v1.yml"),
CustomResourceDefinition.class);
assertNotNull(d);
assertEquals("Example", d.getSpec().getNames().getKind());
Map<String, JSONSchemaProps> props = d.getSpec().getVersions().get(0).getSchema().getOpenAPIV3Schema().getProperties()
.get("spec").getProperties();
assertEquals(5, props.size());
assertEquals("number", props.get("timestamp").getType());
assertEquals("array", props.get("local").getType());
assertEquals("string", props.get("uuid").getType());
assertEquals("integer", props.get("shortValue").getType());
assertEquals("boolean", props.get("ab").getType());
}
}

0 comments on commit dcf75ea

Please sign in to comment.