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 committed Oct 25, 2024
1 parent 641b2c8 commit fd32725
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 105 deletions.
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().getClassSimpleName()));
return;
}
settings.getLogger().fine(String.format("Generating %ss", templateContext.getClassName()));
settings.getLogger()
.fine(String.format("Generating %ss", templateContext.getClassInformation().getClassSimpleName()));
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().getClassSimpleName());
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().getClassSimpleName()) != null) {
deserializer = deserializerForJavaClass(ret.getClassInformation().getClassSimpleName());
} 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
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
* No args constructor for use in serialization
*{{emptySpace}}
*/
public {{className}}() {
public {{classInformation.classSimpleName}}() {
}

public {{className}}({{#fields}}{{type}} {{name}}{{^-last}}, {{/-last}}{{/fields}}) {
public {{classInformation.classSimpleName}}({{#fields}}{{type}} {{name}}{{^-last}}, {{/-last}}{{/fields}}) {
super();
{{#fields}}
this.{{name}} = {{name}};
Expand Down
Loading

0 comments on commit fd32725

Please sign in to comment.