Skip to content

Commit d732193

Browse files
authored
Json writer and reader for enum with generic value (#221)
1 parent f11639e commit d732193

File tree

22 files changed

+409
-141
lines changed

22 files changed

+409
-141
lines changed

json/json-annotation-processor/src/main/java/ru/tinkoff/kora/json/annotation/processor/reader/EnumReaderGenerator.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package ru.tinkoff.kora.json.annotation.processor.reader;
22

33
import com.squareup.javapoet.*;
4+
import ru.tinkoff.kora.annotation.processor.common.AnnotationUtils;
45
import ru.tinkoff.kora.annotation.processor.common.CommonClassNames;
56
import ru.tinkoff.kora.json.annotation.processor.JsonTypes;
67
import ru.tinkoff.kora.json.annotation.processor.JsonUtils;
78

89
import javax.annotation.Nullable;
10+
import javax.lang.model.element.ElementKind;
11+
import javax.lang.model.element.ExecutableElement;
912
import javax.lang.model.element.Modifier;
1013
import javax.lang.model.element.TypeElement;
1114
import java.io.IOException;
@@ -14,6 +17,7 @@ public class EnumReaderGenerator {
1417

1518
public TypeSpec generateForEnum(TypeElement typeElement) {
1619
var typeName = ClassName.get(typeElement);
20+
var enumValue = this.detectValueType(typeElement);
1721

1822
var typeBuilder = TypeSpec.classBuilder(JsonUtils.jsonReaderName(typeElement))
1923
.addAnnotation(AnnotationSpec.builder(CommonClassNames.koraGenerated)
@@ -22,13 +26,13 @@ public TypeSpec generateForEnum(TypeElement typeElement) {
2226
.addSuperinterface(ParameterizedTypeName.get(JsonTypes.jsonReader, typeName))
2327
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
2428
.addOriginatingElement(typeElement);
25-
var delegateType = ParameterizedTypeName.get(JsonTypes.enumJsonReader, typeName);
29+
var delegateType = ParameterizedTypeName.get(JsonTypes.enumJsonReader, typeName, enumValue.type.box());
2630

2731
typeBuilder.addField(delegateType, "delegate", Modifier.PRIVATE, Modifier.FINAL);
2832
typeBuilder.addMethod(MethodSpec.constructorBuilder()
2933
.addModifiers(Modifier.PUBLIC)
30-
// todo detect string representation method for enum
31-
.addCode("this.delegate = new $T<>($T.values(), v -> v.toString());\n", JsonTypes.enumJsonReader, typeName)
34+
.addParameter(ParameterizedTypeName.get(JsonTypes.jsonReader, enumValue.type.box()), "valueReader")
35+
.addCode("this.delegate = new $T<>($T.values(), $T::$N, valueReader);\n", JsonTypes.enumJsonReader, typeName, typeName, enumValue.accessor)
3236
.build());
3337
typeBuilder.addMethod(MethodSpec.methodBuilder("read")
3438
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
@@ -42,4 +46,22 @@ public TypeSpec generateForEnum(TypeElement typeElement) {
4246
);
4347
return typeBuilder.build();
4448
}
49+
50+
record EnumValue(TypeName type, String accessor) {}
51+
52+
private EnumValue detectValueType(TypeElement typeElement) {
53+
for (var enclosedElement : typeElement.getEnclosedElements()) {
54+
if (!enclosedElement.getModifiers().contains(Modifier.PUBLIC)) continue;
55+
if (enclosedElement.getModifiers().contains(Modifier.STATIC)) continue;
56+
if (enclosedElement.getKind() != ElementKind.METHOD) continue;
57+
if (enclosedElement instanceof ExecutableElement executableElement && executableElement.getParameters().isEmpty()) {
58+
if (AnnotationUtils.isAnnotationPresent(executableElement, JsonTypes.json)) {
59+
var typeName = TypeName.get(executableElement.getReturnType());
60+
return new EnumValue(typeName, executableElement.getSimpleName().toString());
61+
}
62+
}
63+
}
64+
var typeName = ClassName.get(String.class);
65+
return new EnumValue(typeName, "toString");
66+
}
4567
}

json/json-annotation-processor/src/main/java/ru/tinkoff/kora/json/annotation/processor/writer/EnumWriterGenerator.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package ru.tinkoff.kora.json.annotation.processor.writer;
22

33
import com.squareup.javapoet.*;
4+
import ru.tinkoff.kora.annotation.processor.common.AnnotationUtils;
45
import ru.tinkoff.kora.annotation.processor.common.CommonClassNames;
56
import ru.tinkoff.kora.json.annotation.processor.JsonTypes;
67
import ru.tinkoff.kora.json.annotation.processor.JsonUtils;
78

89
import javax.annotation.Nullable;
10+
import javax.lang.model.element.ElementKind;
11+
import javax.lang.model.element.ExecutableElement;
912
import javax.lang.model.element.Modifier;
1013
import javax.lang.model.element.TypeElement;
1114
import java.io.IOException;
@@ -21,12 +24,14 @@ public TypeSpec generateEnumWriter(TypeElement typeElement) {
2124
.addSuperinterface(ParameterizedTypeName.get(JsonTypes.jsonWriter, typeName))
2225
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
2326
.addOriginatingElement(typeElement);
24-
var delegateType = ParameterizedTypeName.get(JsonTypes.enumJsonWriter, typeName);
27+
var enumValue = this.detectValueType(typeElement);
28+
var delegateType = ParameterizedTypeName.get(JsonTypes.enumJsonWriter, typeName, enumValue.type.box());
2529

2630
typeBuilder.addField(delegateType, "delegate", Modifier.PRIVATE, Modifier.FINAL);
2731
typeBuilder.addMethod(MethodSpec.constructorBuilder()
2832
.addModifiers(Modifier.PUBLIC)
29-
.addCode("this.delegate = new $T<>($T.values(), v -> v.toString());\n", JsonTypes.enumJsonWriter, typeName)
33+
.addParameter(ParameterizedTypeName.get(JsonTypes.jsonWriter, enumValue.type.box()), "valueWriter")
34+
.addCode("this.delegate = new $T<>($T.values(), $T::$N, valueWriter);\n", JsonTypes.enumJsonWriter, typeName, typeName, enumValue.accessor)
3035
.build());
3136
typeBuilder.addMethod(MethodSpec.methodBuilder("write")
3237
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
@@ -39,4 +44,22 @@ public TypeSpec generateEnumWriter(TypeElement typeElement) {
3944
);
4045
return typeBuilder.build();
4146
}
47+
48+
record EnumValue(TypeName type, String accessor) {}
49+
50+
private EnumValue detectValueType(TypeElement typeElement) {
51+
for (var enclosedElement : typeElement.getEnclosedElements()) {
52+
if (!enclosedElement.getModifiers().contains(Modifier.PUBLIC)) continue;
53+
if (enclosedElement.getModifiers().contains(Modifier.STATIC)) continue;
54+
if (enclosedElement.getKind() != ElementKind.METHOD) continue;
55+
if (enclosedElement instanceof ExecutableElement executableElement && executableElement.getParameters().isEmpty()) {
56+
if (AnnotationUtils.isAnnotationPresent(executableElement, JsonTypes.json)) {
57+
var typeName = TypeName.get(executableElement.getReturnType());
58+
return new EnumValue(typeName, executableElement.getSimpleName().toString());
59+
}
60+
}
61+
}
62+
var typeName = ClassName.get(String.class);
63+
return new EnumValue(typeName, "toString");
64+
}
4265
}

json/json-annotation-processor/src/test/java/ru/tinkoff/kora/json/annotation/processor/EnumTest.java

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
package ru.tinkoff.kora.json.annotation.processor;
22

3+
import com.fasterxml.jackson.core.JsonGenerator;
4+
import com.fasterxml.jackson.core.JsonParser;
35
import org.junit.jupiter.api.Test;
6+
import ru.tinkoff.kora.json.common.JsonReader;
7+
import ru.tinkoff.kora.json.common.JsonWriter;
48
import ru.tinkoff.kora.kora.app.annotation.processor.KoraAppProcessor;
59

610
import java.util.List;
711

812
import static org.assertj.core.api.Assertions.assertThat;
913

1014
public class EnumTest extends AbstractJsonAnnotationProcessorTest {
15+
JsonReader<String> stringReader = JsonParser::getValueAsString;
16+
JsonWriter<String> stringWriter = JsonGenerator::writeString;
17+
1118
@Test
1219
public void testEnum() {
1320
compile("""
@@ -19,11 +26,34 @@ public enum TestEnum {
1926

2027
compileResult.assertSuccess();
2128

22-
var mapper = mapper("TestEnum");
29+
var mapper = mapper("TestEnum", List.of(stringReader), List.of(stringWriter));
2330
mapper.verify(enumConstant("TestEnum", "VALUE1"), "\"VALUE1\"");
2431
mapper.verify(enumConstant("TestEnum", "VALUE2"), "\"VALUE2\"");
2532
}
2633

34+
@Test
35+
public void testEnumWithCustomJsonValue() {
36+
compile("""
37+
@Json
38+
public enum TestEnum {
39+
VALUE1, VALUE2;
40+
41+
@Json
42+
public int intValue() {
43+
return ordinal();
44+
}
45+
}
46+
""");
47+
48+
compileResult.assertSuccess();
49+
JsonReader<Integer> intReader = JsonParser::getIntValue;
50+
JsonWriter<Integer> intWriter = JsonGenerator::writeNumber;
51+
52+
var mapper = mapper("TestEnum", List.of(intReader), List.of(intWriter));
53+
mapper.verify(enumConstant("TestEnum", "VALUE1"), "0");
54+
mapper.verify(enumConstant("TestEnum", "VALUE2"), "1");
55+
}
56+
2757

2858
@Test
2959
public void testReaderFromExtension() {
@@ -34,13 +64,16 @@ enum TestEnum {
3464
VALUE1, VALUE2
3565
}
3666
67+
default ru.tinkoff.kora.json.common.JsonReader<String> stringReader() { return com.fasterxml.jackson.core.JsonParser::getValueAsString; }
68+
default ru.tinkoff.kora.json.common.JsonWriter<String> stringWriter() { return com.fasterxml.jackson.core.JsonGenerator::writeString; }
69+
3770
@Root
3871
default String root(ru.tinkoff.kora.json.common.JsonReader<TestEnum> r) {return "";}
3972
}
4073
""");
4174

4275
compileResult.assertSuccess();
43-
assertThat(reader("TestApp_TestEnum")).isNotNull();
76+
assertThat(reader("TestApp_TestEnum", stringReader)).isNotNull();
4477
}
4578

4679
@Test
@@ -51,14 +84,17 @@ public interface TestApp {
5184
enum TestEnum {
5285
VALUE1, VALUE2
5386
}
54-
87+
88+
default ru.tinkoff.kora.json.common.JsonReader<String> stringReader() { return com.fasterxml.jackson.core.JsonParser::getValueAsString; }
89+
default ru.tinkoff.kora.json.common.JsonWriter<String> stringWriter() { return com.fasterxml.jackson.core.JsonGenerator::writeString; }
90+
5591
@Root
5692
default String root(ru.tinkoff.kora.json.common.JsonWriter<TestEnum> r) {return "";}
5793
}
5894
""");
5995

6096
compileResult.assertSuccess();
61-
assertThat(writer("TestApp_TestEnum")).isNotNull();
97+
assertThat(writer("TestApp_TestEnum", stringWriter)).isNotNull();
6298
}
6399

64100
@Test
@@ -70,14 +106,17 @@ public interface TestApp {
70106
enum TestEnum {
71107
VALUE1, VALUE2
72108
}
73-
109+
110+
default ru.tinkoff.kora.json.common.JsonReader<String> stringReader() { return com.fasterxml.jackson.core.JsonParser::getValueAsString; }
111+
default ru.tinkoff.kora.json.common.JsonWriter<String> stringWriter() { return com.fasterxml.jackson.core.JsonGenerator::writeString; }
112+
74113
@Root
75114
default String root(ru.tinkoff.kora.json.common.JsonReader<TestEnum> r) {return "";}
76115
}
77116
""");
78117

79118
compileResult.assertSuccess();
80-
assertThat(reader("TestApp_TestEnum")).isNotNull();
119+
assertThat(reader("TestApp_TestEnum", stringReader)).isNotNull();
81120
}
82121

83122
@Test
@@ -89,13 +128,16 @@ public interface TestApp {
89128
enum TestEnum {
90129
VALUE1, VALUE2
91130
}
92-
131+
132+
default ru.tinkoff.kora.json.common.JsonReader<String> stringReader() { return com.fasterxml.jackson.core.JsonParser::getValueAsString; }
133+
default ru.tinkoff.kora.json.common.JsonWriter<String> stringWriter() { return com.fasterxml.jackson.core.JsonGenerator::writeString; }
134+
93135
@Root
94136
default String root(ru.tinkoff.kora.json.common.JsonWriter<TestEnum> r) {return "";}
95137
}
96138
""");
97139

98140
compileResult.assertSuccess();
99-
assertThat(writer("TestApp_TestEnum")).isNotNull();
141+
assertThat(writer("TestApp_TestEnum", stringWriter)).isNotNull();
100142
}
101143
}

json/json-annotation-processor/src/test/java/ru/tinkoff/kora/json/annotation/processor/JsonAnnotationProcessorTest.java

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -321,22 +321,6 @@ void testNullableBeans() throws Exception {
321321
.hasMessageStartingWith("Expecting [VALUE_NUMBER_INT] token for field 'field4', got VALUE_NULL");
322322
}
323323

324-
@Test
325-
void testEnum() throws Exception {
326-
var cl = processClass0(DtoWithEnum.class);
327-
var reader = cl.reader(DtoWithEnum.class, new EnumJsonReader<>(DtoWithEnum.TestEnum.values(), Enum::name));
328-
var writer = cl.writer(DtoWithEnum.class, cl.writer(DtoWithEnum.TestEnum.class));
329-
330-
var expected = new DtoWithEnum(DtoWithEnum.TestEnum.VAL1);
331-
var json = """
332-
{
333-
"testEnum" : "VAL1"
334-
}""";
335-
336-
assertThat(fromJson(reader, json)).isEqualTo(expected);
337-
assertThat(toJson(writer, expected)).isEqualTo(json);
338-
}
339-
340324
@Test
341325
void testObject() throws Exception {
342326
var cl = processClass0(DtoWithObject.class);

json/json-annotation-processor/src/test/java/ru/tinkoff/kora/json/annotation/processor/dto/DtoWithEnum.java

Lines changed: 0 additions & 11 deletions
This file was deleted.

json/json-common/src/main/java/ru/tinkoff/kora/json/common/EnumJsonReader.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,35 @@
22

33
import com.fasterxml.jackson.core.JsonParseException;
44
import com.fasterxml.jackson.core.JsonParser;
5-
import com.fasterxml.jackson.core.JsonToken;
65

76
import javax.annotation.Nullable;
87
import java.io.IOException;
98
import java.util.HashMap;
109
import java.util.Map;
1110
import java.util.function.Function;
1211

13-
public final class EnumJsonReader<T extends Enum<T>> implements JsonReader<T> {
14-
private final Map<String, T> values;
12+
public final class EnumJsonReader<T extends Enum<T>, V> implements JsonReader<T> {
13+
private final Map<V, T> values;
14+
private final JsonReader<V> valueReader;
1515

16-
public EnumJsonReader(T[] values, Function<T, String> mapper) {
16+
public EnumJsonReader(T[] values, Function<T, V> mapper, JsonReader<V> valueReader) {
1717
this.values = new HashMap<>();
1818
for (var value : values) {
1919
this.values.put(mapper.apply(value), value);
2020
}
21+
this.valueReader = valueReader;
2122
}
2223

2324
@Nullable
2425
@Override
2526
public T read(JsonParser parser) throws IOException {
26-
var token = parser.currentToken();
27-
if (token == JsonToken.VALUE_NULL) {
27+
var jsonValue = this.valueReader.read(parser);
28+
if (jsonValue == null) {
2829
return null;
2930
}
30-
if (token != JsonToken.VALUE_STRING) {
31-
throw new JsonParseException(parser, "Expecting VALUE_STRING token, got " + token);
32-
}
33-
var stringValue = parser.getText();
34-
var value = this.values.get(stringValue);
31+
var value = this.values.get(jsonValue);
3532
if (value == null) {
36-
throw new JsonParseException(parser, "Expecting one of " + this.values.keySet() + ", got " + stringValue);
33+
throw new JsonParseException(parser, "Expecting one of " + this.values.keySet() + ", got " + jsonValue);
3734
}
3835
return value;
3936
}
Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,35 @@
11
package ru.tinkoff.kora.json.common;
22

33
import com.fasterxml.jackson.core.JsonGenerator;
4-
import com.fasterxml.jackson.core.io.SerializedString;
54

65
import javax.annotation.Nullable;
76
import java.io.IOException;
7+
import java.io.UncheckedIOException;
88
import java.util.function.Function;
99

10-
public final class EnumJsonWriter<T extends Enum<T>> implements JsonWriter<T> {
11-
private final SerializedString[] values;
10+
public final class EnumJsonWriter<T extends Enum<T>, V> implements JsonWriter<T> {
11+
private final RawJson[] values;
1212

13-
public EnumJsonWriter(T[] values, Function<T, String> mapper) {
14-
this.values = new SerializedString[values.length];
13+
public EnumJsonWriter(T[] values, Function<T, V> valueExtractor, JsonWriter<V> valueWriter) {
14+
this.values = new RawJson[values.length];
1515
for (int i = 0; i < values.length; i++) {
16-
this.values[i] = new SerializedString(mapper.apply(values[i]));
16+
var enumValue = values[i];
17+
var value = valueExtractor.apply(enumValue);
18+
try {
19+
var bytes = valueWriter.toByteArray(value);
20+
this.values[i] = new RawJson(bytes);
21+
} catch (IOException e) {
22+
throw new UncheckedIOException(e);
23+
}
1724
}
1825
}
1926

2027
@Override
2128
public void write(JsonGenerator gen, @Nullable T object) throws IOException {
2229
if (object == null) {
2330
gen.writeNull();
24-
} else {
25-
gen.writeString(this.values[object.ordinal()]);
31+
return;
2632
}
33+
gen.writeRawValue(this.values[object.ordinal()]);
2734
}
2835
}

0 commit comments

Comments
 (0)