From c07e07c13af134f7a104d890751e1db155bde51e Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Fri, 25 Oct 2024 05:55:19 +0200 Subject: [PATCH] feat(openapi): generation of JsonUnwrapped interface fields Signed-off-by: Marc Nuri --- .../pkg/openapi/openapi-gen-processors.go | 35 ++++++++++++------- .../generator/model/ModelGenerator.java | 16 +++++++-- .../generator/model/TemplateContext.java | 4 +-- .../schema/generator/schema/SchemaUtils.java | 17 +++++++++ .../resources/templates/model_fields.mustache | 3 ++ .../templates/model_methods.mustache | 3 ++ .../schema/generator/SchemaUtilsTest.java | 7 ++++ 7 files changed, 67 insertions(+), 18 deletions(-) diff --git a/kubernetes-model-generator/openapi/generator/pkg/openapi/openapi-gen-processors.go b/kubernetes-model-generator/openapi/generator/pkg/openapi/openapi-gen-processors.go index cf6ce6a64f..924822a3a4 100644 --- a/kubernetes-model-generator/openapi/generator/pkg/openapi/openapi-gen-processors.go +++ b/kubernetes-model-generator/openapi/generator/pkg/openapi/openapi-gen-processors.go @@ -63,9 +63,26 @@ func processPatchComments(_ *generator.Context, _ *types.Package, t *types.Type, } } +func addOrAppend(commentLines []string, prefix, value string) []string { + added := false + for i, commentLine := range commentLines{ + if strings.HasPrefix(commentLine, prefix) { + commentLines[i] = commentLine +","+value + added = true + break + } + } + if !added { + commentLines = append(commentLines, prefix+value) + } + return commentLines +} + // func processProtobufOneof // To generate interfaces and extending classes for oneof fields // This is something extensively used in the Istio API, that uses these as marker interfaces +// +// For processing we'll add +k8s:openapi-gen=x-kubernetes tags that will be later processed by kube-openapi and added to the OpenAPI json spec func processProtobufOneof(_ *generator.Context, pkg *types.Package, t *types.Type, m *types.Member, memberIndex int) { publicInterfaceName := func(name string) string { if unicode.IsUpper(rune(name[0])) { @@ -80,8 +97,10 @@ func processProtobufOneof(_ *generator.Context, pkg *types.Package, t *types.Typ m.Type.Kind = types.Struct // Ensure it's exported t.Members[memberIndex].Type.Name.Name = publicInterfaceName(m.Type.Name.Name) - // Add comment tag to mark this as an interface, this is later processed by kube-openapi and added to the OpenAPI json spec - m.Type.CommentLines = append(m.Type.CommentLines, "+k8s:openapi-gen=x-kubernetes-fabric8-type:interface") + //// Add comment tag to the referenced type and mark it as an interface + //t.Members[memberIndex].Type.CommentLines = append(m.Type.CommentLines, "+k8s:openapi-gen=x-kubernetes-fabric8-type:interface") + // Add comment tag to the current type to mark it as it has fields that are interfaces (useful for the OpenAPI Java generator) + t.CommentLines = addOrAppend(t.CommentLines, "+k8s:openapi-gen=x-kubernetes-fabric8-interface-fields:", m.Name) } // Implementations // It's just a marker interface, it contains a single method that has the same name as the interface @@ -94,17 +113,7 @@ func processProtobufOneof(_ *generator.Context, pkg *types.Package, t *types.Typ } if reflect.ValueOf(t.Methods).MapKeys()[0].String() == reflect.ValueOf(candidateType.Methods).MapKeys()[0].String() { t.CommentLines = append(t.CommentLines, "+k8s:openapi-gen=x-kubernetes-fabric8-implements:"+publicInterfaceName(candidateType.Name.Name)) - addedImplementation := false - for i, candidateCommentLine := range candidateType.CommentLines { - if strings.HasPrefix(candidateCommentLine, "+k8s:openapi-gen=x-kubernetes-fabric8-implementation") { - candidateType.CommentLines[i] = candidateCommentLine +","+t.Name.Name - addedImplementation = true - break - } - } - if !addedImplementation { - candidateType.CommentLines = append(candidateType.CommentLines, "+k8s:openapi-gen=x-kubernetes-fabric8-implementation:"+t.Name.Name) - } + candidateType.CommentLines = addOrAppend(candidateType.CommentLines, "+k8s:openapi-gen=x-kubernetes-fabric8-implementation:", t.Name.Name) } } } diff --git a/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/ModelGenerator.java b/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/ModelGenerator.java index 19ca501b4c..a23941134a 100644 --- a/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/ModelGenerator.java +++ b/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/ModelGenerator.java @@ -121,8 +121,15 @@ private void processTemplate(TemplateContext ret) { ret.addImport("com.fasterxml.jackson.databind.annotation.JsonSerialize"); ret.put("classJsonSerializeUsing", serializer); } - ret.put("classJsonDeserializeUsing", Optional.ofNullable(deserializerForJavaClass(ret.getClassName())) - .orElse("com.fasterxml.jackson.databind.JsonDeserializer.None.class")); + final String deserializer; + if (SchemaUtils.hasInterfaceFields(ret.getClassSchema())) { + deserializer = "io.fabric8.kubernetes.model.jackson.JsonUnwrappedDeserializer.class"; + } else if (deserializerForJavaClass(ret.getClassName()) != null) { + deserializer = deserializerForJavaClass(ret.getClassName()); + } else { + deserializer = "com.fasterxml.jackson.databind.JsonDeserializer.None.class"; + } + ret.put("classJsonDeserializeUsing", deserializer); ret.put("package", ret.getPackageName()); if (settings.isGenerateJavadoc()) { ret.put("hasDescription", !sanitizeDescription(ret.getClassSchema().getDescription()).trim().isEmpty()); @@ -162,6 +169,7 @@ private void processTemplate(TemplateContext ret) { private List> templateFields(TemplateContext templateContext) { final List> properties = new ArrayList<>(); + final Set interfaceFields = SchemaUtils.interfaceFields(templateContext.getClassSchema()); for (Entry property : templateContext.getSchemaProperties().entrySet()) { final Map templateProp = new HashMap<>(); final Schema propertySchema = property.getValue(); @@ -190,6 +198,10 @@ private List> templateFields(TemplateContext templateContext templateContext.addImport("com.fasterxml.jackson.databind.annotation.JsonDeserialize"); templateProp.put("deserializeUsing", deserializeUsing); } + if (interfaceFields.contains(property.getKey())) { + templateContext.addImport("com.fasterxml.jackson.annotation.JsonUnwrapped"); + templateProp.put("jsonUnwrapped", true); + } // Default values if (isArray(propertySchema)) { templateContext.addImport("com.fasterxml.jackson.annotation.JsonInclude"); diff --git a/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/TemplateContext.java b/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/TemplateContext.java index 928d247379..18edb63339 100644 --- a/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/TemplateContext.java +++ b/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/TemplateContext.java @@ -26,7 +26,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.TreeSet; @@ -55,8 +54,7 @@ final class TemplateContext implements ImportManager { inRootPackage = packageName.equals(settings.getPackageName()); classSimpleName = SchemaUtils.refToClassName(classKey); className = packageName + "." + classSimpleName; - isInterface = classSchema.getExtensions() != null - && Objects.equals(classSchema.getExtensions().get("x-kubernetes-fabric8-type"), "interface"); + isInterface = SchemaUtils.isInterface(classSchema); imports = new TreeSet<>(new ImportOrderComparator()); kubernetesListType = apiVersion == null ? null : schemaUtils.kubernetesListType(this, classSchema); hasMetadata = apiVersion != null && kubernetesListType == null && schemaUtils.isHasMetadata(classSchema); diff --git a/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/schema/SchemaUtils.java b/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/schema/SchemaUtils.java index 0b0778da18..5ec68870ad 100644 --- a/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/schema/SchemaUtils.java +++ b/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/schema/SchemaUtils.java @@ -253,6 +253,23 @@ public String schemaToClassName(ImportManager imports, Schema schema) { return schemaTypeToJavaPrimitive(schema); } + public static boolean isInterface(Schema schema) { + return schema.getExtensions() != null + && Objects.equals(schema.getExtensions().get("x-kubernetes-fabric8-type"), "interface"); + } + + public static boolean hasInterfaceFields(Schema schema) { + return schema.getExtensions() != null + && schema.getExtensions().containsKey("x-kubernetes-fabric8-interface-fields"); + } + + public static Set interfaceFields(Schema schema) { + if (hasInterfaceFields(schema)) { + return Set.of(schema.getExtensions().get("x-kubernetes-fabric8-interface-fields").toString().split(",")); + } + return Collections.emptySet(); + } + public static boolean isArray(Schema schema) { return schema instanceof ArraySchema; } diff --git a/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model_fields.mustache b/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model_fields.mustache index 2846ffd062..ee823fb1a7 100644 --- a/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model_fields.mustache +++ b/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model_fields.mustache @@ -37,6 +37,9 @@ {{#jsonInclude}} @JsonInclude(JsonInclude.Include.{{.}}) {{/jsonInclude}} +{{#jsonUnwrapped}} + @JsonUnwrapped +{{/jsonUnwrapped}} private {{type}} {{name}}{{#defaultValue}} = {{.}}{{/defaultValue}};{{/fields}} {{#additionalProperties}} @JsonIgnore diff --git a/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model_methods.mustache b/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model_methods.mustache index d40ec3f4ad..2de30d2e1d 100644 --- a/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model_methods.mustache +++ b/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model_methods.mustache @@ -25,6 +25,9 @@ {{#jsonInclude}} @JsonInclude(JsonInclude.Include.{{.}}) {{/jsonInclude}} +{{#jsonUnwrapped}} + @JsonUnwrapped +{{/jsonUnwrapped}} public {{type}} {{getterName}}() { return {{name}}; } diff --git a/kubernetes-model-generator/openapi/maven-plugin/src/test/java/io/fabric8/kubernetes/schema/generator/SchemaUtilsTest.java b/kubernetes-model-generator/openapi/maven-plugin/src/test/java/io/fabric8/kubernetes/schema/generator/SchemaUtilsTest.java index 2139b4ffdd..d3389f83d0 100644 --- a/kubernetes-model-generator/openapi/maven-plugin/src/test/java/io/fabric8/kubernetes/schema/generator/SchemaUtilsTest.java +++ b/kubernetes-model-generator/openapi/maven-plugin/src/test/java/io/fabric8/kubernetes/schema/generator/SchemaUtilsTest.java @@ -135,6 +135,13 @@ void serializerForJavaClass(String javaClass, String expected) { assertEquals(expected, SchemaUtils.serializerForJavaClass(javaClass)); } + @Test + void isInterface() { + final ObjectSchema schema = new ObjectSchema(); + schema.setExtensions(Map.of("x-kubernetes-fabric8-type", "interface")); + assertTrue(SchemaUtils.isInterface(schema)); + } + @Nested class SchemaToClassName {