Skip to content

Commit

Permalink
feat: Write comments into generated Java and Kotlin (#3104)
Browse files Browse the repository at this point in the history
Closes #2791
  • Loading branch information
tomdaffurn authored Oct 14, 2024
1 parent 1a5602f commit 59abfa8
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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.TypeAlias;
import xyz.block.ftl.v1.schema.Verb;

public abstract class JVMCodeGenerator implements CodeGenProvider {
Expand Down Expand Up @@ -77,7 +78,7 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
if (existing != null) {
nativeTypeAliasMap.put(new DeclRef(module.getName(), data.getName()),
nativeName);
generateTypeAliasMapper(module.getName(), data.getName(), packageName,
generateTypeAliasMapper(module.getName(), data, packageName,
Optional.of(nativeName),
context.outDir());
handled = true;
Expand All @@ -87,7 +88,7 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
}
}
if (!handled) {
generateTypeAliasMapper(module.getName(), data.getName(), packageName, Optional.empty(),
generateTypeAliasMapper(module.getName(), data, packageName, Optional.empty(),
context.outDir());
typeAliasMap.put(new DeclRef(module.getName(), data.getName()), data.getType());
}
Expand Down Expand Up @@ -141,7 +142,7 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
return true;
}

protected abstract void generateTypeAliasMapper(String module, String name, String packageName,
protected abstract void generateTypeAliasMapper(String module, TypeAlias typeAlias, String packageName,
Optional<String> nativeTypeAlias, Path outputDir) throws IOException;

protected abstract void generateTopicSubscription(Module module, Topic data, String packageName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ public class JavaCodeGenerator extends JVMCodeGenerator {
public static final String PACKAGE_PREFIX = "ftl.";

@Override
protected void generateTypeAliasMapper(String module, String name, String packageName, Optional<String> nativeTypeAlias,
Path outputDir) throws IOException {
TypeSpec.Builder typeBuilder = TypeSpec.interfaceBuilder(className(name) + TYPE_MAPPER)
protected void generateTypeAliasMapper(String module, xyz.block.ftl.v1.schema.TypeAlias typeAlias,
String packageName, Optional<String> nativeTypeAlias, Path outputDir) throws IOException {
TypeSpec.Builder typeBuilder = TypeSpec.interfaceBuilder(className(typeAlias.getName()) + TYPE_MAPPER)
.addAnnotation(AnnotationSpec.builder(TypeAlias.class)
.addMember("name", "\"" + name + "\"")
.addMember("name", "\"" + typeAlias.getName() + "\"")
.addMember("module", "\"" + module + "\"")
.build())
.addModifiers(Modifier.PUBLIC);
.addModifiers(Modifier.PUBLIC)
.addJavadoc(String.join("\n", typeAlias.getCommentsList()));
if (nativeTypeAlias.isEmpty()) {
TypeVariableName finalType = TypeVariableName.get("T");
typeBuilder.addTypeVariable(finalType);
Expand All @@ -71,12 +72,9 @@ protected void generateTypeAliasMapper(String module, String name, String packag
typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.get(TypeAliasMapper.class),
ClassName.bestGuess(nativeTypeAlias.get()), ClassName.get(String.class)));
}
TypeSpec theType = typeBuilder
.build();

TypeSpec theType = typeBuilder.build();
JavaFile javaFile = JavaFile.builder(packageName, theType)
.build();

javaFile.writeTo(outputDir);
}

Expand Down Expand Up @@ -104,18 +102,19 @@ protected void generateTopicSubscription(Module module, Topic data, String packa
javaFile.writeTo(outputDir);
}

protected void generateEnum(Module module, Enum data, String packageName, Map<DeclRef, Type> typeAliasMap,
protected void generateEnum(Module module, Enum ennum, String packageName, Map<DeclRef, Type> typeAliasMap,
Map<DeclRef, String> nativeTypeAliasMap, Map<DeclRef, List<EnumInfo>> enumVariantInfoMap, Path outputDir)
throws IOException {
String interfaceType = className(data.getName());
if (data.hasType()) {
String interfaceType = className(ennum.getName());
if (ennum.hasType()) {
//Enums with a type are "value enums" - Java natively supports these
TypeSpec.Builder dataBuilder = TypeSpec.enumBuilder(interfaceType)
.addAnnotation(getGeneratedRefAnnotation(module.getName(), data.getName()))
.addAnnotation(getGeneratedRefAnnotation(module.getName(), ennum.getName()))
.addAnnotation(AnnotationSpec.builder(xyz.block.ftl.Enum.class).build())
.addModifiers(Modifier.PUBLIC);
.addModifiers(Modifier.PUBLIC)
.addJavadoc(String.join("\n", ennum.getCommentsList()));

TypeName enumType = toAnnotatedJavaTypeName(data.getType(), typeAliasMap, nativeTypeAliasMap);
TypeName enumType = toAnnotatedJavaTypeName(ennum.getType(), typeAliasMap, nativeTypeAliasMap);
dataBuilder.addField(enumType, "value", Modifier.PRIVATE, Modifier.FINAL);
dataBuilder.addMethod(MethodSpec.constructorBuilder()
.addParameter(enumType, "value")
Expand All @@ -128,8 +127,8 @@ protected void generateEnum(Module module, Enum data, String packageName, Map<De
.addStatement("return value")
.build());

var format = data.getType().hasString() ? "$S" : "$L";
for (var i : data.getVariantsList()) {
var format = ennum.getType().hasString() ? "$S" : "$L";
for (var i : ennum.getVariantsList()) {
Object value = toJavaValue(i.getValue());
dataBuilder.addEnumConstant(i.getName(), TypeSpec.anonymousClassBuilder(format, value).build());
}
Expand All @@ -143,14 +142,15 @@ protected void generateEnum(Module module, Enum data, String packageName, Map<De
// TODO JavaPoet doesn't support 'sealed' or 'permits' syntax yet, so we can't seal the interface
// https://github.com/square/javapoet/issues/823
TypeSpec.Builder interfaceBuilder = TypeSpec.interfaceBuilder(interfaceType)
.addAnnotation(getGeneratedRefAnnotation(module.getName(), data.getName()))
.addAnnotation(getGeneratedRefAnnotation(module.getName(), ennum.getName()))
.addAnnotation(AnnotationSpec.builder(xyz.block.ftl.Enum.class).build())
.addModifiers(Modifier.PUBLIC);
.addModifiers(Modifier.PUBLIC)
.addJavadoc(String.join("\n", ennum.getCommentsList()));

Map<String, TypeName> variantValuesTypes = data.getVariantsList().stream().collect(
Map<String, TypeName> variantValuesTypes = ennum.getVariantsList().stream().collect(
Collectors.toMap(EnumVariant::getName, v -> toAnnotatedJavaTypeName(v.getValue().getTypeValue().getValue(),
typeAliasMap, nativeTypeAliasMap)));
for (var variant : data.getVariantsList()) {
for (var variant : ennum.getVariantsList()) {
// Interface has isX and getX methods for each variant
String name = variant.getName();
TypeName valueTypeName = variantValuesTypes.get(name);
Expand All @@ -170,7 +170,7 @@ protected void generateEnum(Module module, Enum data, String packageName, Map<De
// Store this variant in enumVariantInfoMap so we can fetch it later
DeclRef key = new DeclRef(module.getName(), name);
List<EnumInfo> variantInfos = enumVariantInfoMap.computeIfAbsent(key, k -> new ArrayList<>());
variantInfos.add(new EnumInfo(interfaceType, variant, data.getVariantsList()));
variantInfos.add(new EnumInfo(interfaceType, variant, ennum.getVariantsList()));
} else {
// Value type isn't a Ref, so we make a wrapper class that implements our interface
TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(className(name))
Expand Down Expand Up @@ -206,7 +206,8 @@ protected void generateDataObject(Module module, Data data, String packageName,
String thisType = className(data.getName());
TypeSpec.Builder dataBuilder = TypeSpec.classBuilder(thisType)
.addAnnotation(getGeneratedRefAnnotation(module.getName(), data.getName()))
.addModifiers(Modifier.PUBLIC);
.addModifiers(Modifier.PUBLIC)
.addJavadoc(String.join("\n", data.getCommentsList()));

// if data is part of a type enum, generate the interface methods for each variant
DeclRef key = new DeclRef(module.getName(), data.getName());
Expand Down Expand Up @@ -284,36 +285,40 @@ protected void generateVerb(Module module, Verb verb, String packageName, Map<De
.addMember("module", "\"" + module.getName() + "\"")
.build())
.addModifiers(Modifier.PUBLIC);
var comments = String.join("\n", verb.getCommentsList());
if (verb.getRequest().hasUnit() && verb.getResponse().hasUnit()) {
typeBuilder.addSuperinterface(ClassName.get(VerbClientEmpty.class));
typeBuilder.addSuperinterface(ClassName.get(VerbClientEmpty.class))
.addJavadoc(comments);
} else if (verb.getRequest().hasUnit()) {
typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.get(VerbClientSource.class),
toJavaTypeName(verb.getResponse(), typeAliasMap, nativeTypeAliasMap, true)));
typeBuilder.addMethod(MethodSpec.methodBuilder("call")
.returns(toAnnotatedJavaTypeName(verb.getResponse(), typeAliasMap, nativeTypeAliasMap))
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC).build());
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.addJavadoc(comments)
.build());
} else if (verb.getResponse().hasUnit()) {
typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.get(VerbClientSink.class),
toJavaTypeName(verb.getRequest(), typeAliasMap, nativeTypeAliasMap, true)));
typeBuilder.addMethod(MethodSpec.methodBuilder("call").returns(TypeName.VOID)
typeBuilder.addMethod(MethodSpec.methodBuilder("call")
.returns(TypeName.VOID)
.addParameter(toAnnotatedJavaTypeName(verb.getRequest(), typeAliasMap, nativeTypeAliasMap), "value")
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC).build());
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.addJavadoc(comments)
.build());
} else {
typeBuilder.addSuperinterface(ParameterizedTypeName.get(ClassName.get(VerbClient.class),
toJavaTypeName(verb.getRequest(), typeAliasMap, nativeTypeAliasMap, true),
toJavaTypeName(verb.getResponse(), typeAliasMap, nativeTypeAliasMap, true)));
typeBuilder.addMethod(MethodSpec.methodBuilder("call")
.returns(toAnnotatedJavaTypeName(verb.getResponse(), typeAliasMap, nativeTypeAliasMap))
.addParameter(toAnnotatedJavaTypeName(verb.getRequest(), typeAliasMap, nativeTypeAliasMap), "value")
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC).build());
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.addJavadoc(comments)
.build());
}

TypeSpec helloWorld = typeBuilder
.build();

JavaFile javaFile = JavaFile.builder(packageName, helloWorld)
.build();

JavaFile javaFile = JavaFile.builder(packageName, typeBuilder.build()).build();
javaFile.writeTo(outputDir);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,17 @@ public class KotlinCodeGenerator extends JVMCodeGenerator {
public static final String PACKAGE_PREFIX = "ftl.";

@Override
protected void generateTypeAliasMapper(String module, String name, String packageName, Optional<String> nativeTypeAlias,
protected void generateTypeAliasMapper(String module, xyz.block.ftl.v1.schema.TypeAlias typeAlias, String packageName,
Optional<String> nativeTypeAlias,
Path outputDir) throws IOException {
String thisType = className(name) + TYPE_MAPPER;
String thisType = className(typeAlias.getName()) + TYPE_MAPPER;
TypeSpec.Builder typeBuilder = TypeSpec.interfaceBuilder(thisType)
.addAnnotation(AnnotationSpec.builder(TypeAlias.class)
.addMember("name=\"" + name + "\"")
.addMember("name=\"" + typeAlias.getName() + "\"")
.addMember("module=\"" + module + "\"")
.build())
.addModifiers(KModifier.PUBLIC);
.addModifiers(KModifier.PUBLIC)
.addKdoc(String.join("\n", typeAlias.getCommentsList()));
if (nativeTypeAlias.isEmpty()) {
TypeVariableName finalType = TypeVariableName.get("T");
typeBuilder.addTypeVariable(finalType);
Expand Down Expand Up @@ -101,7 +103,8 @@ protected void generateEnum(Module module, Enum data, String packageName, Map<De
AnnotationSpec.builder(GeneratedRef.class)
.addMember("name=\"" + data.getName() + "\"")
.addMember("module=\"" + module.getName() + "\"").build())
.addModifiers(KModifier.PUBLIC);
.addModifiers(KModifier.PUBLIC)
.addKdoc(String.join("\n", data.getCommentsList()));

for (var i : data.getVariantsList()) {
dataBuilder.addEnumConstant(i.getName());
Expand All @@ -122,7 +125,8 @@ protected void generateDataObject(Module module, Data data, String packageName,
AnnotationSpec.builder(GeneratedRef.class)
.addMember("name=\"" + data.getName() + "\"")
.addMember("module=\"" + module.getName() + "\"").build())
.addModifiers(KModifier.PUBLIC);
.addModifiers(KModifier.PUBLIC)
.addKdoc(String.join("\n", data.getCommentsList()));
if (!data.getFieldsList().isEmpty()) {
dataBuilder.addModifiers(KModifier.DATA);
}
Expand Down Expand Up @@ -159,28 +163,36 @@ protected void generateVerb(Module module, Verb verb, String packageName, Map<De
.addMember("module=\"" + module.getName() + "\"")
.build())
.addModifiers(KModifier.PUBLIC);
String comments = String.join("\n", verb.getCommentsList());
if (verb.getRequest().hasUnit() && verb.getResponse().hasUnit()) {
typeBuilder.addSuperinterface(className(VerbClientEmpty.class), CodeBlock.of(""));
typeBuilder.addSuperinterface(className(VerbClientEmpty.class), CodeBlock.of(""))
.addKdoc(comments);
} 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());
.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE, KModifier.ABSTRACT)
.addKdoc(comments)
.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());
.addParameter("value", toKotlinTypeName(verb.getRequest(), typeAliasMap, nativeTypeAliasMap))
.addKdoc(comments)
.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());
.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE, KModifier.ABSTRACT)
.addKdoc(comments)
.build());
}

FileSpec javaFile = FileSpec.builder(packageName, thisType)
Expand Down
29 changes: 29 additions & 0 deletions jvm-runtime/testdata/go/gomodule/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ import (
"github.com/TBD54566975/ftl/go-runtime/ftl"
)

// Comment on topic
//
//ftl:export
var TestTopic = ftl.Topic[TestObject]("testTopic")

// Comment on subscription
//
//ftl:export
var testSubscription = ftl.Subscription(TestTopic, "testSubscription")

// Comment on data
type TestObject struct {
IntField int
FloatField float64
Expand All @@ -21,6 +32,8 @@ type TestObject struct {
MapField map[string]string
}

// Multiline comment
// Is longer than others
type TestObjectOptionalFields struct {
IntField ftl.Option[int]
FloatField ftl.Option[float64]
Expand All @@ -39,6 +52,8 @@ type ParameterizedType[T any] struct {
Map map[string]T
}

// Comment on value enum
//
//ftl:enum export
type ColorInt int

Expand Down Expand Up @@ -77,13 +92,17 @@ type TypeEnumWrapper struct {
Type TypeWrapperEnum
}

// Comment on type enum
//
//ftl:enum
type Animal interface{ animal() }
type Cat struct {
Name string
Breed string
FurLength int
}

// Comment on type enum variant
type Dog struct{}

func (Cat) animal() {}
Expand All @@ -101,12 +120,16 @@ type AnimalWrapper struct {
//func (Word) mixed() {}
//func (Dog) mixed() {}

// Comment on type alias
//
//ftl:typealias
//ftl:typemap kotlin "web5.sdk.dids.didcore.Did"
type DID = did.DID

// Test different signatures

// Comment on source verb
//
//ftl:verb export
func SourceVerb(ctx context.Context) (string, error) {
return "Source Verb", nil
Expand All @@ -117,11 +140,15 @@ type ExportedType[T any, S any] interface {
FTLDecode(in S) (T, error)
}

// Comment on sink verb
//
//ftl:verb export
func SinkVerb(ctx context.Context, req string) error {
return nil
}

// Comment on empty verb
//
//ftl:verb export
func EmptyVerb(ctx context.Context) error {
return nil
Expand All @@ -134,6 +161,8 @@ func ErrorEmptyVerb(ctx context.Context) error {

// Test different param and return types

// Comment on verb
//
//ftl:verb export
func IntVerb(ctx context.Context, val int) (int, error) {
return val, nil
Expand Down

0 comments on commit 59abfa8

Please sign in to comment.