Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] @JsonUnboxed annotation #86

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ public RuntimeException compilationException() {
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
.map(Object::toString)
.collect(Collectors.joining("\n"));
throw new RuntimeException("CompilationError: \n" + errors.indent(2) + "\n" + j.toString().indent(2));
return new RuntimeException("CompilationError: \n" + errors.indent(2) + "\n" + j.toString().indent(2));
} catch (IOException e) {
throw new RuntimeException(e);
return new RuntimeException(e);
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package ru.tinkoff.kora.json.annotation.processor;

import com.squareup.javapoet.JavaFile;
import javax.lang.model.element.AnnotationMirror;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ru.tinkoff.kora.annotation.processor.common.AnnotationUtils;
import ru.tinkoff.kora.annotation.processor.common.CommonUtils;
import ru.tinkoff.kora.annotation.processor.common.ComparableTypeMirror;
import ru.tinkoff.kora.annotation.processor.common.ProcessingErrorException;
import ru.tinkoff.kora.annotation.processor.common.SealedTypeUtils;
import ru.tinkoff.kora.json.annotation.processor.reader.EnumReaderGenerator;
import ru.tinkoff.kora.json.annotation.processor.reader.JsonReaderGenerator;
import ru.tinkoff.kora.json.annotation.processor.reader.UnboxedReaderGenerator;
import ru.tinkoff.kora.json.annotation.processor.reader.ReaderTypeMetaParser;
import ru.tinkoff.kora.json.annotation.processor.reader.SealedInterfaceReaderGenerator;
import ru.tinkoff.kora.json.annotation.processor.writer.EnumWriterGenerator;
import ru.tinkoff.kora.json.annotation.processor.writer.JsonWriterGenerator;
import ru.tinkoff.kora.json.annotation.processor.writer.SealedInterfaceWriterGenerator;
import ru.tinkoff.kora.json.annotation.processor.writer.UnboxedWriterGenerator;
import ru.tinkoff.kora.json.annotation.processor.writer.WriterTypeMetaParser;

import javax.annotation.processing.ProcessingEnvironment;
Expand All @@ -38,6 +43,8 @@ public class JsonProcessor {
private final SealedInterfaceWriterGenerator sealedWriterGenerator;
private final EnumReaderGenerator enumReaderGenerator;
private final EnumWriterGenerator enumWriterGenerator;
private final UnboxedReaderGenerator unboxedReaderGenerator;
private final UnboxedWriterGenerator unboxedWriterGenerator;

public JsonProcessor(ProcessingEnvironment processingEnv) {
this.processingEnv = processingEnv;
Expand All @@ -52,6 +59,8 @@ public JsonProcessor(ProcessingEnvironment processingEnv) {
this.sealedWriterGenerator = new SealedInterfaceWriterGenerator(this.processingEnv);
this.enumReaderGenerator = new EnumReaderGenerator();
this.enumWriterGenerator = new EnumWriterGenerator();
this.unboxedReaderGenerator = new UnboxedReaderGenerator();
this.unboxedWriterGenerator = new UnboxedWriterGenerator();
}

public void generateReader(TypeElement jsonElement) {
Expand All @@ -62,6 +71,13 @@ public void generateReader(TypeElement jsonElement) {
if (readerElement != null) {
return;
}

var unboxedAnnotation = AnnotationUtils.findAnnotation(jsonElement, JsonTypes.jsonUnboxed);
if (unboxedAnnotation != null) {
this.generateUnboxedReader(jsonElement, jsonElementType, unboxedAnnotation);
return;
}

if (jsonElement.getKind() == ElementKind.ENUM) {
this.generateEnumReader(jsonElement);
return;
Expand Down Expand Up @@ -98,6 +114,22 @@ private void generateDtoReader(TypeElement typeElement, TypeMirror jsonTypeMirro
CommonUtils.safeWriteTo(this.processingEnv, javaFile);
}

private void generateUnboxedReader(
TypeElement typeElement,
TypeMirror jsonTypeMirror,
AnnotationMirror unboxedAnnotation
) {
checkUnboxedTarget(typeElement, unboxedAnnotation);

var packageElement = JsonUtils.jsonClassPackage(this.elements, typeElement);
var meta = Objects.requireNonNull(this.readerTypeMetaParser.parseUnboxed(typeElement, jsonTypeMirror));
var readerType = Objects.requireNonNull(this.unboxedReaderGenerator.generateForUnboxed(meta));

var javaFile = JavaFile.builder(packageElement, readerType).build();

CommonUtils.safeWriteTo(this.processingEnv, javaFile);
}

private void generateEnumWriter(TypeElement jsonElement) {
var packageElement = JsonUtils.jsonClassPackage(this.elements, jsonElement);
var enumWriterType = this.enumWriterGenerator.generateEnumWriter(jsonElement);
Expand All @@ -113,6 +145,11 @@ public void generateWriter(TypeElement jsonElement) {
if (writerElement != null) {
return;
}
var unboxedAnnotation = AnnotationUtils.findAnnotation(jsonElement, JsonTypes.jsonUnboxed);
if (unboxedAnnotation != null) {
this.generateUnboxedWriter(jsonElement, jsonElement.asType(), unboxedAnnotation);
return;
}
if (jsonElement.getKind() == ElementKind.ENUM) {
this.generateEnumWriter(jsonElement);
return;
Expand All @@ -133,6 +170,22 @@ private void generateSealedWriter(TypeElement jsonElement) {
CommonUtils.safeWriteTo(this.processingEnv, javaFile);
}

private void generateUnboxedWriter(
TypeElement typeElement,
TypeMirror jsonTypeMirror,
AnnotationMirror unboxedAnnotation
) {
checkUnboxedTarget(typeElement, unboxedAnnotation);

var meta = Objects.requireNonNull(this.writerTypeMetaParser.parseUnboxed(typeElement, jsonTypeMirror));

var writerType = Objects.requireNonNull(this.unboxedWriterGenerator.generate(meta));
var packageElement = JsonUtils.jsonClassPackage(this.elements, typeElement);
var javaFile = JavaFile.builder(packageElement, writerType).build();

CommonUtils.safeWriteTo(this.processingEnv, javaFile);
}

private void tryGenerateWriter(TypeElement jsonElement, TypeMirror jsonTypeMirror) {
var meta = Objects.requireNonNull(this.writerTypeMetaParser.parse(jsonElement, jsonTypeMirror));
var packageElement = JsonUtils.jsonClassPackage(this.elements, jsonElement);
Expand All @@ -141,4 +194,16 @@ private void tryGenerateWriter(TypeElement jsonElement, TypeMirror jsonTypeMirro
var javaFile = JavaFile.builder(packageElement, writerType).build();
CommonUtils.safeWriteTo(this.processingEnv, javaFile);
}

private void checkUnboxedTarget(TypeElement typeElement, AnnotationMirror unboxedAnnotation) {
var kind = typeElement.getKind();

if (kind != ElementKind.CLASS && kind != ElementKind.RECORD) {
throw new ProcessingErrorException(
"@JsonUnboxed supported only for classes and records",
typeElement,
unboxedAnnotation
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class JsonTypes {
public static final ClassName jsonInclude = ClassName.get("ru.tinkoff.kora.json.common.annotation", "JsonInclude");
public static final ClassName jsonDiscriminatorField = ClassName.get("ru.tinkoff.kora.json.common.annotation", "JsonDiscriminatorField");
public static final ClassName jsonDiscriminatorValue = ClassName.get("ru.tinkoff.kora.json.common.annotation", "JsonDiscriminatorValue");
public static final ClassName jsonUnboxed = ClassName.get("ru.tinkoff.kora.json.common.annotation", "JsonUnboxed");


public static final ClassName jsonReaderAnnotation = ClassName.get("ru.tinkoff.kora.json.common.annotation", "JsonReader");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ private TypeSpec generateForClass(JsonClassReaderMeta meta) {
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addException(IOException.class)
.addParameter(JsonTypes.jsonParser, "_parser")
.returns(TypeName.get(meta.typeElement().asType()))
.returns(TypeName.get(meta.typeMirror()))
.addAnnotation(Override.class)
.addAnnotation(Nullable.class);
method.addStatement("var _token = _parser.currentToken()");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,30 @@ public JsonClassReaderMeta parse(TypeElement jsonClass, TypeMirror typeMirror) t
return new JsonClassReaderMeta(typeMirror, jsonClass, fields);
}

public UnboxedReaderMeta parseUnboxed(TypeElement jsonClass, TypeMirror typeMirror) throws ProcessingErrorException {
if (jsonClass.getKind() != ElementKind.CLASS && jsonClass.getKind() != ElementKind.RECORD) {
throw new IllegalArgumentException("Should not be called for non class elements");
}
if (jsonClass.getModifiers().contains(Modifier.ABSTRACT)) {
throw new IllegalArgumentException("Should not be called for abstract elements");
}

var jsonConstructor = Objects.requireNonNull(this.findJsonConstructor(jsonClass));

if (jsonConstructor.getParameters().size() != 1) {
throw new ProcessingErrorException(
"@JsonUnboxed JsonReader can be created only for constructors with single parameter",
jsonConstructor
);
}

VariableElement parameter = jsonConstructor.getParameters().get(0);

var fieldMeta = new UnboxedReaderMeta.FieldMeta(parameter, TypeName.get(parameter.asType()));

return new UnboxedReaderMeta(typeMirror, jsonClass, fieldMeta);
}

@Nullable
public ReaderFieldType parseReaderFieldType(TypeMirror jsonClass) {
var knownType = this.knownTypes.detect(jsonClass);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package ru.tinkoff.kora.json.annotation.processor.reader;

import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import jakarta.annotation.Nullable;
import java.io.IOException;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeParameterElement;
import ru.tinkoff.kora.annotation.processor.common.CommonClassNames;
import ru.tinkoff.kora.annotation.processor.common.CommonUtils;
import ru.tinkoff.kora.json.annotation.processor.JsonTypes;
import ru.tinkoff.kora.json.annotation.processor.JsonUtils;

public class UnboxedReaderGenerator {

public TypeSpec generateForUnboxed(UnboxedReaderMeta meta) {

var typeBuilder = TypeSpec.classBuilder(JsonUtils.jsonReaderName(meta.typeElement()))
.addAnnotation(AnnotationSpec.builder(CommonClassNames.koraGenerated)
.addMember("value", CodeBlock.of("$S", UnboxedReaderGenerator.class.getCanonicalName()))
.build())
.addSuperinterface(ParameterizedTypeName.get(JsonTypes.jsonReader, TypeName.get(meta.typeMirror())))
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addOriginatingElement(meta.typeElement());

for (TypeParameterElement typeParameter : meta.typeElement().getTypeParameters()) {
typeBuilder.addTypeVariable(TypeVariableName.get(typeParameter));
}

var field = meta.field();

var fieldName = this.readerFieldName(field);
var fieldType = ParameterizedTypeName.get(JsonTypes.jsonReader, field.typeName());
var readerField = FieldSpec.builder(fieldType, fieldName, Modifier.PRIVATE, Modifier.FINAL).build();
var readerParameter = ParameterSpec.builder(fieldType, fieldName).build();

var constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(readerParameter)
.addStatement("this.$N = $N", readerField, readerParameter);

typeBuilder.addField(readerField);
typeBuilder.addMethod(constructor.build());

var method = MethodSpec.methodBuilder("read")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addException(IOException.class)
.addParameter(JsonTypes.jsonParser, "_parser")
.returns(TypeName.get(meta.typeMirror()))
.addAnnotation(Override.class)
.addAnnotation(Nullable.class);

method.addStatement("var _token = _parser.currentToken()");
method.beginControlFlow("if (_token == $T.VALUE_NULL)", JsonTypes.jsonToken);

if (isNullable(field)) {
method.addStatement("return new $T(null)", meta.typeElement());
} else {
method.addStatement(
"throw new $T(_parser, $S)",
JsonTypes.jsonParseException,
"Expecting nonnull value, got VALUE_NULL token"
);
}

method.endControlFlow();

method.addStatement("return new $T($N.read(_parser))", meta.typeElement(), readerField);

typeBuilder.addMethod(method.build());

return typeBuilder.build();
}

private String readerFieldName(UnboxedReaderMeta.FieldMeta field) {
return field.parameter().getSimpleName() + "Reader";
}

private boolean isNullable(UnboxedReaderMeta.FieldMeta field) {
if (field.parameter().asType().getKind().isPrimitive()) {
return false;
}

return CommonUtils.isNullable(field.parameter());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ru.tinkoff.kora.json.annotation.processor.reader;

import com.squareup.javapoet.TypeName;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;

public record UnboxedReaderMeta(TypeMirror typeMirror, TypeElement typeElement, FieldMeta field) {
public record FieldMeta(VariableElement parameter, TypeName typeName) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package ru.tinkoff.kora.json.annotation.processor.writer;

import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import jakarta.annotation.Nullable;
import java.io.IOException;
import javax.lang.model.element.Modifier;
import ru.tinkoff.kora.annotation.processor.common.CommonClassNames;
import ru.tinkoff.kora.json.annotation.processor.JsonTypes;
import ru.tinkoff.kora.json.annotation.processor.JsonUtils;

public class UnboxedWriterGenerator {

@Nullable
public TypeSpec generate(UnboxedWriterMeta meta) {
var typeBuilder = TypeSpec.classBuilder(JsonUtils.jsonWriterName(meta.typeElement()))
.addAnnotation(AnnotationSpec.builder(CommonClassNames.koraGenerated)
.addMember("value", CodeBlock.of("$S", UnboxedWriterGenerator.class.getCanonicalName()))
.build())
.addSuperinterface(ParameterizedTypeName.get(JsonTypes.jsonWriter, TypeName.get(meta.typeMirror())))
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addOriginatingElement(meta.typeElement());

for (var typeParameter : meta.typeElement().getTypeParameters()) {
typeBuilder.addTypeVariable(TypeVariableName.get(typeParameter));
}

var field = meta.field();

var fieldName = this.writerFieldName(field);
var fieldType = ParameterizedTypeName.get(JsonTypes.jsonWriter, TypeName.get(field.typeMirror()));

var writerField = FieldSpec.builder(fieldType, fieldName, Modifier.PRIVATE, Modifier.FINAL).build();
var writerParameter = ParameterSpec.builder(fieldType, fieldName).build();

var constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(writerParameter)
.addStatement("this.$N = $N", writerField, writerParameter);

typeBuilder.addField(writerField);
typeBuilder.addMethod(constructor.build());

var method = MethodSpec.methodBuilder("write")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addException(IOException.class)
.addParameter(JsonTypes.jsonGenerator, "_gen")
.addParameter(ParameterSpec.builder(TypeName.get(meta.typeMirror()), "_object").addAnnotation(Nullable.class).build())
.addAnnotation(Override.class)
.addCode("if (_object == null) {$>\n_gen.writeNull();\nreturn;$<\n}\n");

method.addStatement("$N.write(_gen, _object.$L)", writerField, field.accessor());

typeBuilder.addMethod(method.build());
return typeBuilder.build();
}

private String writerFieldName(UnboxedWriterMeta.FieldMeta field) {
return field.accessor().getSimpleName() + "Writer";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ru.tinkoff.kora.json.annotation.processor.writer;

import com.squareup.javapoet.TypeName;
import jakarta.annotation.Nullable;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;

public record UnboxedWriterMeta(TypeMirror typeMirror, TypeElement typeElement, FieldMeta field) {
public record FieldMeta(
VariableElement field,
TypeMirror typeMirror,
ExecutableElement accessor
) {}
}
Loading
Loading