diff --git a/sourcegen-generator-java/src/main/java/io/micronaut/sourcegen/JavaPoetSourceGenerator.java b/sourcegen-generator-java/src/main/java/io/micronaut/sourcegen/JavaPoetSourceGenerator.java index 093ae17f..4b8745c1 100644 --- a/sourcegen-generator-java/src/main/java/io/micronaut/sourcegen/JavaPoetSourceGenerator.java +++ b/sourcegen-generator-java/src/main/java/io/micronaut/sourcegen/JavaPoetSourceGenerator.java @@ -40,6 +40,7 @@ import io.micronaut.sourcegen.model.MethodDef; import io.micronaut.sourcegen.model.ObjectDef; import io.micronaut.sourcegen.model.PropertyDef; +import io.micronaut.sourcegen.model.RecordDef; import io.micronaut.sourcegen.model.StatementDef; import io.micronaut.sourcegen.model.TypeDef; import io.micronaut.sourcegen.model.VariableDef; @@ -69,145 +70,168 @@ public VisitorContext.Language getLanguage() { @Override public void write(ObjectDef objectDef, Writer writer) throws IOException { if (objectDef instanceof ClassDef classDef) { - TypeSpec.Builder classBuilder = TypeSpec.classBuilder(classDef.getSimpleName()); - classBuilder.addModifiers(classDef.getModifiersArray()); - classDef.getTypeVariables().stream().map(this::asTypeVariable).forEach(classBuilder::addTypeVariable); - classDef.getSuperinterfaces().stream().map(this::asType).forEach(classBuilder::addSuperinterface); + writeClass(writer, classDef); + } else if (objectDef instanceof RecordDef recordDef) { + writeRecord(writer, recordDef); + } else if (objectDef instanceof InterfaceDef interfaceDef) { + writeInterface(writer, interfaceDef); + } else { + throw new IllegalStateException("Unknown object definition: " + objectDef); + } + } - for (PropertyDef property : classDef.getProperties()) { - TypeName propertyType = asType(property.getType()); - String propertyName = property.getName(); - FieldSpec.Builder fieldBuilder = FieldSpec.builder( - propertyType, - propertyName - ).addModifiers(Modifier.PRIVATE); - for (AnnotationDef annotation : property.getAnnotations()) { - fieldBuilder.addAnnotation( - asAnnotationSpec(annotation) - ); - } - classBuilder.addField( - fieldBuilder - .build() + private void writeInterface(Writer writer, InterfaceDef interfaceDef) throws IOException { + TypeSpec.Builder interfaceBuilder = TypeSpec.interfaceBuilder(interfaceDef.getSimpleName()); + interfaceBuilder.addModifiers(interfaceDef.getModifiersArray()); + interfaceDef.getTypeVariables().stream().map(this::asTypeVariable).forEach(interfaceBuilder::addTypeVariable); + interfaceDef.getSuperinterfaces().stream().map(this::asType).forEach(interfaceBuilder::addSuperinterface); + + for (PropertyDef property : interfaceDef.getProperties()) { + TypeName propertyType = asType(property.getType()); + String propertyName = property.getName(); + FieldSpec.Builder fieldBuilder = FieldSpec.builder( + propertyType, + propertyName + ).addModifiers(Modifier.PRIVATE); + for (AnnotationDef annotation : property.getAnnotations()) { + fieldBuilder.addAnnotation( + asAnnotationSpec(annotation) ); - String capitalizedPropertyName = NameUtils.capitalize(propertyName); - classBuilder.addMethod(MethodSpec.methodBuilder("get" + capitalizedPropertyName) - .addModifiers(property.getModifiersArray()) - .returns(propertyType) - .addStatement("return this." + propertyName) - .build()); - classBuilder.addMethod(MethodSpec.methodBuilder("set" + capitalizedPropertyName) - .addModifiers(property.getModifiersArray()) - .addParameter(ParameterSpec.builder(propertyType, propertyName).build()) - .addStatement("this." + propertyName + " = " + propertyName) - .build()); } - for (FieldDef field : classDef.getFields()) { - FieldSpec.Builder fieldBuilder = FieldSpec.builder( - asType(field.getType()), - field.getName() - ).addModifiers(field.getModifiersArray()); - for (AnnotationDef annotation : field.getAnnotations()) { - fieldBuilder.addAnnotation( - asAnnotationSpec(annotation) - ); - } - classBuilder.addField( - fieldBuilder - .build() + interfaceBuilder.addField( + fieldBuilder + .build() + ); + String capitalizedPropertyName = NameUtils.capitalize(propertyName); + interfaceBuilder.addMethod(MethodSpec.methodBuilder("get" + capitalizedPropertyName) + .addModifiers(property.getModifiersArray()) + .returns(propertyType) +// .addStatement("return this." + propertyName) + .build()); + interfaceBuilder.addMethod(MethodSpec.methodBuilder("set" + capitalizedPropertyName) + .addModifiers(property.getModifiersArray()) + .addParameter(ParameterSpec.builder(propertyType, propertyName).build()) +// .addStatement("this." + propertyName + " = " + propertyName) + .build()); + } + for (MethodDef method : interfaceDef.getMethods()) { + interfaceBuilder.addMethod( + asMethodSpec(interfaceDef, method) + ); + } + JavaFile javaFile = JavaFile.builder(interfaceDef.getPackageName(), interfaceBuilder.build()).build(); + javaFile.writeTo(writer); + } + + private void writeClass(Writer writer, ClassDef classDef) throws IOException { + TypeSpec.Builder classBuilder = TypeSpec.classBuilder(classDef.getSimpleName()); + classBuilder.addModifiers(classDef.getModifiersArray()); + classDef.getTypeVariables().stream().map(this::asTypeVariable).forEach(classBuilder::addTypeVariable); + classDef.getSuperinterfaces().stream().map(this::asType).forEach(classBuilder::addSuperinterface); + + for (PropertyDef property : classDef.getProperties()) { + TypeName propertyType = asType(property.getType()); + String propertyName = property.getName(); + FieldSpec.Builder fieldBuilder = FieldSpec.builder( + propertyType, + propertyName + ).addModifiers(Modifier.PRIVATE); + for (AnnotationDef annotation : property.getAnnotations()) { + fieldBuilder.addAnnotation( + asAnnotationSpec(annotation) ); } - for (MethodDef method : classDef.getMethods()) { - MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(method.getName()) - .addModifiers(method.getModifiersArray()) - .returns(asType(method.getReturnType())) - .addParameters( - method.getParameters().stream() - .map(param -> ParameterSpec.builder( - asType(param.getType()), - param.getName(), - param.getModifiersArray() - ).build()) - .toList() - ); - for (AnnotationDef annotation : method.getAnnotations()) { - methodBuilder.addAnnotation( - asAnnotationSpec(annotation) - ); - } - method.getStatements().stream() - .map(st -> renderStatement(classDef, method, st)) - .forEach(methodBuilder::addStatement); - classBuilder.addMethod( - methodBuilder.build() + classBuilder.addField( + fieldBuilder + .build() + ); + String capitalizedPropertyName = NameUtils.capitalize(propertyName); + classBuilder.addMethod(MethodSpec.methodBuilder("get" + capitalizedPropertyName) + .addModifiers(property.getModifiersArray()) + .returns(propertyType) + .addStatement("return this." + propertyName) + .build()); + classBuilder.addMethod(MethodSpec.methodBuilder("set" + capitalizedPropertyName) + .addModifiers(property.getModifiersArray()) + .addParameter(ParameterSpec.builder(propertyType, propertyName).build()) + .addStatement("this." + propertyName + " = " + propertyName) + .build()); + } + for (FieldDef field : classDef.getFields()) { + FieldSpec.Builder fieldBuilder = FieldSpec.builder( + asType(field.getType()), + field.getName() + ).addModifiers(field.getModifiersArray()); + for (AnnotationDef annotation : field.getAnnotations()) { + fieldBuilder.addAnnotation( + asAnnotationSpec(annotation) ); } - JavaFile javaFile = JavaFile.builder(classDef.getPackageName(), classBuilder.build()).build(); - javaFile.writeTo(writer); - } else if (objectDef instanceof InterfaceDef interfaceDef) { - TypeSpec.Builder interfaceBuilder = TypeSpec.interfaceBuilder(interfaceDef.getSimpleName()); - interfaceBuilder.addModifiers(interfaceDef.getModifiersArray()); - interfaceDef.getTypeVariables().stream().map(this::asTypeVariable).forEach(interfaceBuilder::addTypeVariable); - interfaceDef.getSuperinterfaces().stream().map(this::asType).forEach(interfaceBuilder::addSuperinterface); + classBuilder.addField( + fieldBuilder + .build() + ); + } + for (MethodDef method : classDef.getMethods()) { + classBuilder.addMethod( + asMethodSpec(classDef, method) + ); + } + JavaFile javaFile = JavaFile.builder(classDef.getPackageName(), classBuilder.build()).build(); + javaFile.writeTo(writer); + } - for (PropertyDef property : interfaceDef.getProperties()) { - TypeName propertyType = asType(property.getType()); - String propertyName = property.getName(); - FieldSpec.Builder fieldBuilder = FieldSpec.builder( - propertyType, - propertyName - ).addModifiers(Modifier.PRIVATE); - for (AnnotationDef annotation : property.getAnnotations()) { - fieldBuilder.addAnnotation( - asAnnotationSpec(annotation) - ); - } - interfaceBuilder.addField( - fieldBuilder - .build() - ); - String capitalizedPropertyName = NameUtils.capitalize(propertyName); - interfaceBuilder.addMethod(MethodSpec.methodBuilder("get" + capitalizedPropertyName) - .addModifiers(property.getModifiersArray()) - .returns(propertyType) -// .addStatement("return this." + propertyName) - .build()); - interfaceBuilder.addMethod(MethodSpec.methodBuilder("set" + capitalizedPropertyName) - .addModifiers(property.getModifiersArray()) - .addParameter(ParameterSpec.builder(propertyType, propertyName).build()) -// .addStatement("this." + propertyName + " = " + propertyName) - .build()); - } - for (MethodDef method : interfaceDef.getMethods()) { - MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(method.getName()) - .addModifiers(method.getModifiersArray()) - .returns(asType(method.getReturnType())) - .addParameters( - method.getParameters().stream() - .map(param -> ParameterSpec.builder( - asType(param.getType()), - param.getName(), - param.getModifiersArray() - ).build()) - .toList() - ); - for (AnnotationDef annotation : method.getAnnotations()) { - methodBuilder.addAnnotation( - asAnnotationSpec(annotation) - ); - } - method.getStatements().stream() - .map(st -> renderStatement(interfaceDef, method, st)) - .forEach(methodBuilder::addStatement); - interfaceBuilder.addMethod( - methodBuilder.build() + private void writeRecord(Writer writer, RecordDef recordDef) throws IOException { + TypeSpec.Builder classBuilder = TypeSpec.recordBuilder(recordDef.getSimpleName()); + classBuilder.addModifiers(recordDef.getModifiersArray()); + recordDef.getTypeVariables().stream().map(this::asTypeVariable).forEach(classBuilder::addTypeVariable); + recordDef.getSuperinterfaces().stream().map(this::asType).forEach(classBuilder::addSuperinterface); + + for (PropertyDef property : recordDef.getProperties()) { + TypeName propertyType = asType(property.getType()); + String propertyName = property.getName(); + ParameterSpec.Builder componentBuilder = ParameterSpec.builder(propertyType, propertyName); + for (AnnotationDef annotation : property.getAnnotations()) { + componentBuilder.addAnnotation( + asAnnotationSpec(annotation) ); } - JavaFile javaFile = JavaFile.builder(interfaceDef.getPackageName(), interfaceBuilder.build()).build(); - javaFile.writeTo(writer); - } else { - throw new IllegalStateException("Unknown object definition: " + objectDef); + classBuilder.addRecordComponent( + componentBuilder.build() + ); + } + for (MethodDef method : recordDef.getMethods()) { + classBuilder.addMethod( + asMethodSpec(recordDef, method) + ); } + JavaFile javaFile = JavaFile.builder(recordDef.getPackageName(), classBuilder.build()).build(); + javaFile.writeTo(writer); + } + + private MethodSpec asMethodSpec(ObjectDef objectDef, MethodDef method) { + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(method.getName()) + .addModifiers(method.getModifiersArray()) + .returns(asType(method.getReturnType())) + .addParameters( + method.getParameters().stream() + .map(param -> ParameterSpec.builder( + asType(param.getType()), + param.getName(), + param.getModifiersArray() + ).build()) + .toList() + ); + for (AnnotationDef annotation : method.getAnnotations()) { + methodBuilder.addAnnotation( + asAnnotationSpec(annotation) + ); + } + method.getStatements().stream() + .map(st -> renderStatement(objectDef, method, st)) + .forEach(methodBuilder::addStatement); + + return methodBuilder.build(); } private TypeVariableName asTypeVariable(TypeDef.TypeVariable tv) { diff --git a/sourcegen-generator-kotlin/src/main/java/io/micronaut/sourcegen/KotlinPoetSourceGenerator.java b/sourcegen-generator-kotlin/src/main/java/io/micronaut/sourcegen/KotlinPoetSourceGenerator.java index 9e674a54..e2c40965 100644 --- a/sourcegen-generator-kotlin/src/main/java/io/micronaut/sourcegen/KotlinPoetSourceGenerator.java +++ b/sourcegen-generator-kotlin/src/main/java/io/micronaut/sourcegen/KotlinPoetSourceGenerator.java @@ -42,6 +42,7 @@ import io.micronaut.sourcegen.model.MethodDef; import io.micronaut.sourcegen.model.ObjectDef; import io.micronaut.sourcegen.model.PropertyDef; +import io.micronaut.sourcegen.model.RecordDef; import io.micronaut.sourcegen.model.StatementDef; import io.micronaut.sourcegen.model.TypeDef; import io.micronaut.sourcegen.model.VariableDef; @@ -78,135 +79,198 @@ public VisitorContext.Language getLanguage() { @Override public void write(ObjectDef objectDef, Writer writer) throws IOException { if (objectDef instanceof ClassDef classDef) { - TypeSpec.Builder classBuilder = TypeSpec.classBuilder(classDef.getSimpleName()); - classBuilder.addModifiers(asKModifiers(classDef.getModifiers())); - classDef.getTypeVariables().stream().map(this::asTypeVariable).forEach(classBuilder::addTypeVariable); - classDef.getSuperinterfaces().stream().map(this::asType).forEach(it -> classBuilder.addSuperinterface(it, CodeBlock.Companion.getEMPTY$kotlinpoet())); - - TypeSpec.Builder companionBuilder = null; - List notNullProperties = new ArrayList<>(); - for (PropertyDef property : classDef.getProperties()) { - PropertySpec propertySpec; - if (property.getType().isNullable()) { - propertySpec = buildNullableProperty( - property.getName(), - property.getType().makeNullable(), - property.getModifiers(), - property.getAnnotations() - ); - } else { - propertySpec = buildNotNullProperty( - property.getName(), - property.getType(), - property.getModifiers(), - property.getAnnotations() - ); - notNullProperties.add(property); - } - classBuilder.addProperty( - propertySpec + writeClass(writer, classDef); + } else if (objectDef instanceof RecordDef recordDef) { + writeRecordDef(writer, recordDef); + } else if (objectDef instanceof InterfaceDef interfaceDef) { + writeInterface(writer, interfaceDef); + } else { + throw new IllegalStateException("Unknown object definition: " + objectDef); + } + } + + private void writeInterface(Writer writer, InterfaceDef interfaceDef) throws IOException { + TypeSpec.Builder interfaceBuilder = TypeSpec.interfaceBuilder(interfaceDef.getSimpleName()); + interfaceBuilder.addModifiers(asKModifiers(interfaceDef.getModifiers())); + interfaceDef.getTypeVariables().stream().map(this::asTypeVariable).forEach(interfaceBuilder::addTypeVariable); + interfaceDef.getSuperinterfaces().stream().map(this::asType).forEach(it -> interfaceBuilder.addSuperinterface(it, CodeBlock.Companion.getEMPTY$kotlinpoet())); + + TypeSpec.Builder companionBuilder = null; + for (PropertyDef property : interfaceDef.getProperties()) { + PropertySpec propertySpec; + if (property.getType().isNullable()) { + propertySpec = buildNullableProperty( + property.getName(), + property.getType().makeNullable(), + property.getModifiers(), + property.getAnnotations() ); - } - if (!notNullProperties.isEmpty()) { - classBuilder.setPrimaryConstructor$kotlinpoet( - FunSpec.constructorBuilder().addModifiers(KModifier.PUBLIC).addParameters( - notNullProperties.stream().map(prop -> ParameterSpec.builder(prop.getName(), asType(prop.getType())).build()).toList() - ).build() + } else { + propertySpec = buildConstructorProperty( + property.getName(), + property.getType(), + property.getModifiers(), + property.getAnnotations() ); } - for (FieldDef field : classDef.getFields()) { - Set modifiers = field.getModifiers(); - if (modifiers.contains(Modifier.STATIC)) { - if (companionBuilder == null) { - companionBuilder = TypeSpec.companionObjectBuilder(); - } - companionBuilder.addProperty( - buildProperty(field, stripStatic(modifiers)) - ); - } else { - classBuilder.addProperty( - buildProperty(field, modifiers) - ); + interfaceBuilder.addProperty( + propertySpec + ); + } + for (MethodDef method : interfaceDef.getMethods()) { + Set modifiers = method.getModifiers(); + if (modifiers.contains(Modifier.STATIC)) { + if (companionBuilder == null) { + companionBuilder = TypeSpec.companionObjectBuilder(); } + modifiers = stripStatic(modifiers); + companionBuilder.addFunction( + buildFunction(null, method, modifiers) + ); + } else { + interfaceBuilder.addFunction( + buildFunction(interfaceDef, method, modifiers) + ); } + } + if (companionBuilder != null) { + interfaceBuilder.addType(companionBuilder.build()); + } + FileSpec.builder(interfaceDef.getPackageName(), interfaceDef.getSimpleName() + ".kt") + .addType(interfaceBuilder.build()) + .build() + .writeTo(writer); + } - for (MethodDef method : classDef.getMethods()) { - Set modifiers = method.getModifiers(); - if (modifiers.contains(Modifier.STATIC)) { - if (companionBuilder == null) { - companionBuilder = TypeSpec.companionObjectBuilder(); - } - modifiers = stripStatic(modifiers); - companionBuilder.addFunction( - buildFunction(null, method, modifiers) - ); - } else { - classBuilder.addFunction( - buildFunction(classDef, method, modifiers) - ); - } - } - if (companionBuilder != null) { - classBuilder.addType(companionBuilder.build()); + private void writeClass(Writer writer, ClassDef classDef) throws IOException { + TypeSpec.Builder classBuilder = TypeSpec.classBuilder(classDef.getSimpleName()); + classBuilder.addModifiers(asKModifiers(classDef.getModifiers())); + classDef.getTypeVariables().stream().map(this::asTypeVariable).forEach(classBuilder::addTypeVariable); + classDef.getSuperinterfaces().stream().map(this::asType).forEach(it -> classBuilder.addSuperinterface(it, CodeBlock.Companion.getEMPTY$kotlinpoet())); + + TypeSpec.Builder companionBuilder = null; + List notNullProperties = new ArrayList<>(); + for (PropertyDef property : classDef.getProperties()) { + PropertySpec propertySpec; + if (property.getType().isNullable()) { + propertySpec = buildNullableProperty( + property.getName(), + property.getType().makeNullable(), + property.getModifiers(), + property.getAnnotations() + ); + } else { + propertySpec = buildConstructorProperty( + property.getName(), + property.getType(), + property.getModifiers(), + property.getAnnotations() + ); + notNullProperties.add(property); } - FileSpec.builder(classDef.getPackageName(), classDef.getSimpleName() + ".kt") - .addType(classBuilder.build()) - .build() - .writeTo(writer); - } else if (objectDef instanceof InterfaceDef interfaceDef) { - TypeSpec.Builder interfaceBuilder = TypeSpec.interfaceBuilder(interfaceDef.getSimpleName()); - interfaceBuilder.addModifiers(asKModifiers(interfaceDef.getModifiers())); - interfaceDef.getTypeVariables().stream().map(this::asTypeVariable).forEach(interfaceBuilder::addTypeVariable); - interfaceDef.getSuperinterfaces().stream().map(this::asType).forEach(it -> interfaceBuilder.addSuperinterface(it, CodeBlock.Companion.getEMPTY$kotlinpoet())); - - TypeSpec.Builder companionBuilder = null; - for (PropertyDef property : interfaceDef.getProperties()) { - PropertySpec propertySpec; - if (property.getType().isNullable()) { - propertySpec = buildNullableProperty( - property.getName(), - property.getType().makeNullable(), - property.getModifiers(), - property.getAnnotations() - ); - } else { - propertySpec = buildNotNullProperty( - property.getName(), - property.getType(), - property.getModifiers(), - property.getAnnotations() - ); + classBuilder.addProperty( + propertySpec + ); + } + if (!notNullProperties.isEmpty()) { + classBuilder.setPrimaryConstructor$kotlinpoet( + FunSpec.constructorBuilder().addModifiers(KModifier.PUBLIC).addParameters( + notNullProperties.stream().map(prop -> ParameterSpec.builder(prop.getName(), asType(prop.getType())).build()).toList() + ).build() + ); + } + for (FieldDef field : classDef.getFields()) { + Set modifiers = field.getModifiers(); + if (modifiers.contains(Modifier.STATIC)) { + if (companionBuilder == null) { + companionBuilder = TypeSpec.companionObjectBuilder(); } - interfaceBuilder.addProperty( - propertySpec + companionBuilder.addProperty( + buildNullableProperty(field, stripStatic(modifiers)) + ); + } else { + classBuilder.addProperty( + buildNullableProperty(field, modifiers) ); } - for (MethodDef method : interfaceDef.getMethods()) { - Set modifiers = method.getModifiers(); - if (modifiers.contains(Modifier.STATIC)) { - if (companionBuilder == null) { - companionBuilder = TypeSpec.companionObjectBuilder(); - } - modifiers = stripStatic(modifiers); - companionBuilder.addFunction( - buildFunction(null, method, modifiers) - ); - } else { - interfaceBuilder.addFunction( - buildFunction(interfaceDef, method, modifiers) - ); + } + + for (MethodDef method : classDef.getMethods()) { + Set modifiers = method.getModifiers(); + if (modifiers.contains(Modifier.STATIC)) { + if (companionBuilder == null) { + companionBuilder = TypeSpec.companionObjectBuilder(); } + modifiers = stripStatic(modifiers); + companionBuilder.addFunction( + buildFunction(null, method, modifiers) + ); + } else { + classBuilder.addFunction( + buildFunction(classDef, method, modifiers) + ); } - if (companionBuilder != null) { - interfaceBuilder.addType(companionBuilder.build()); + } + if (companionBuilder != null) { + classBuilder.addType(companionBuilder.build()); + } + FileSpec.builder(classDef.getPackageName(), classDef.getSimpleName() + ".kt") + .addType(classBuilder.build()) + .build() + .writeTo(writer); + } + + private void writeRecordDef(Writer writer, RecordDef recordDef) throws IOException { + TypeSpec.Builder classBuilder = TypeSpec.classBuilder(recordDef.getSimpleName()); + classBuilder.addModifiers(KModifier.DATA); + classBuilder.addModifiers(asKModifiers(recordDef.getModifiers())); + recordDef.getTypeVariables().stream().map(this::asTypeVariable).forEach(classBuilder::addTypeVariable); + recordDef.getSuperinterfaces().stream().map(this::asType).forEach(it -> classBuilder.addSuperinterface(it, CodeBlock.Companion.getEMPTY$kotlinpoet())); + + TypeSpec.Builder companionBuilder = null; + List constructorProperties = new ArrayList<>(); + for (PropertyDef property : recordDef.getProperties()) { + constructorProperties.add(property); + classBuilder.addProperty( + buildConstructorProperty( + property.getName(), + property.getType(), + extendModifiers(property.getModifiers(), Modifier.FINAL), + property.getAnnotations() + ) + ); + } + if (!constructorProperties.isEmpty()) { + classBuilder.setPrimaryConstructor$kotlinpoet( + FunSpec.constructorBuilder().addModifiers(KModifier.PUBLIC).addParameters( + constructorProperties.stream().map(prop -> ParameterSpec.builder(prop.getName(), asType(prop.getType())).build()).toList() + ).build() + ); + } + + for (MethodDef method : recordDef.getMethods()) { + Set modifiers = method.getModifiers(); + if (modifiers.contains(Modifier.STATIC)) { + if (companionBuilder == null) { + companionBuilder = TypeSpec.companionObjectBuilder(); + } + modifiers = stripStatic(modifiers); + companionBuilder.addFunction( + buildFunction(null, method, modifiers) + ); + } else { + classBuilder.addFunction( + buildFunction(recordDef, method, modifiers) + ); } - FileSpec.builder(interfaceDef.getPackageName(), interfaceDef.getSimpleName() + ".kt") - .addType(interfaceBuilder.build()) - .build() - .writeTo(writer); - } else { - throw new IllegalStateException("Unknown object definition: " + objectDef); } + if (companionBuilder != null) { + classBuilder.addType(companionBuilder.build()); + } + FileSpec.builder(recordDef.getPackageName(), recordDef.getSimpleName() + ".kt") + .addType(classBuilder.build()) + .build() + .writeTo(writer); } private PropertySpec buildNullableProperty(String name, @@ -230,10 +294,10 @@ private PropertySpec buildNullableProperty(String name, .initializer("null").build(); } - private PropertySpec buildNotNullProperty(String name, - TypeDef typeDef, - Set modifiers, - List annotations) { + private PropertySpec buildConstructorProperty(String name, + TypeDef typeDef, + Set modifiers, + List annotations) { PropertySpec.Builder propertyBuilder = PropertySpec.builder( name, asType(typeDef), @@ -252,7 +316,7 @@ private PropertySpec buildNotNullProperty(String name, .build(); } - private PropertySpec buildProperty(FieldDef field, Set modifiers) { + private PropertySpec buildNullableProperty(FieldDef field, Set modifiers) { return buildNullableProperty(field.getName(), field.getType(), modifiers, field.getAnnotations()); } @@ -262,6 +326,15 @@ private static Set stripStatic(Set modifiers) { return modifiers; } + private static Set extendModifiers(Set modifiers, Modifier modifier) { + if (modifiers.contains(modifier)) { + return modifiers; + } + modifiers = new HashSet<>(modifiers); + modifiers.add(modifier); + return modifiers; + } + private FunSpec buildFunction(ObjectDef objectDef, MethodDef method, Set modifiers) { FunSpec.Builder funBuilder = FunSpec.builder(method.getName()) .addModifiers(asKModifiers(modifiers)) diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElement.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElement.java index 401b11ac..11be1209 100644 --- a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElement.java +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElement.java @@ -29,7 +29,7 @@ * @since 1.0 */ @Experimental -abstract sealed class AbstractElement permits ClassDef, FieldDef, InterfaceDef, MethodDef, ParameterDef, PropertyDef { +abstract sealed class AbstractElement permits ClassDef, FieldDef, InterfaceDef, MethodDef, ParameterDef, PropertyDef, RecordDef { protected final String name; protected final Set modifiers; diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElementBuilder.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElementBuilder.java index 3d6f200f..f556c7dc 100644 --- a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElementBuilder.java +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/AbstractElementBuilder.java @@ -31,7 +31,7 @@ * @since 1.0 */ @Experimental -public sealed class AbstractElementBuilder permits ClassDef.ClassDefBuilder, FieldDef.FieldDefBuilder, InterfaceDef.InterfaceDefBuilder, MethodDef.MethodDefBuilder, ParameterDef.ParameterDefBuilder, PropertyDef.PropertyDefBuilder { +public sealed class AbstractElementBuilder permits ClassDef.ClassDefBuilder, FieldDef.FieldDefBuilder, InterfaceDef.InterfaceDefBuilder, MethodDef.MethodDefBuilder, ParameterDef.ParameterDefBuilder, PropertyDef.PropertyDefBuilder, RecordDef.RecordDefBuilder { protected final String name; protected EnumSet modifiers = EnumSet.noneOf(Modifier.class); diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/RecordDef.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/RecordDef.java new file mode 100644 index 00000000..8288ace8 --- /dev/null +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/RecordDef.java @@ -0,0 +1,117 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.model; + +import io.micronaut.core.annotation.Experimental; + +import javax.lang.model.element.Modifier; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** + * The class definition. + * + * @author Denis Stepanov + * @since 1.0 + */ +@Experimental +public final class RecordDef extends AbstractElement implements ObjectDef { + + private final List methods; + private final List properties; + private final List typeVariables; + private final List superinterfaces; + + private RecordDef(String name, + EnumSet modifiers, + List methods, + List properties, + List annotations, + List typeVariables, + List superinterfaces) { + super(name, modifiers, annotations); + this.methods = methods; + this.properties = properties; + this.typeVariables = typeVariables; + this.superinterfaces = superinterfaces; + } + + public static RecordDefBuilder builder(String name) { + return new RecordDefBuilder(name); + } + + public List getMethods() { + return methods; + } + + public List getProperties() { + return properties; + } + + public List getTypeVariables() { + return typeVariables; + } + + public List getSuperinterfaces() { + return superinterfaces; + } + + /** + * The record definition builder. + * + * @author Denis Stepanov + * @since 1.0 + */ + @Experimental + public static final class RecordDefBuilder extends AbstractElementBuilder { + + private final List methods = new ArrayList<>(); + private final List properties = new ArrayList<>(); + private final List typeVariables = new ArrayList<>(); + private final List superinterfaces = new ArrayList<>(); + + private RecordDefBuilder(String name) { + super(name); + } + + public RecordDefBuilder addMethod(MethodDef method) { + methods.add(method); + return this; + } + + public RecordDefBuilder addProperty(PropertyDef property) { + properties.add(property); + return this; + } + + public RecordDefBuilder addTypeVariable(TypeDef.TypeVariable typeVariable) { + typeVariables.add(typeVariable); + return this; + } + + public RecordDefBuilder addSuperinterface(TypeDef superinterface) { + superinterfaces.add(superinterface); + return this; + } + + public RecordDef build() { + return new RecordDef(name, modifiers, methods, properties, annotations, typeVariables, superinterfaces); + } + + } + +} diff --git a/test-suite-custom-annotations/src/main/java/io/micronaut/sourcegen/custom/example/GenerateMyRecord1.java b/test-suite-custom-annotations/src/main/java/io/micronaut/sourcegen/custom/example/GenerateMyRecord1.java new file mode 100644 index 00000000..6512f0ab --- /dev/null +++ b/test-suite-custom-annotations/src/main/java/io/micronaut/sourcegen/custom/example/GenerateMyRecord1.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.custom.example; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Retention(RUNTIME) +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +public @interface GenerateMyRecord1 { +} diff --git a/test-suite-custom-generators/src/main/java/io/micronaut/sourcegen/custom/visitor/GenerateMyRecord1Visitor.java b/test-suite-custom-generators/src/main/java/io/micronaut/sourcegen/custom/visitor/GenerateMyRecord1Visitor.java new file mode 100644 index 00000000..a5a2d93a --- /dev/null +++ b/test-suite-custom-generators/src/main/java/io/micronaut/sourcegen/custom/visitor/GenerateMyRecord1Visitor.java @@ -0,0 +1,130 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.custom.visitor; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.sourcegen.custom.example.GenerateMyBean1; +import io.micronaut.sourcegen.custom.example.GenerateMyRecord1; +import io.micronaut.sourcegen.generator.SourceGenerator; +import io.micronaut.sourcegen.generator.SourceGenerators; +import io.micronaut.sourcegen.model.AnnotationDef; +import io.micronaut.sourcegen.model.ClassDef; +import io.micronaut.sourcegen.model.ClassTypeDef; +import io.micronaut.sourcegen.model.PropertyDef; +import io.micronaut.sourcegen.model.RecordDef; +import io.micronaut.sourcegen.model.TypeDef; + +import javax.lang.model.element.Modifier; +import java.io.IOException; +import java.util.List; + +@Internal +public final class GenerateMyRecord1Visitor implements TypeElementVisitor { + + ClassElement thisElement; + + @Override + public @NonNull VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + thisElement = element; + } + + @Override + public void finish(VisitorContext visitorContext) { + if (thisElement != null) { + generate(thisElement, visitorContext); + thisElement = null; + } + } + + private void generate(ClassElement element, VisitorContext context) { + String builderClassName = element.getPackageName() + ".MyRecord1"; + + RecordDef beanDef = RecordDef.builder(builderClassName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + + .addProperty( + PropertyDef.builder("id") + .addModifiers(Modifier.PUBLIC) + .ofType(TypeDef.primitive(int.class)) + .addAnnotation(Deprecated.class) + .build() + ) + + .addProperty( + PropertyDef.builder("name") + .addModifiers(Modifier.PUBLIC) + .ofType(TypeDef.of(String.class)) + .build() + ) + + .addProperty( + PropertyDef.builder("age") + .addModifiers(Modifier.PUBLIC) + .ofType(TypeDef.of(Integer.class)) + .addAnnotation(AnnotationDef.builder(ClassTypeDef.of(Deprecated.class)) + .addMember("since", "xyz") + .addMember("forRemoval", true) + .build()) + .build() + ) + + .addProperty( + PropertyDef.builder("addresses") + .addModifiers(Modifier.PUBLIC) + .ofType(new ClassTypeDef.Parameterized( + ClassTypeDef.of(List.class), + List.of(TypeDef.of(String.class)) + )) + .build() + ) + + .addProperty( + PropertyDef.builder("tags") + .addModifiers(Modifier.PUBLIC) + .ofType(new ClassTypeDef.Parameterized( + ClassTypeDef.of(List.class), + List.of( + TypeDef.wildcard() + ) + )) + .build() + ) + .build(); + + SourceGenerator sourceGenerator = SourceGenerators.findByLanguage(context.getLanguage()).orElse(null); + if (sourceGenerator == null) { + return; + } + context.visitGeneratedSourceFile(beanDef.getPackageName(), beanDef.getSimpleName(), thisElement) + .ifPresent(generatedFile -> { + try { + generatedFile.write(writer -> sourceGenerator.write(beanDef, writer)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + +} diff --git a/test-suite-custom-generators/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/test-suite-custom-generators/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 8576b46d..18df2139 100644 --- a/test-suite-custom-generators/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/test-suite-custom-generators/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -2,3 +2,4 @@ io.micronaut.sourcegen.custom.visitor.GenerateMyBean1Visitor io.micronaut.sourcegen.custom.visitor.GenerateMyBean2Visitor io.micronaut.sourcegen.custom.visitor.GenerateMyInterface1Visitor io.micronaut.sourcegen.custom.visitor.GenerateMyRepository1Visitor +io.micronaut.sourcegen.custom.visitor.GenerateMyRecord1Visitor diff --git a/test-suite-java/src/main/java/io/micronaut/sourcegen/example/Trigger.java b/test-suite-java/src/main/java/io/micronaut/sourcegen/example/Trigger.java index 160f8401..fdb4a176 100644 --- a/test-suite-java/src/main/java/io/micronaut/sourcegen/example/Trigger.java +++ b/test-suite-java/src/main/java/io/micronaut/sourcegen/example/Trigger.java @@ -19,10 +19,12 @@ import io.micronaut.sourcegen.custom.example.GenerateMyBean1; import io.micronaut.sourcegen.custom.example.GenerateMyBean2; import io.micronaut.sourcegen.custom.example.GenerateMyInterface1; +import io.micronaut.sourcegen.custom.example.GenerateMyRecord1; import io.micronaut.sourcegen.custom.example.GenerateMyRepository1; @GenerateMyBean1 @GenerateMyBean2 +@GenerateMyRecord1 @GenerateMyInterface1 @GenerateMyRepository1 public class Trigger { diff --git a/test-suite-java/src/test/java/io/micronaut/sourcegen/example/MyRecord1Test.java b/test-suite-java/src/test/java/io/micronaut/sourcegen/example/MyRecord1Test.java new file mode 100644 index 00000000..c5512482 --- /dev/null +++ b/test-suite-java/src/test/java/io/micronaut/sourcegen/example/MyRecord1Test.java @@ -0,0 +1,50 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.sourcegen.example; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Modifier; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MyRecord1Test { + @Test + public void test() throws Exception { + MyRecord1 bean = new MyRecord1(123, "TheName", 55, List.of("Address 1"), List.of("X", "Y")); + + assertEquals("TheName", bean.name()); + assertEquals(123, bean.id()); + assertEquals(55, bean.age()); + assertEquals(List.of("Address 1"), bean.addresses()); + assertEquals(List.of("X", "Y"), bean.tags()); + + assertTrue(Modifier.isPrivate( + bean.getClass().getDeclaredField("id").getModifiers() + )); + assertTrue( + bean.getClass().getDeclaredField("id").getDeclaredAnnotations()[0] instanceof Deprecated + ); + assertTrue(Modifier.isPublic( + bean.getClass().getDeclaredMethod("id").getModifiers() + )); + Deprecated deprecated = (Deprecated) bean.getClass().getDeclaredField("age").getDeclaredAnnotations()[0]; + assertEquals(deprecated.since(), "xyz"); + assertTrue(deprecated.forRemoval()); + } +} diff --git a/test-suite-kotlin/src/main/kotlin/io/micronaut/sourcegen/example/Trigger.kt b/test-suite-kotlin/src/main/kotlin/io/micronaut/sourcegen/example/Trigger.kt index d3564463..03085680 100644 --- a/test-suite-kotlin/src/main/kotlin/io/micronaut/sourcegen/example/Trigger.kt +++ b/test-suite-kotlin/src/main/kotlin/io/micronaut/sourcegen/example/Trigger.kt @@ -18,10 +18,12 @@ package io.micronaut.sourcegen.example import io.micronaut.sourcegen.custom.example.GenerateMyBean1 import io.micronaut.sourcegen.custom.example.GenerateMyBean2 import io.micronaut.sourcegen.custom.example.GenerateMyInterface1 +import io.micronaut.sourcegen.custom.example.GenerateMyRecord1 import io.micronaut.sourcegen.custom.example.GenerateMyRepository1 @GenerateMyBean1 @GenerateMyBean2 @GenerateMyInterface1 @GenerateMyRepository1 +@GenerateMyRecord1 class Trigger diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/sourcegen/example/MyRecord1Test.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/sourcegen/example/MyRecord1Test.kt new file mode 100644 index 00000000..d1c52d18 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/sourcegen/example/MyRecord1Test.kt @@ -0,0 +1,40 @@ +package io.micronaut.sourcegen.example + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.lang.Deprecated +import java.lang.reflect.Modifier + +class MyRecord1Test { + + @Test + @Throws(Exception::class) + fun test() { + val bean = MyRecord1(123, "TheName", 55, listOf("Address 1"), listOf("X", "Y")) + + assertEquals("TheName", bean.name) + assertEquals(123, bean.id) + assertEquals(55, bean.age) + assertEquals(listOf("Address 1"), bean.addresses) + assertEquals(listOf("X", "Y"), bean.tags) + + Assertions.assertTrue( + Modifier.isPrivate( + bean.javaClass.getDeclaredField("id").modifiers + ) + ) + Assertions.assertTrue( + bean.javaClass.constructors[0].parameters[0].declaredAnnotations[0] is Deprecated + ) + Assertions.assertTrue( + Modifier.isPublic( + bean.javaClass.getDeclaredMethod("getId").modifiers + ) + ) + val deprecated = bean.javaClass.constructors[0].parameters[2].declaredAnnotations[0] as Deprecated + assertEquals(deprecated.since, "xyz") + Assertions.assertTrue(deprecated.forRemoval) + } +} +