Skip to content

Commit

Permalink
feat(openapi): generated classes can implement interfaces
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Nuri <marc@marcnuri.com>
  • Loading branch information
manusa authored Oct 25, 2024
1 parent 641b2c8 commit d6e4c22
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func processProtobufTags(_ *generator.Context, _ *types.Package, t *types.Type,
tags := reflect.StructTag(m.Tags)
protobufTag := tags.Get("protobuf")
jsonTag := tags.Get("json")
if protobufTag != "" && jsonTag != "" && strings.Contains(protobufTag, "json=") {
if protobufTag != "" && strings.Contains(protobufTag, "json=") {
name := strings.Split(protobufTag, "json=")[1]
name = strings.Split(name, ",")[0]
var updatedJsonTag string
Expand All @@ -134,7 +134,11 @@ func processProtobufTags(_ *generator.Context, _ *types.Package, t *types.Type,
} else {
updatedJsonTag = name
}
t.Members[memberIndex].Tags = strings.Replace(t.Members[memberIndex].Tags, jsonTag, updatedJsonTag, 1)
if jsonTag == "" {
t.Members[memberIndex].Tags = t.Members[memberIndex].Tags + " json:\"" + updatedJsonTag+"\""
} else {
t.Members[memberIndex].Tags = strings.Replace(t.Members[memberIndex].Tags, jsonTag, updatedJsonTag, 1)
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions kubernetes-model-generator/openapi/maven-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
limitations under the License.
-->
<!--
Use
mvn -Dinvoker.mavenExecutable=mvnDebug clean verify
for running a test with mvnDebug
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,35 @@
@Getter
public class ClassInformation implements ImportManager {

private final SchemaUtils schemaUtils;
private final Set<String> imports;
private final String kubernetesListType;
private final String packageName;
private final boolean inRootPackage;
private final boolean isInterface;
private final boolean isHasMetadata;
private final boolean isNamespaced;
private final String classSimpleName;
private final String className;
private final String implementedInterfaces;
private final JsonSubTypes jsonSubTypes;

ClassInformation(SchemaUtils schemaUtils, Map.Entry<String, Schema<?>> clazz) {
this.schemaUtils = schemaUtils;
imports = new TreeSet<>(new ImportOrderComparator());
final var classKey = clazz.getKey();
final var classSchema = clazz.getValue();
final var apiVersion = schemaUtils.getSettings().getApiVersions().get(classKey);
// packageName must be resolved first, since the rest of computed fields depend on it
packageName = schemaUtils.toModelPackage(classKey.substring(0, classKey.lastIndexOf('.')));
kubernetesListType = apiVersion == null ? null : schemaUtils.kubernetesListType(this, classSchema);
inRootPackage = getPackageName().equals(schemaUtils.getSettings().getPackageName());
isInterface = SchemaUtils.isInterface(classSchema);
isHasMetadata = apiVersion != null && kubernetesListType == null && schemaUtils.isHasMetadata(classSchema);
isNamespaced = apiVersion != null && apiVersion.isNamespaced();
classSimpleName = SchemaUtils.refToClassName(classKey);
className = getPackageName() + "." + getClassSimpleName();
implementedInterfaces = resolveImplementedInterfaces(classSchema);
if (isInterface) {
addImport("com.fasterxml.jackson.annotation.JsonSubTypes");
addImport("com.fasterxml.jackson.annotation.JsonTypeInfo");
Expand All @@ -49,7 +67,77 @@ public class ClassInformation implements ImportManager {
}
}

@Override
public boolean hasSimpleClassName(String className) {
// If the provided class name matches the current class simple name it means that we'll need the fully
// qualified class name to avoid conflicts.
if (simpleClassName(className).equals(getClassSimpleName())) {
return true;
}
return ImportManager.super.hasSimpleClassName(className);
}

boolean isEditable() {
return !isInterface();
}

public final String getClassInterface() {
return isInterface() ? "interface" : "class";
}

public final String getBuilderName() {
return getClassSimpleName() + "Builder";
}

private String resolveImplementedInterfaces(Schema<?> classSchema) {
final StringBuilder implementedInterfaces = new StringBuilder();
final var interfaceImplemented = SchemaUtils.interfaceImplemented(classSchema);
if (interfaceImplemented != null) {
implementedInterfaces.append(interfaceImplemented).append(", ");
}
if (isEditable()) {
addImport("com.fasterxml.jackson.annotation.JsonIgnore");
addImport(schemaUtils.getSettings().getBuilderPackage() + "." + "Editable");
implementedInterfaces.append("Editable<").append(getBuilderName()).append(">");
implementedInterfaces.append(" , "); // TODO: weird comma introduced by jsonschema2pojo
}
// HasMetadata
if (isHasMetadata()) {
if (!isInRootPackage()) {
addImport(schemaUtils.getSettings().getHasMetadataClass());
}
implementedInterfaces.append(schemaUtils.getSettings().getHasMetadataClassSimpleName());
}
// KubernetesResource
else {
if (getClassSimpleName().equals(schemaUtils.getSettings().getKubernetesResourceClassSimpleName())) {
// There's a class actually named KubernetesResource in the tekton package
implementedInterfaces.append(schemaUtils.getSettings().getKubernetesResourceClass());
} else {
if (!isInRootPackage()) {
addImport(schemaUtils.getSettings().getKubernetesResourceClass());
}
implementedInterfaces.append(schemaUtils.getSettings().getKubernetesResourceClassSimpleName());
}
}
// Namespaced
if (isNamespaced() && getKubernetesListType() == null) {
if (!isInRootPackage()) {
addImport(schemaUtils.getSettings().getNamespacedClass());
}
implementedInterfaces.append(", ").append(schemaUtils.getSettings().getNamespacedClassSimpleName());
}
// KubernetesResourceList
if (getKubernetesListType() != null) {
if (!isInRootPackage()) {
addImport(schemaUtils.getSettings().getKubernetesResourceListClass());
}
implementedInterfaces.append(", ").append(schemaUtils.getSettings().getKubernetesResourceListClassSimpleName())
.append("<")
// TODO: remove after generator migration, match jsonschema2pojo generation for KubernetesResourceList
.append(getPackageName()).append(".").append(getKubernetesListType())
.append(">");
}
return implementedInterfaces.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ void generate() {
.forEach(classEntry -> {
final TemplateContext templateContext = new TemplateContext(settings, classEntry);
if (hasOverride(templateContext)) {
settings.getLogger().fine(String.format("Skipping %s since it has an override", templateContext.getClassName()));
settings.getLogger().fine(String.format("Skipping %s since it has an override",
templateContext.getClassInformation().getClassName()));
return;
}
settings.getLogger().fine(String.format("Generating %ss", templateContext.getClassName()));
settings.getLogger()
.fine(String.format("Generating %ss", templateContext.getClassInformation().getClassName()));
mkPackageDirectories(templateContext);
processTemplate(templateContext);
final String fileContents = modelTemplate.execute(templateContext.getContext());
Expand All @@ -105,7 +107,7 @@ private void processTemplate(TemplateContext ret) {
ret.addImport("io.fabric8.kubernetes.model.annotation.Version");
ret.put("version", ret.getApiVersion().getVersion());
// TODO: we might want to generify this logic for other annotations and imports
if (Objects.equals("Group", ret.getClassSimpleName())) {
if (Objects.equals("Group", ret.getClassInformation().getClassSimpleName())) {
ret.put("group", "@io.fabric8.kubernetes.model.annotation.Group(\"" + ret.getApiVersion().getGroup() + "\")");
} else {
ret.addImport("io.fabric8.kubernetes.model.annotation.Group");
Expand All @@ -115,16 +117,16 @@ private void processTemplate(TemplateContext ret) {
ret.addImport("io.sundr.transform.annotations.TemplateTransformations");
ret.put("kubernetesResourceClass", settings.getKubernetesResourceClass());
}
final String serializer = serializerForJavaClass(ret.getClassName());
final String serializer = serializerForJavaClass(ret.getClassInformation().getClassName());
if (serializer != null) {
ret.addImport("com.fasterxml.jackson.databind.annotation.JsonSerialize");
ret.put("classJsonSerializeUsing", serializer);
}
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 if (deserializerForJavaClass(ret.getClassInformation().getClassName()) != null) {
deserializer = deserializerForJavaClass(ret.getClassInformation().getClassName());
} else {
deserializer = "com.fasterxml.jackson.databind.JsonDeserializer.None.class";
}
Expand All @@ -146,10 +148,7 @@ private void processTemplate(TemplateContext ret) {
ret.put("hasDescription", !sanitizeDescription(ret.getClassSchema().getDescription()).trim().isEmpty());
ret.put("description", sanitizeDescription(ret.getClassSchema().getDescription()));
}
ret.put("classInterface", ret.getClassInformation().isInterface() ? "interface" : "class");
ret.put("className", ret.getClassSimpleName());
ret.put("implementsExtends", ret.getClassInformation().isInterface() ? "extends" : "implements");
ret.put("implementedInterfaces", resolveImplementedInterfaces(ret));
final List<Map<String, Object>> templateFields = templateFields(ret);
ret.put("fields", templateFields);
if (!templateFields.isEmpty()) {
Expand Down Expand Up @@ -225,18 +224,19 @@ private List<Map<String, Object>> templateFields(TemplateContext templateContext
&& Objects.equals(type, "String")
&& templateContext.getApiVersion() != null) {
templateProp.put("legacyRequired", true); // TODO: remove after generator migration
templateProp.put("defaultValue", String.format("\"%s\"", templateContext.getClassSimpleName()));
templateProp.put("defaultValue", String.format("\"%s\"", templateContext.getClassInformation().getClassSimpleName()));
} else if (Objects.equals(property.getKey(), "apiVersion")
&& Objects.equals(type, "String")
&& templateContext.getApiVersion() != null) {
templateProp.put("legacyRequired", true); // TODO: remove after generator migration
templateProp.put("defaultValue", String.format("\"%s\"", templateContext.getApiVersion()));
}
// TODO: remove after generator migration, match jsonschema2pojo generation for items
if (templateContext.getKubernetesListType() != null
if (templateContext.getClassInformation().getKubernetesListType() != null
&& Objects.equals(property.getKey(), "items")) {
templateProp.put("type",
"List<" + templateContext.getPackageName() + "." + templateContext.getKubernetesListType() + ">");
"List<" + templateContext.getPackageName() + "." + templateContext.getClassInformation().getKubernetesListType()
+ ">");
}
}
return properties;
Expand All @@ -253,7 +253,7 @@ private void mkPackageDirectories(TemplateContext templateContext) {
private boolean hasOverride(TemplateContext templateContext) {
return settings.getOverridesDirectory().toPath()
.resolve(templateContext.getPackageName().replace('.', File.separatorChar))
.resolve(templateContext.getClassSimpleName().concat(".java"))
.resolve(templateContext.getClassInformation().getClassSimpleName().concat(".java"))
.toFile().exists();
}

Expand All @@ -262,54 +262,6 @@ private Path resolvePackageDirectory(TemplateContext templateContext) {
.resolve(templateContext.getPackageName().replace('.', File.separatorChar));
}

private String resolveImplementedInterfaces(TemplateContext templateContext) {
final StringBuilder implementedInterfaces = new StringBuilder();
if (templateContext.getClassInformation().isEditable()) {
templateContext.addImport("com.fasterxml.jackson.annotation.JsonIgnore");
templateContext.addImport(settings.getBuilderPackage() + "." + "Editable");
implementedInterfaces.append("Editable<").append(templateContext.getClassSimpleName()).append("Builder>");
implementedInterfaces.append(" , "); // TODO: weird comma introduced by jsonschema2pojo
}
// HasMetadata
if (templateContext.isHasMetadata()) {
if (!templateContext.isInRootPackage()) {
templateContext.addImport(settings.getHasMetadataClass());
}
implementedInterfaces.append(settings.getHasMetadataClassSimpleName());
}
// KubernetesResource
else {
if (templateContext.getClassSimpleName().equals(settings.getKubernetesResourceClassSimpleName())) {
// There's a class actually named KubernetesResource in the tekton package
implementedInterfaces.append(settings.getKubernetesResourceClass());
} else {
if (!templateContext.isInRootPackage()) {
templateContext.addImport(settings.getKubernetesResourceClass());
}
implementedInterfaces.append(settings.getKubernetesResourceClassSimpleName());
}
}
// Namespaced
if (templateContext.isNamespaced() && templateContext.getKubernetesListType() == null) {
if (!templateContext.isInRootPackage()) {
templateContext.addImport(settings.getNamespacedClass());
}
implementedInterfaces.append(", ").append(settings.getNamespacedClassSimpleName());
}
// KubernetesResourceList
if (templateContext.getKubernetesListType() != null) {
if (!templateContext.isInRootPackage()) {
templateContext.addImport(settings.getKubernetesResourceListClass());
}
implementedInterfaces.append(", ").append(settings.getKubernetesResourceListClassSimpleName())
.append("<")
// TODO: remove after generator migration, match jsonschema2pojo generation for KubernetesResourceList
.append(templateContext.getPackageName()).append(".").append(templateContext.getKubernetesListType())
.append(">");
}
return implementedInterfaces.toString();
}

private Map<String, List<String>> buildableReferences(TemplateContext templateContext, List<Map<String, Object>> fields) {
final List<String> references = new ArrayList<>(settings.getBuildableReferences());
references.add(0, settings.getObjectMetaClass());
Expand All @@ -321,7 +273,7 @@ private Map<String, List<String>> buildableReferences(TemplateContext templateCo
return Collections.singletonMap("refs", references.stream()
.map(r -> {
final String referenceSimpleClass = r.substring(r.lastIndexOf('.') + 1);
if (templateContext.getClassSimpleName().equals(referenceSimpleClass)) {
if (templateContext.getClassInformation().getClassSimpleName().equals(referenceSimpleClass)) {
return r;
}
// Don't add import if there's an import for a class with the same name e.g. ObjectReference in k8s core and monitoring
Expand All @@ -335,7 +287,8 @@ private Map<String, List<String>> buildableReferences(TemplateContext templateCo
}

private void writeFile(TemplateContext context, String fileContents) {
final Path file = resolvePackageDirectory(context).resolve(context.getClassSimpleName().concat(".java"));
final Path file = resolvePackageDirectory(context)
.resolve(context.getClassInformation().getClassSimpleName().concat(".java"));
generatorUtils.writeFile(file, fileContents);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,6 @@ final class TemplateContext implements ImportManager {
private final Schema<?> classSchema;
private final ApiVersion apiVersion;
private final ClassInformation classInformation;
// TODO: Move to ClassInformation
private final boolean inRootPackage;
private final String classSimpleName;
private final String className;
private final boolean hasMetadata;
private final String kubernetesListType;
private final Map<String, Object> context;

TemplateContext(GeneratorSettings settings, Map.Entry<String, Schema<?>> clazz) {
Expand All @@ -48,11 +42,6 @@ final class TemplateContext implements ImportManager {
classSchema = clazz.getValue();
apiVersion = settings.getApiVersions().get(classKey);
classInformation = new ClassInformation(schemaUtils, clazz);
inRootPackage = getClassInformation().getPackageName().equals(settings.getPackageName());
classSimpleName = SchemaUtils.refToClassName(classKey);
className = getClassInformation().getPackageName() + "." + classSimpleName;
kubernetesListType = apiVersion == null ? null : schemaUtils.kubernetesListType(this, classSchema);
hasMetadata = apiVersion != null && kubernetesListType == null && schemaUtils.isHasMetadata(classSchema);
context = new HashMap<>();
context.put("imports", classInformation.getImports());
context.put("classInformation", classInformation);
Expand All @@ -66,10 +55,6 @@ Map<String, Schema> getSchemaProperties() {
return classSchema.getProperties() == null ? Collections.emptyMap() : classSchema.getProperties();
}

boolean isNamespaced() {
return getApiVersion() != null && getApiVersion().isNamespaced();
}

@Override
public String getPackageName() {
return getClassInformation().getPackageName();
Expand All @@ -82,11 +67,6 @@ public Collection<String> getImports() {

@Override
public boolean hasSimpleClassName(String className) {
// If the provided class name matches the current class simple name it means that we'll need the fully
// qualified class name to avoid conflicts.
if (simpleClassName(className).equals(classSimpleName)) {
return true;
}
return ImportManager.super.hasSimpleClassName(className);
return getClassInformation().hasSimpleClassName(className);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import io.swagger.v3.oas.models.media.MapSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import lombok.Getter;

import java.util.Arrays;
import java.util.Collection;
Expand All @@ -45,6 +46,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Getter
public class SchemaUtils {

public static final String APPLICATION_JSON = "application/json";
Expand Down Expand Up @@ -282,6 +284,13 @@ public static List<String> interfaceImplementation(Schema<?> schema) {
return Collections.emptyList();
}

public static String interfaceImplemented(Schema<?> schema) {
if (schema.getExtensions() != null && schema.getExtensions().containsKey("x-kubernetes-fabric8-implements")) {
return refToClassName(schema.getExtensions().get("x-kubernetes-fabric8-implements").toString());
}
return null;
}

public static boolean isArray(Schema<?> schema) {
return schema instanceof ArraySchema;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {{.}};
/**
* {{description}}
*/{{/hasDescription}}
{{> model_class_annotations}}public {{classInterface}} {{className}} {{implementsExtends}} {{implementedInterfaces}}
{{> model_class_annotations}}public {{classInformation.classInterface}} {{classInformation.classSimpleName}} {{implementsExtends}} {{classInformation.implementedInterfaces}}
{
{{>model_fields}}

Expand Down
Loading

0 comments on commit d6e4c22

Please sign in to comment.