All Downloads are FREE. Search and download functionalities are using the official Maven repository.

xyz.block.ftl.kotlin.deployment.KotlinCodeGenerator Maven / Gradle / Ivy

There is a newer version: 0.368.1
Show newest version
package xyz.block.ftl.kotlin.deployment;

import java.io.IOException;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import com.squareup.kotlinpoet.AnnotationSpec;
import com.squareup.kotlinpoet.ClassName;
import com.squareup.kotlinpoet.CodeBlock;
import com.squareup.kotlinpoet.FileSpec;
import com.squareup.kotlinpoet.FunSpec;
import com.squareup.kotlinpoet.KModifier;
import com.squareup.kotlinpoet.ParameterizedTypeName;
import com.squareup.kotlinpoet.PropertySpec;
import com.squareup.kotlinpoet.TypeName;
import com.squareup.kotlinpoet.TypeSpec;
import com.squareup.kotlinpoet.TypeVariableName;
import com.squareup.kotlinpoet.WildcardTypeName;

import xyz.block.ftl.GeneratedRef;
import xyz.block.ftl.Subscription;
import xyz.block.ftl.TypeAlias;
import xyz.block.ftl.TypeAliasMapper;
import xyz.block.ftl.VerbClient;
import xyz.block.ftl.VerbClientDefinition;
import xyz.block.ftl.VerbClientEmpty;
import xyz.block.ftl.VerbClientSink;
import xyz.block.ftl.VerbClientSource;
import xyz.block.ftl.deployment.JVMCodeGenerator;
import xyz.block.ftl.v1.schema.Data;
import xyz.block.ftl.v1.schema.Enum;
import xyz.block.ftl.v1.schema.Module;
import xyz.block.ftl.v1.schema.Topic;
import xyz.block.ftl.v1.schema.Type;
import xyz.block.ftl.v1.schema.Verb;

public class KotlinCodeGenerator extends JVMCodeGenerator {

    public static final String CLIENT = "Client";
    public static final String PACKAGE_PREFIX = "ftl.";

    @Override
    protected void generateTypeAliasMapper(String module, String name, String packageName, Optional nativeTypeAlias,
            Path outputDir) throws IOException {
        String thisType = className(name) + TYPE_MAPPER;
        TypeSpec.Builder typeBuilder = TypeSpec.interfaceBuilder(thisType)
                .addAnnotation(AnnotationSpec.builder(TypeAlias.class)
                        .addMember("name=\"" + name + "\"")
                        .addMember("module=\"" + module + "\"")
                        .build())
                .addModifiers(KModifier.PUBLIC);
        if (nativeTypeAlias.isEmpty()) {
            TypeVariableName finalType = TypeVariableName.get("T");
            typeBuilder.addTypeVariable(finalType);
            typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.bestGuess(TypeAliasMapper.class.getName()),
                    finalType, new ClassName("kotlin", "String")), CodeBlock.of(""));
        } else {
            typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.bestGuess(TypeAliasMapper.class.getName()),
                    ClassName.bestGuess(nativeTypeAlias.get()), new ClassName("kotlin", "String")), CodeBlock.of(""));
        }

        FileSpec javaFile = FileSpec.builder(packageName, thisType)
                .addType(typeBuilder.build())
                .build();
        javaFile.writeTo(outputDir);
    }

    protected void generateTopicSubscription(Module module, Topic data, String packageName, Map typeAliasMap,
            Map nativeTypeAliasMap, Path outputDir) throws IOException {
        String thisType = className(data.getName() + "Subscription");

        TypeSpec.Builder dataBuilder = TypeSpec.annotationBuilder(ClassName.bestGuess(thisType));
        dataBuilder.addModifiers(KModifier.PUBLIC);
        if (data.getEvent().hasRef()) {
            dataBuilder.addKdoc("Subscription to the topic of type {@link $L}",
                    data.getEvent().getRef().getName());
        }
        dataBuilder.addAnnotation(AnnotationSpec.builder(Subscription.class)
                .addMember("topic=\"" + data.getName() + "\"")
                .addMember("module=\"" + module.getName() + "\"")
                .addMember("name=\"" + data.getName() + "Subscription\"")
                .build());

        FileSpec javaFile = FileSpec.builder(packageName, thisType)
                .addType(dataBuilder.build())
                .build();

        javaFile.writeTo(outputDir);
    }

    protected void generateEnum(Module module, Enum data, String packageName, Map typeAliasMap,
            Map nativeTypeAliasMap, Path outputDir)
            throws IOException {
        String thisType = className(data.getName());
        TypeSpec.Builder dataBuilder = TypeSpec.enumBuilder(thisType)
                .addAnnotation(
                        AnnotationSpec.builder(GeneratedRef.class)
                                .addMember("name=\"" + data.getName() + "\"")
                                .addMember("module=\"" + module.getName() + "\"").build())
                .addModifiers(KModifier.PUBLIC);

        for (var i : data.getVariantsList()) {
            dataBuilder.addEnumConstant(i.getName());
        }

        FileSpec javaFile = FileSpec.builder(packageName, thisType)
                .addType(dataBuilder.build())
                .build();
        javaFile.writeTo(outputDir);
    }

    protected void generateDataObject(Module module, Data data, String packageName, Map typeAliasMap,
            Map nativeTypeAliasMap, Path outputDir) throws IOException {
        String thisType = className(data.getName());
        TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(thisType)
                .addAnnotation(
                        AnnotationSpec.builder(GeneratedRef.class)
                                .addMember("name=\"" + data.getName() + "\"")
                                .addMember("module=\"" + module.getName() + "\"").build())
                .addModifiers(KModifier.PUBLIC);
        if (!data.getFieldsList().isEmpty()) {
            dataBuilder.addModifiers(KModifier.DATA);
        }

        for (var param : data.getTypeParametersList()) {
            dataBuilder.addTypeVariable(TypeVariableName.get(param.getName()));
        }
        FunSpec.Builder constructorBuilder = FunSpec.constructorBuilder();

        for (var i : data.getFieldsList()) {
            TypeName dataType = toKotlinTypeName(i.getType(), typeAliasMap, nativeTypeAliasMap);
            String name = i.getName();
            var fieldName = toJavaName(name);
            constructorBuilder.addParameter(fieldName, dataType);
            dataBuilder.addProperty(PropertySpec.builder(fieldName, dataType, KModifier.PUBLIC)
                    .initializer(fieldName).build());

        }
        dataBuilder.primaryConstructor(constructorBuilder.build());
        FileSpec javaFile = FileSpec.builder(packageName, thisType)
                .addType(dataBuilder.build())
                .build();

        javaFile.writeTo(outputDir);
    }

    protected void generateVerb(Module module, Verb verb, String packageName, Map typeAliasMap,
            Map nativeTypeAliasMap, Path outputDir)
            throws IOException {
        String thisType = className(verb.getName()) + CLIENT;
        TypeSpec.Builder typeBuilder = TypeSpec.interfaceBuilder(thisType)
                .addAnnotation(AnnotationSpec.builder(VerbClientDefinition.class)
                        .addMember("name=\"" + verb.getName() + "\"")
                        .addMember("module=\"" + module.getName() + "\"")
                        .build())
                .addModifiers(KModifier.PUBLIC);
        if (verb.getRequest().hasUnit() && verb.getResponse().hasUnit()) {
            typeBuilder.addSuperinterface(className(VerbClientEmpty.class), CodeBlock.of(""));
        } else if (verb.getRequest().hasUnit()) {
            typeBuilder.addSuperinterface(ParameterizedTypeName.get(className(VerbClientSource.class),
                    toKotlinTypeName(verb.getResponse(), typeAliasMap, nativeTypeAliasMap)), CodeBlock.of(""));
            typeBuilder.addFunction(FunSpec.builder("call")
                    .returns(toKotlinTypeName(verb.getResponse(), typeAliasMap, nativeTypeAliasMap))
                    .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE, KModifier.ABSTRACT).build());
        } else if (verb.getResponse().hasUnit()) {
            typeBuilder.addSuperinterface(ParameterizedTypeName.get(className(VerbClientSink.class),
                    toKotlinTypeName(verb.getRequest(), typeAliasMap, nativeTypeAliasMap)), CodeBlock.of(""));
            typeBuilder.addFunction(FunSpec.builder("call")
                    .addModifiers(KModifier.OVERRIDE, KModifier.ABSTRACT)
                    .addParameter("value", toKotlinTypeName(verb.getRequest(), typeAliasMap, nativeTypeAliasMap)).build());
        } else {
            typeBuilder.addSuperinterface(ParameterizedTypeName.get(className(VerbClient.class),
                    toKotlinTypeName(verb.getRequest(), typeAliasMap, nativeTypeAliasMap),
                    toKotlinTypeName(verb.getResponse(), typeAliasMap, nativeTypeAliasMap)), CodeBlock.of(""));
            typeBuilder.addFunction(FunSpec.builder("call")
                    .returns(toKotlinTypeName(verb.getResponse(), typeAliasMap, nativeTypeAliasMap))
                    .addParameter("value", toKotlinTypeName(verb.getRequest(), typeAliasMap, nativeTypeAliasMap))
                    .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE, KModifier.ABSTRACT).build());
        }

        FileSpec javaFile = FileSpec.builder(packageName, thisType)
                .addType(typeBuilder.build())
                .build();

        javaFile.writeTo(outputDir);
    }

    private String toJavaName(String name) {
        if (JAVA_KEYWORDS.contains(name)) {
            return name + "_";
        }
        return name;
    }

    private ClassName className(Class clazz) {
        if (clazz.getEnclosingClass() != null) {
            return className(clazz.getEnclosingClass()).nestedClass(clazz.getSimpleName());
        }
        return new ClassName(clazz.getPackage().getName(), clazz.getSimpleName());
    }

    private TypeName toKotlinTypeName(Type type, Map typeAliasMap, Map nativeTypeAliasMap) {
        if (type.hasArray()) {
            return ParameterizedTypeName.get(new ClassName("kotlin.collections", "List"),
                    toKotlinTypeName(type.getArray().getElement(), typeAliasMap, nativeTypeAliasMap));
        } else if (type.hasString()) {
            return new ClassName("kotlin", "String");
        } else if (type.hasOptional()) {
            // Always box for optional, as normal primities can't be null
            return toKotlinTypeName(type.getOptional().getType(), typeAliasMap, nativeTypeAliasMap);
        } else if (type.hasRef()) {
            if (type.getRef().getModule().isEmpty()) {
                return TypeVariableName.get(type.getRef().getName());
            }
            DeclRef key = new DeclRef(type.getRef().getModule(), type.getRef().getName());
            if (nativeTypeAliasMap.containsKey(key)) {
                String className = nativeTypeAliasMap.get(key);
                var idx = className.lastIndexOf('.');
                if (idx != -1) {
                    return new ClassName(className.substring(0, idx), className.substring(idx + 1));
                }
                return new ClassName("", className);
            }
            if (typeAliasMap.containsKey(key)) {
                return toKotlinTypeName(typeAliasMap.get(key), typeAliasMap, nativeTypeAliasMap);
            }
            var params = type.getRef().getTypeParametersList();
            ClassName className = new ClassName(PACKAGE_PREFIX + type.getRef().getModule(), type.getRef().getName());
            if (params.isEmpty()) {
                return className;
            }
            List javaTypes = params.stream()
                    .map(s -> s.hasUnit() ? WildcardTypeName.consumerOf(new ClassName("kotlin", "Any"))
                            : toKotlinTypeName(s, typeAliasMap, nativeTypeAliasMap))
                    .toList();
            return ParameterizedTypeName.get(className, javaTypes.toArray(new TypeName[javaTypes.size()]));
        } else if (type.hasMap()) {
            return ParameterizedTypeName.get(new ClassName("kotlin.collections", "Map"),
                    toKotlinTypeName(type.getMap().getKey(), typeAliasMap, nativeTypeAliasMap),
                    toKotlinTypeName(type.getMap().getValue(), typeAliasMap, nativeTypeAliasMap));
        } else if (type.hasTime()) {
            return className(ZonedDateTime.class);
        } else if (type.hasInt()) {
            return new ClassName("kotlin", "Long");
        } else if (type.hasUnit()) {
            return new ClassName("kotlin", "Unit");
        } else if (type.hasBool()) {
            return new ClassName("kotlin", "Boolean");
        } else if (type.hasFloat()) {
            return new ClassName("kotlin", "Double");
        } else if (type.hasBytes()) {
            return new ClassName("kotlin", "ByteArray");
        } else if (type.hasAny()) {
            return new ClassName("kotlin", "Any");
        }

        throw new RuntimeException("Cannot generate Kotlin type name: " + type);
    }

    // TODO: fix keywords
    protected static final Set JAVA_KEYWORDS = Set.of("abstract", "continue", "for", "new", "switch", "assert",
            "default", "goto", "package", "synchronized", "boolean", "do", "if", "private", "this", "break", "double",
            "implements", "protected", "throw", "byte", "else", "import", "public", "throws", "case", "enum", "instanceof",
            "return", "transient", "catch", "extends", "int", "short", "try", "char", "final", "interface", "static", "void",
            "class", "finally", "long", "strictfp", "volatile", "const", "float", "native", "super", "while");
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy