Skip to content

Commit a4c9d89

Browse files
authored
Merge pull request #47 from SentryMan/main
Support Generic Records
2 parents d71e3d5 + 946f87d commit a4c9d89

File tree

7 files changed

+140
-22
lines changed

7 files changed

+140
-22
lines changed

blackbox-test/src/main/java/org/example/customer/generics/MyGenericHolder.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ public class MyGenericHolder<T> {
99
String author;
1010
T document;
1111

12+
@Json
13+
public record MyGenericHolderRecord<T,T2,T3>(T title, T2 author, T3 document) {}
14+
1215
public String getTitle() {
1316
return title;
1417
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package org.example.customer.generics;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.LinkedHashMap;
6+
7+
import org.example.customer.Address;
8+
import org.example.customer.generics.MyGenericHolder.MyGenericHolderRecord;
9+
import org.junit.jupiter.api.Test;
10+
11+
import io.avaje.jsonb.JsonType;
12+
import io.avaje.jsonb.Jsonb;
13+
import io.avaje.jsonb.Types;
14+
15+
class MyGenericHolderRecordTest {
16+
17+
Jsonb jsonb = Jsonb.builder().build();
18+
19+
private static MyGenericHolderRecord<String, String, Address> createTestData() {
20+
return new MyGenericHolderRecord<>("hello", "art", new Address(90L, "one"));
21+
}
22+
23+
@SuppressWarnings({"rawtypes"})
24+
@Test
25+
void toJson() {
26+
final var bean = createTestData();
27+
28+
final var type = jsonb.type(MyGenericHolderRecord.class);
29+
30+
final var asJson = type.toJson(bean);
31+
assertThat(asJson)
32+
.isEqualTo(
33+
"{\"title\":\"hello\",\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}");
34+
assertThat(jsonb.toJson(bean)).isEqualTo(asJson);
35+
36+
final var pageResult = type.fromJson(asJson);
37+
final var document = pageResult.document();
38+
// reading via Object means the list contains LinkedHashMap
39+
assertThat(document).isInstanceOf(LinkedHashMap.class);
40+
final var asMap = (LinkedHashMap) document;
41+
assertThat(asMap.get("street")).isEqualTo("one");
42+
43+
final var view = type.view("author,document(id)");
44+
final var partialJson2 = view.toJson(bean);
45+
// not supporting partial on the generic object (output includes street)
46+
assertThat(partialJson2)
47+
.isEqualTo("{\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}");
48+
}
49+
50+
@Test
51+
void toJson_withGenericParam() {
52+
final var bean = createTestData();
53+
54+
final var jsonb = Jsonb.builder().build();
55+
final JsonType<MyGenericHolderRecord<String, String, Address>> type =
56+
jsonb.type(
57+
Types.newParameterizedType(
58+
MyGenericHolderRecord.class, String.class, String.class, Address.class));
59+
60+
final var asJson = type.toJson(bean);
61+
assertThat(asJson)
62+
.isEqualTo(
63+
"{\"title\":\"hello\",\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}");
64+
assertThat(jsonb.toJson(bean)).isEqualTo(asJson);
65+
66+
final var genericResult = type.fromJson(asJson);
67+
final var document = genericResult.document();
68+
69+
assertThat(document.getId()).isEqualTo(90L);
70+
assertThat(document.getStreet()).isEqualTo("one");
71+
72+
final var partial = type.view("author,document(*)");
73+
final var partialJson = partial.toJson(bean);
74+
assertThat(partialJson)
75+
.isEqualTo("{\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}");
76+
77+
final var partial2 = type.view("author,document(id)");
78+
final var partialJson2 = partial2.toJson(bean);
79+
assertThat(partialJson2).isEqualTo("{\"author\":\"art\",\"document\":{\"id\":90}}");
80+
}
81+
}

jsonb-generator/src/main/java/io/avaje/jsonb/generator/BeanReader.java

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package io.avaje.jsonb.generator;
22

3-
import io.avaje.jsonb.Json;
4-
5-
import javax.lang.model.element.Element;
6-
import javax.lang.model.element.TypeElement;
3+
import java.lang.reflect.InvocationTargetException;
74
import java.util.HashSet;
85
import java.util.List;
96
import java.util.Set;
107
import java.util.TreeSet;
118

9+
import javax.lang.model.element.Element;
10+
import javax.lang.model.element.TypeElement;
11+
12+
import io.avaje.jsonb.Json;
13+
1214
class BeanReader {
1315

1416
private final TypeElement beanType;
@@ -26,6 +28,7 @@ class BeanReader {
2628
private final boolean caseInsensitiveKeys;
2729
private FieldReader unmappedField;
2830
private boolean hasRaw;
31+
private final boolean isRecord;
2932

3033
BeanReader(TypeElement beanType, ProcessingContext context) {
3134
this.beanType = beanType;
@@ -41,6 +44,7 @@ class BeanReader {
4144
this.hasSubTypes = typeReader.hasSubTypes();
4245
this.allFields = typeReader.allFields();
4346
this.constructor = typeReader.constructor();
47+
this.isRecord = isRecord(beanType);
4448
}
4549

4650
public BeanReader(TypeElement beanType, TypeElement mixInElement, ProcessingContext context) {
@@ -57,6 +61,21 @@ public BeanReader(TypeElement beanType, TypeElement mixInElement, ProcessingCont
5761
this.hasSubTypes = typeReader.hasSubTypes();
5862
this.allFields = typeReader.allFields();
5963
this.constructor = typeReader.constructor();
64+
this.isRecord = isRecord(beanType);
65+
}
66+
67+
boolean isRecord(TypeElement beanType) {
68+
try {
69+
final List<? extends Element> recordComponents =
70+
(List<? extends Element>)
71+
TypeElement.class.getMethod("getRecordComponents").invoke(beanType);
72+
return !recordComponents.isEmpty();
73+
} catch (IllegalAccessException
74+
| InvocationTargetException
75+
| NoSuchMethodException
76+
| SecurityException e) {
77+
return false;
78+
}
6079
}
6180

6281
int genericTypeParamsCount() {
@@ -281,9 +300,14 @@ void writeFromJson(Append writer) {
281300
// default public constructor
282301
writer.append(" %s _$%s = new %s();", shortName, varName, shortName).eol();
283302
} else {
284-
writer.append(" // variables to read json values into, constructor params don't need _set$ flags").eol();
285-
for (FieldReader allField : allFields) {
286-
if (allField.includeFromJson()) {
303+
writer
304+
.append(
305+
" // variables to read json values into, constructor params don't need _set$ flags")
306+
.eol();
307+
for (final FieldReader allField : allFields) {
308+
if (isRecord) {
309+
allField.writeFromJsonVariablesRecord(writer);
310+
} else if (allField.includeFromJson()) {
287311
allField.writeFromJsonVariables(writer);
288312
}
289313
}

jsonb-generator/src/main/java/io/avaje/jsonb/generator/FieldReader.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,7 @@ private String initAdapterShortType(String shortType) {
8484

8585
private String initShortName() {
8686
if (genericTypeParameter) {
87-
String name = genericType.shortName();
88-
for (String typeParam : genericTypeParams) {
89-
name = name.replace(typeParam, "");
90-
}
87+
final String name = genericType.shortName();
9188
return Util.initLower(name) + "JsonAdapterGeneric";
9289
}
9390
return Util.initLower(genericType.shortName()) + "JsonAdapter";
@@ -288,6 +285,13 @@ void writeFromJsonVariables(Append writer) {
288285
writer.eol();
289286
}
290287

288+
void writeFromJsonVariablesRecord(Append writer) {
289+
final String type = genericTypeParameter ? "Object" : genericType.shortType();
290+
writer.append(" %s _val$%s = %s;", pad(type), fieldName, defaultValue);
291+
292+
writer.eol();
293+
}
294+
291295
private String pad(String value) {
292296
final int pad = 10 - value.length();
293297
if (pad < 1) {

jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleAdapterWriter.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,14 @@ void write() throws IOException {
5353

5454
private void writeFactory() {
5555
if (genericParamsCount > 0) {
56+
57+
String typeName = adapterShortName;
58+
int nestedIndex = adapterShortName.indexOf("$");
59+
if (nestedIndex != -1) {
60+
typeName = typeName.substring(nestedIndex + 1);
61+
}
5662
writer.append(" public static final JsonAdapter.Factory Factory = (type, jsonb) -> {").eol();
57-
writer.append(" if (type instanceof ParameterizedType && Types.rawType(type) == %s.class) {", adapterShortName).eol();
63+
writer.append(" if (type instanceof ParameterizedType && Types.rawType(type) == %s.class) {", typeName).eol();
5864
writer.append(" Type[] args = Types.typeArguments(type);").eol();
5965
writer.append(" return new %sJsonAdapter(jsonb", adapterShortName);
6066
for (int i = 0; i < genericParamsCount; i++) {

jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleComponentWriter.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ void writeMetaInf() throws IOException {
5353
private void writeRegister() {
5454
writer.append(" @Override").eol();
5555
writer.append(" public void register(Jsonb.Builder builder) {").eol();
56-
List<String> strings = metaData.allFactories();
57-
for (String adapterFullName : strings) {
58-
String adapterShortName = Util.shortName(adapterFullName);
59-
String typeName = typeShortName(adapterShortName);
60-
writer.append(" builder.add(%sJsonAdapter.Factory);", typeName).eol();
56+
final List<String> strings = metaData.allFactories();
57+
for (final String adapterFullName : strings) {
58+
final String adapterShortName = Util.shortName(adapterFullName);
59+
60+
writer.append(" builder.add(%s.Factory);", adapterShortName).eol();
6161
}
6262
for (String adapterFullName : metaData.all()) {
6363
String adapterShortName = Util.shortName(adapterFullName);

jsonb/src/main/java/io/avaje/jsonb/core/Util.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,13 @@ static final class ParameterizedTypeImpl implements ParameterizedType {
286286

287287
public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) {
288288
// Require an owner type if the raw type needs it.
289-
if (rawType instanceof Class<?>) {
289+
if (ownerType != null && rawType instanceof Class<?>) {
290290
Class<?> enclosingClass = ((Class<?>) rawType).getEnclosingClass();
291-
if (ownerType != null) {
292-
if (enclosingClass == null || Util.rawType(ownerType) != enclosingClass) {
293-
throw new IllegalArgumentException(
291+
292+
if (enclosingClass == null || Util.rawType(ownerType) != enclosingClass) {
293+
throw new IllegalArgumentException(
294294
"unexpected owner type for " + rawType + ": " + ownerType);
295-
}
295+
296296
} else if (enclosingClass != null) {
297297
throw new IllegalArgumentException("unexpected owner type for " + rawType + ": null");
298298
}

0 commit comments

Comments
 (0)