Skip to content

Support Generic Records #47

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

Merged
merged 7 commits into from
Jan 2, 2023
Merged
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 @@ -9,6 +9,9 @@ public class MyGenericHolder<T> {
String author;
T document;

@Json
public record MyGenericHolderRecord<T,T2,T3>(T title, T2 author, T3 document) {}

public String getTitle() {
return title;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.example.customer.generics;

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

import java.util.LinkedHashMap;

import org.example.customer.Address;
import org.example.customer.generics.MyGenericHolder.MyGenericHolderRecord;
import org.junit.jupiter.api.Test;

import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import io.avaje.jsonb.Types;

class MyGenericHolderRecordTest {

Jsonb jsonb = Jsonb.builder().build();

private static MyGenericHolderRecord<String, String, Address> createTestData() {
return new MyGenericHolderRecord<>("hello", "art", new Address(90L, "one"));
}

@SuppressWarnings({"rawtypes"})
@Test
void toJson() {
final var bean = createTestData();

final var type = jsonb.type(MyGenericHolderRecord.class);

final var asJson = type.toJson(bean);
assertThat(asJson)
.isEqualTo(
"{\"title\":\"hello\",\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}");
assertThat(jsonb.toJson(bean)).isEqualTo(asJson);

final var pageResult = type.fromJson(asJson);
final var document = pageResult.document();
// reading via Object means the list contains LinkedHashMap
assertThat(document).isInstanceOf(LinkedHashMap.class);
final var asMap = (LinkedHashMap) document;
assertThat(asMap.get("street")).isEqualTo("one");

final var view = type.view("author,document(id)");
final var partialJson2 = view.toJson(bean);
// not supporting partial on the generic object (output includes street)
assertThat(partialJson2)
.isEqualTo("{\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}");
}

@Test
void toJson_withGenericParam() {
final var bean = createTestData();

final var jsonb = Jsonb.builder().build();
final JsonType<MyGenericHolderRecord<String, String, Address>> type =
jsonb.type(
Types.newParameterizedType(
MyGenericHolderRecord.class, String.class, String.class, Address.class));

final var asJson = type.toJson(bean);
assertThat(asJson)
.isEqualTo(
"{\"title\":\"hello\",\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}");
assertThat(jsonb.toJson(bean)).isEqualTo(asJson);

final var genericResult = type.fromJson(asJson);
final var document = genericResult.document();

assertThat(document.getId()).isEqualTo(90L);
assertThat(document.getStreet()).isEqualTo("one");

final var partial = type.view("author,document(*)");
final var partialJson = partial.toJson(bean);
assertThat(partialJson)
.isEqualTo("{\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}");

final var partial2 = type.view("author,document(id)");
final var partialJson2 = partial2.toJson(bean);
assertThat(partialJson2).isEqualTo("{\"author\":\"art\",\"document\":{\"id\":90}}");
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package io.avaje.jsonb.generator;

import io.avaje.jsonb.Json;

import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import io.avaje.jsonb.Json;

class BeanReader {

private final TypeElement beanType;
Expand All @@ -25,6 +27,7 @@ class BeanReader {
private final boolean nonAccessibleField;
private FieldReader unmappedField;
private boolean hasRaw;
private final boolean isRecord;

BeanReader(TypeElement beanType, ProcessingContext context) {
this.beanType = beanType;
Expand All @@ -40,6 +43,7 @@ class BeanReader {
this.hasSubTypes = typeReader.hasSubTypes();
this.allFields = typeReader.allFields();
this.constructor = typeReader.constructor();
this.isRecord = isRecord(beanType);
}

public BeanReader(TypeElement beanType, TypeElement mixInElement, ProcessingContext context) {
Expand All @@ -55,6 +59,21 @@ public BeanReader(TypeElement beanType, TypeElement mixInElement, ProcessingCont
this.hasSubTypes = typeReader.hasSubTypes();
this.allFields = typeReader.allFields();
this.constructor = typeReader.constructor();
this.isRecord = isRecord(beanType);
}

boolean isRecord(TypeElement beanType) {
try {
final List<? extends Element> recordComponents =
(List<? extends Element>)
TypeElement.class.getMethod("getRecordComponents").invoke(beanType);
return !recordComponents.isEmpty();
} catch (IllegalAccessException
| InvocationTargetException
| NoSuchMethodException
| SecurityException e) {
return false;
}
}

int genericTypeParamsCount() {
Expand Down Expand Up @@ -279,9 +298,14 @@ void writeFromJson(Append writer) {
// default public constructor
writer.append(" %s _$%s = new %s();", shortName, varName, shortName).eol();
} else {
writer.append(" // variables to read json values into, constructor params don't need _set$ flags").eol();
for (FieldReader allField : allFields) {
if (allField.includeFromJson()) {
writer
.append(
" // variables to read json values into, constructor params don't need _set$ flags")
.eol();
for (final FieldReader allField : allFields) {
if (isRecord) {
allField.writeFromJsonVariablesRecord(writer);
} else if (allField.includeFromJson()) {
allField.writeFromJsonVariables(writer);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,7 @@ private String initAdapterShortType(String shortType) {

private String initShortName() {
if (genericTypeParameter) {
String name = genericType.shortName();
for (String typeParam : genericTypeParams) {
name = name.replace(typeParam, "");
}
final String name = genericType.shortName();
return Util.initLower(name) + "JsonAdapterGeneric";
}
return Util.initLower(genericType.shortName()) + "JsonAdapter";
Expand Down Expand Up @@ -288,6 +285,13 @@ void writeFromJsonVariables(Append writer) {
writer.eol();
}

void writeFromJsonVariablesRecord(Append writer) {
final String type = genericTypeParameter ? "Object" : genericType.shortType();
writer.append(" %s _val$%s = %s;", pad(type), fieldName, defaultValue);

writer.eol();
}

private String pad(String value) {
final int pad = 10 - value.length();
if (pad < 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,14 @@ void write() throws IOException {

private void writeFactory() {
if (genericParamsCount > 0) {

String typeName = adapterShortName;
int nestedIndex = adapterShortName.indexOf("$");
if (nestedIndex != -1) {
typeName = typeName.substring(nestedIndex + 1);
}
writer.append(" public static final JsonAdapter.Factory Factory = (type, jsonb) -> {").eol();
writer.append(" if (type instanceof ParameterizedType && Types.rawType(type) == %s.class) {", adapterShortName).eol();
writer.append(" if (type instanceof ParameterizedType && Types.rawType(type) == %s.class) {", typeName).eol();
writer.append(" Type[] args = Types.typeArguments(type);").eol();
writer.append(" return new %sJsonAdapter(jsonb", adapterShortName);
for (int i = 0; i < genericParamsCount; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ void writeMetaInf() throws IOException {
private void writeRegister() {
writer.append(" @Override").eol();
writer.append(" public void register(Jsonb.Builder builder) {").eol();
List<String> strings = metaData.allFactories();
for (String adapterFullName : strings) {
String adapterShortName = Util.shortName(adapterFullName);
String typeName = typeShortName(adapterShortName);
writer.append(" builder.add(%sJsonAdapter.Factory);", typeName).eol();
final List<String> strings = metaData.allFactories();
for (final String adapterFullName : strings) {
final String adapterShortName = Util.shortName(adapterFullName);

writer.append(" builder.add(%s.Factory);", adapterShortName).eol();
}
for (String adapterFullName : metaData.all()) {
String adapterShortName = Util.shortName(adapterFullName);
Expand Down
10 changes: 5 additions & 5 deletions jsonb/src/main/java/io/avaje/jsonb/core/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -286,13 +286,13 @@ static final class ParameterizedTypeImpl implements ParameterizedType {

public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) {
// Require an owner type if the raw type needs it.
if (rawType instanceof Class<?>) {
if (ownerType != null && rawType instanceof Class<?>) {
Class<?> enclosingClass = ((Class<?>) rawType).getEnclosingClass();
if (ownerType != null) {
if (enclosingClass == null || Util.rawType(ownerType) != enclosingClass) {
throw new IllegalArgumentException(

if (enclosingClass == null || Util.rawType(ownerType) != enclosingClass) {
throw new IllegalArgumentException(
"unexpected owner type for " + rawType + ": " + ownerType);
}

} else if (enclosingClass != null) {
throw new IllegalArgumentException("unexpected owner type for " + rawType + ": null");
}
Expand Down