Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(openapi): generation of JsonUnwrapped interface fields #6496

Merged
merged 1 commit into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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])) {
Expand All @@ -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
Expand All @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -162,6 +169,7 @@ private void processTemplate(TemplateContext ret) {

private List<Map<String, Object>> templateFields(TemplateContext templateContext) {
final List<Map<String, Object>> properties = new ArrayList<>();
final Set<String> interfaceFields = SchemaUtils.interfaceFields(templateContext.getClassSchema());
for (Entry<String, Schema> property : templateContext.getSchemaProperties().entrySet()) {
final Map<String, Object> templateProp = new HashMap<>();
final Schema<?> propertySchema = property.getValue();
Expand Down Expand Up @@ -190,6 +198,10 @@ private List<Map<String, Object>> 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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
{{#jsonInclude}}
@JsonInclude(JsonInclude.Include.{{.}})
{{/jsonInclude}}
{{#jsonUnwrapped}}
@JsonUnwrapped
{{/jsonUnwrapped}}
private {{type}} {{name}}{{#defaultValue}} = {{.}}{{/defaultValue}};{{/fields}}
{{#additionalProperties}}
@JsonIgnore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
{{#jsonInclude}}
@JsonInclude(JsonInclude.Include.{{.}})
{{/jsonInclude}}
{{#jsonUnwrapped}}
@JsonUnwrapped
{{/jsonUnwrapped}}
public {{type}} {{getterName}}() {
return {{name}};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Loading