Skip to content

Commit

Permalink
feat(openapi): generation of JsonUnwrapped interface fields
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Nuri <[email protected]>
  • Loading branch information
manusa authored Oct 25, 2024
1 parent 7384ef6 commit 0c1c753
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 18 deletions.
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

0 comments on commit 0c1c753

Please sign in to comment.