Implement reflective support for Java Records (#1381)

* Standardize around JDK 8

* Update GJF to support newer JDKs

* Fix misc java 8 issues in tests

* Prepare java 16/records checking at runtime

* Implement real RecordJsonAdapter

* Spotless

* Prepare build for JDK 16+

* Fix property name for kapt

* Small cleanup

* Make FallbackEnum java-8 happy

* Remove animalsniffer

* Fix format

* Add opens for ExtendsPlatformClassWithProtectedFields

* Return null every time in shim for main tests

* Use JDK 16 + release 8 to replace animalsniffer

* Simplify accessor accessible handling

* Remove manifest attrs

* Fix typo

* Fix KCT tests + upgrade it

* Cover another

* Try explicit kotlin daemon args for java 17?

* Disable 17-ea for now until kotlin 1.5.30

* Add JsonQualifier and Json(name) tests + fix qualifiers

* Ensure constructor is accessible

* GJF it properly

* GJF 1.11

* Unwrap InvocationTargetException

* Use MethodHandle for constructor

* Use MethodHandle for accessor too

* Revert "Remove manifest attrs"

This reverts commit 3eb768fd6904bb5c979aa01c3c182e0fb9329d62.

* Proper MR jar

* *actually* fix GJF, which wasn't getting applied before

We can just enable this everywhere now since we require JDK 16 anyway

* Make IDE happy about modules access

* Fixup records tests to play nice with modules

Gotta be public

* Add complex smoke test

* Remove comment

Not a regression test in this case
This commit is contained in:
Zac Sweers
2021-08-23 12:00:06 -04:00
committed by GitHub
parent 2572c29e42
commit 95250b0359
20 changed files with 653 additions and 104 deletions

View File

@@ -10,14 +10,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
java-version: java-version:
- 8 - 16
- 9
- 10
- 11
- 12
- 13
- 14
- 15
steps: steps:
- name: Checkout - name: Checkout
@@ -46,7 +39,7 @@ jobs:
run: ./gradlew build check --stacktrace run: ./gradlew build check --stacktrace
- name: Publish (default branch only) - name: Publish (default branch only)
if: github.repository == 'square/moshi' && github.ref == 'refs/heads/master' && matrix.java-version == '8' if: github.repository == 'square/moshi' && github.ref == 'refs/heads/master' && matrix.java-version == '16'
run: ./gradlew uploadArchives run: ./gradlew uploadArchives
env: env:
ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}

View File

@@ -14,18 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") kotlin("jvm")
id("com.vanniktech.maven.publish") id("com.vanniktech.maven.publish")
id("ru.vyarus.animalsniffer")
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "1.6"
}
} }
dependencies { dependencies {

View File

@@ -33,7 +33,6 @@ plugins {
id("com.vanniktech.maven.publish") version "0.14.2" apply false id("com.vanniktech.maven.publish") version "0.14.2" apply false
id("org.jetbrains.dokka") version "1.4.32" apply false id("org.jetbrains.dokka") version "1.4.32" apply false
id("com.diffplug.spotless") version "5.12.4" id("com.diffplug.spotless") version "5.12.4"
id("ru.vyarus.animalsniffer") version "1.5.3" apply false
id("me.champeau.gradle.japicmp") version "0.2.9" apply false id("me.champeau.gradle.japicmp") version "0.2.9" apply false
} }
@@ -44,8 +43,6 @@ spotless {
indentWithSpaces(2) indentWithSpaces(2)
endWithNewline() endWithNewline()
} }
// GJF not compatible with JDK 15 yet
if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) {
val externalJavaFiles = arrayOf( val externalJavaFiles = arrayOf(
"**/ClassFactory.java", "**/ClassFactory.java",
"**/Iso8601Utils.java", "**/Iso8601Utils.java",
@@ -70,7 +67,7 @@ spotless {
"**/TypesTest.java" "**/TypesTest.java"
) )
val configureCommonJavaFormat: JavaExtension.() -> Unit = { val configureCommonJavaFormat: JavaExtension.() -> Unit = {
googleJavaFormat("1.7") googleJavaFormat("1.11.0")
} }
java { java {
configureCommonJavaFormat() configureCommonJavaFormat()
@@ -88,7 +85,6 @@ spotless {
configureCommonJavaFormat() configureCommonJavaFormat()
target(*externalJavaFiles) target(*externalJavaFiles)
} }
}
kotlin { kotlin {
ktlint(Dependencies.ktlintVersion).userData(mapOf("indent_size" to "2")) ktlint(Dependencies.ktlintVersion).userData(mapOf("indent_size" to "2"))
target("**/*.kt") target("**/*.kt")
@@ -129,15 +125,14 @@ subprojects {
// Apply with "java" instead of just "java-library" so kotlin projects get it too // Apply with "java" instead of just "java-library" so kotlin projects get it too
pluginManager.withPlugin("java") { pluginManager.withPlugin("java") {
configure<JavaPluginExtension> { configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_7 toolchain {
targetCompatibility = JavaVersion.VERSION_1_7 languageVersion.set(JavaLanguageVersion.of(16))
} }
} }
if (project.name != "records-tests") {
pluginManager.withPlugin("ru.vyarus.animalsniffer") { tasks.withType<JavaCompile>().configureEach {
dependencies { options.release.set(8)
"compileOnly"(Dependencies.AnimalSniffer.annotations) }
"signature"(Dependencies.AnimalSniffer.java7Signature)
} }
} }
@@ -146,6 +141,7 @@ subprojects {
kotlinOptions { kotlinOptions {
@Suppress("SuspiciousCollectionReassignment") @Suppress("SuspiciousCollectionReassignment")
freeCompilerArgs += listOf("-progressive") freeCompilerArgs += listOf("-progressive")
jvmTarget = "1.8"
} }
} }

View File

@@ -21,11 +21,6 @@ object Dependencies {
const val ktlintVersion = "0.41.0" const val ktlintVersion = "0.41.0"
const val okio = "com.squareup.okio:okio:2.10.0" const val okio = "com.squareup.okio:okio:2.10.0"
object AnimalSniffer {
const val annotations = "org.codehaus.mojo:animal-sniffer-annotations:1.16"
const val java7Signature = "org.codehaus.mojo.signature:java17:1.0@signature"
}
object AutoService { object AutoService {
private const val version = "1.0" private const val version = "1.0"
const val annotations = "com.google.auto.service:auto-service-annotations:$version" const val annotations = "com.google.auto.service:auto-service-annotations:$version"
@@ -53,7 +48,7 @@ object Dependencies {
object Testing { object Testing {
const val assertj = "org.assertj:assertj-core:3.11.1" const val assertj = "org.assertj:assertj-core:3.11.1"
const val compileTesting = "com.github.tschuchortdev:kotlin-compile-testing:1.4.0" const val compileTesting = "com.github.tschuchortdev:kotlin-compile-testing:1.4.3"
const val junit = "junit:junit:4.13.2" const val junit = "junit:junit:4.13.2"
const val truth = "com.google.truth:truth:1.0.1" const val truth = "com.google.truth:truth:1.0.1"
} }

View File

@@ -58,9 +58,9 @@ final class FallbackEnum {
if (!(annotation instanceof Fallback)) { if (!(annotation instanceof Fallback)) {
return null; return null;
} }
Class<Enum> enumType = (Class<Enum>) rawType; //noinspection rawtypes
Enum<?> fallback = Enum.valueOf(enumType, ((Fallback) annotation).value()); return new FallbackEnumJsonAdapter<>(
return new FallbackEnumJsonAdapter<>(enumType, fallback); (Class<? extends Enum>) rawType, ((Fallback) annotation).value());
} }
}; };
@@ -70,9 +70,9 @@ final class FallbackEnum {
final JsonReader.Options options; final JsonReader.Options options;
final T defaultValue; final T defaultValue;
FallbackEnumJsonAdapter(Class<T> enumType, T defaultValue) { FallbackEnumJsonAdapter(Class<T> enumType, String fallbackName) {
this.enumType = enumType; this.enumType = enumType;
this.defaultValue = defaultValue; this.defaultValue = Enum.valueOf(enumType, fallbackName);
try { try {
constants = enumType.getEnumConstants(); constants = enumType.getEnumConstants();
nameStrings = new String[constants.length]; nameStrings = new String[constants.length];

View File

@@ -15,9 +15,36 @@
# #
# For Dokka https://github.com/Kotlin/dokka/issues/1405 # For Dokka https://github.com/Kotlin/dokka/issues/1405
org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 \
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
kapt.includeCompileClasspath=false # TODO move this to DSL in Kotlin 1.5.30 https://youtrack.jetbrains.com/issue/KT-44266
kotlin.daemon.jvmargs=-Dfile.encoding=UTF-8 \
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
kapt.include.compile.classpath=false
GROUP=com.squareup.moshi GROUP=com.squareup.moshi
VERSION_NAME=1.13.0-SNAPSHOT VERSION_NAME=1.13.0-SNAPSHOT

View File

@@ -27,7 +27,6 @@ plugins {
tasks.withType<KotlinCompile>().configureEach { tasks.withType<KotlinCompile>().configureEach {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8"
@Suppress("SuspiciousCollectionReassignment") @Suppress("SuspiciousCollectionReassignment")
freeCompilerArgs += listOf( freeCompilerArgs += listOf(
"-Xopt-in=com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview" "-Xopt-in=com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview"
@@ -35,10 +34,20 @@ tasks.withType<KotlinCompile>().configureEach {
} }
} }
// To make Gradle happy tasks.withType<Test>().configureEach {
java { // For kapt to work with kotlin-compile-testing
sourceCompatibility = JavaVersion.VERSION_1_8 jvmArgs(
targetCompatibility = JavaVersion.VERSION_1_8 "--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
)
} }
val shade: Configuration = configurations.maybeCreate("compileShaded") val shade: Configuration = configurations.maybeCreate("compileShaded")

View File

@@ -21,6 +21,11 @@ plugins {
kotlin("kapt") kotlin("kapt")
} }
tasks.withType<Test>().configureEach {
// ExtendsPlatformClassWithProtectedField tests a case where we set a protected ByteArrayOutputStream.buf field
jvmArgs("--add-opens=java.base/java.io=ALL-UNNAMED")
}
tasks.withType<KotlinCompile>().configureEach { tasks.withType<KotlinCompile>().configureEach {
kotlinOptions { kotlinOptions {
@Suppress("SuspiciousCollectionReassignment") @Suppress("SuspiciousCollectionReassignment")

View File

@@ -19,14 +19,49 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") kotlin("jvm")
id("com.vanniktech.maven.publish") id("com.vanniktech.maven.publish")
id("ru.vyarus.animalsniffer") }
val mainSourceSet by sourceSets.named("main")
val java16 by sourceSets.creating {
java {
srcDir("src/main/java16")
}
}
tasks.named<JavaCompile>("compileJava16Java") {
javaCompiler.set(
javaToolchains.compilerFor {
languageVersion.set(JavaLanguageVersion.of(16))
}
)
options.release.set(16)
}
// Package our actual RecordJsonAdapter from java16 sources in and denote it as an MRJAR
tasks.named<Jar>("jar") {
from(java16.output) {
into("META-INF/versions/16")
}
manifest {
attributes("Multi-Release" to "true")
}
}
configurations {
"java16Implementation" {
extendsFrom(api.get())
extendsFrom(implementation.get())
}
}
tasks.withType<Test>().configureEach {
// ExtendsPlatformClassWithProtectedField tests a case where we set a protected ByteArrayOutputStream.buf field
jvmArgs("--add-opens=java.base/java.io=ALL-UNNAMED")
} }
tasks.withType<KotlinCompile>() tasks.withType<KotlinCompile>()
.configureEach { .configureEach {
kotlinOptions { kotlinOptions {
jvmTarget = "1.6"
if (name.contains("test", true)) { if (name.contains("test", true)) {
@Suppress("SuspiciousCollectionReassignment") // It's not suspicious @Suppress("SuspiciousCollectionReassignment") // It's not suspicious
freeCompilerArgs += listOf("-Xopt-in=kotlin.ExperimentalStdlibApi") freeCompilerArgs += listOf("-Xopt-in=kotlin.ExperimentalStdlibApi")
@@ -35,6 +70,8 @@ tasks.withType<KotlinCompile>()
} }
dependencies { dependencies {
// So the j16 source set can "see" main Moshi sources
"java16Implementation"(mainSourceSet.output)
compileOnly(Dependencies.jsr305) compileOnly(Dependencies.jsr305)
api(Dependencies.okio) api(Dependencies.okio)

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2021 Square, Inc.
*
* 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.
*/
plugins {
`java-library`
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(16)
}
dependencies {
testImplementation(project(":moshi"))
testCompileOnly(Dependencies.jsr305)
testImplementation(Dependencies.Testing.junit)
testImplementation(Dependencies.Testing.truth)
}

View File

@@ -0,0 +1,195 @@
/*
* Copyright (C) 2021 Square, Inc.
*
* 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 com.squareup.moshi.records;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.squareup.moshi.FromJson;
import com.squareup.moshi.Json;
import com.squareup.moshi.JsonQualifier;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.ToJson;
import com.squareup.moshi.Types;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.junit.Test;
public final class RecordsTest {
private final Moshi moshi = new Moshi.Builder().build();
@Test
public void smokeTest() throws IOException {
var stringAdapter = moshi.adapter(String.class);
var adapter =
moshi
.newBuilder()
.add(CharSequence.class, stringAdapter)
.add(Types.subtypeOf(CharSequence.class), stringAdapter)
.add(Types.supertypeOf(CharSequence.class), stringAdapter)
.build()
.adapter(SmokeTestType.class);
var instance =
new SmokeTestType(
"John",
"Smith",
25,
List.of("American"),
70.5f,
null,
true,
List.of("super wildcards!"),
List.of("extend wildcards!"),
List.of("unbounded"),
List.of("objectList"),
new int[] {1, 2, 3},
new String[] {"fav", "arrays"},
Map.of("italian", "pasta"),
Set.of(List.of(Map.of("someKey", new int[] {1}))),
new Map[] {Map.of("Hello", "value")});
var json = adapter.toJson(instance);
var deserialized = adapter.fromJson(json);
assertThat(deserialized).isEqualTo(instance);
}
public static record SmokeTestType(
@Json(name = "first_name") String firstName,
@Json(name = "last_name") String lastName,
int age,
List<String> nationalities,
float weight,
Boolean tattoos, // Boxed primitive test
boolean hasChildren,
List<? super CharSequence> superWildcard,
List<? extends CharSequence> extendsWildcard,
List<?> unboundedWildcard,
List<Object> objectList,
int[] favoriteThreeNumbers,
String[] favoriteArrayValues,
Map<String, String> foodPreferences,
Set<List<Map<String, int[]>>> setListMapArrayInt,
Map<String, Object>[] nestedArray) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SmokeTestType that = (SmokeTestType) o;
return age == that.age
&& Float.compare(that.weight, weight) == 0
&& hasChildren == that.hasChildren
&& firstName.equals(that.firstName)
&& lastName.equals(that.lastName)
&& nationalities.equals(that.nationalities)
&& Objects.equals(tattoos, that.tattoos)
&& superWildcard.equals(that.superWildcard)
&& extendsWildcard.equals(that.extendsWildcard)
&& unboundedWildcard.equals(that.unboundedWildcard)
&& objectList.equals(that.objectList)
&& Arrays.equals(favoriteThreeNumbers, that.favoriteThreeNumbers)
&& Arrays.equals(favoriteArrayValues, that.favoriteArrayValues)
&& foodPreferences.equals(that.foodPreferences)
// && setListMapArrayInt.equals(that.setListMapArrayInt) // Nested array equality doesn't
// carry over
&& Arrays.equals(nestedArray, that.nestedArray);
}
@Override
public int hashCode() {
int result =
Objects.hash(
firstName,
lastName,
age,
nationalities,
weight,
tattoos,
hasChildren,
superWildcard,
extendsWildcard,
unboundedWildcard,
objectList,
foodPreferences,
setListMapArrayInt);
result = 31 * result + Arrays.hashCode(favoriteThreeNumbers);
result = 31 * result + Arrays.hashCode(favoriteArrayValues);
result = 31 * result + Arrays.hashCode(nestedArray);
return result;
}
}
@Test
public void genericRecord() throws IOException {
var adapter =
moshi.<GenericRecord<String>>adapter(
Types.newParameterizedTypeWithOwner(
RecordsTest.class, GenericRecord.class, String.class));
assertThat(adapter.fromJson("{\"value\":\"Okay!\"}")).isEqualTo(new GenericRecord<>("Okay!"));
}
public static record GenericRecord<T>(T value) {}
@Test
public void genericBoundedRecord() throws IOException {
var adapter =
moshi.<GenericBoundedRecord<Integer>>adapter(
Types.newParameterizedTypeWithOwner(
RecordsTest.class, GenericBoundedRecord.class, Integer.class));
assertThat(adapter.fromJson("{\"value\":4}")).isEqualTo(new GenericBoundedRecord<>(4));
}
@Test
public void qualifiedValues() throws IOException {
var adapter = moshi.newBuilder().add(new ColorAdapter()).build().adapter(QualifiedValues.class);
assertThat(adapter.fromJson("{\"value\":\"#ff0000\"}"))
.isEqualTo(new QualifiedValues(16711680));
}
public static record QualifiedValues(@HexColor int value) {}
@Retention(RUNTIME)
@JsonQualifier
@interface HexColor {}
/** Converts strings like #ff0000 to the corresponding color ints. */
public static class ColorAdapter {
@ToJson
public String toJson(@HexColor int rgb) {
return String.format("#%06x", rgb);
}
@FromJson
@HexColor
public int fromJson(String rgb) {
return Integer.parseInt(rgb.substring(1), 16);
}
}
public static record GenericBoundedRecord<T extends Number>(T value) {}
@Test
public void jsonName() throws IOException {
var adapter = moshi.adapter(JsonName.class);
assertThat(adapter.fromJson("{\"actualValue\":3}")).isEqualTo(new JsonName(3));
}
public static record JsonName(@Json(name = "actualValue") int value) {}
}

View File

@@ -51,6 +51,7 @@ public final class Moshi {
BUILT_IN_FACTORIES.add(CollectionJsonAdapter.FACTORY); BUILT_IN_FACTORIES.add(CollectionJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(MapJsonAdapter.FACTORY); BUILT_IN_FACTORIES.add(MapJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(ArrayJsonAdapter.FACTORY); BUILT_IN_FACTORIES.add(ArrayJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(RecordJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(ClassJsonAdapter.FACTORY); BUILT_IN_FACTORIES.add(ClassJsonAdapter.FACTORY);
} }

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2021 Square, Inc.
*
* 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 com.squareup.moshi;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Set;
import javax.annotation.Nullable;
/**
* This is just a simple shim for linking in {@link StandardJsonAdapters} and swapped with a real
* implementation in Java 16 via MR Jar.
*/
final class RecordJsonAdapter<T> extends JsonAdapter<T> {
static final JsonAdapter.Factory FACTORY =
new JsonAdapter.Factory() {
@Nullable
@Override
public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
return null;
}
};
@Nullable
@Override
public T fromJson(JsonReader reader) throws IOException {
throw new AssertionError();
}
@Override
public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
throw new AssertionError();
}
}

View File

@@ -0,0 +1,218 @@
/*
* Copyright (C) 2021 Square, Inc.
*
* 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 com.squareup.moshi;
import static java.lang.invoke.MethodType.methodType;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.RecordComponent;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* A {@link JsonAdapter} that supports Java {@code record} classes via reflection.
*
* <p><em>NOTE:</em> Java records require JDK 16 or higher.
*/
final class RecordJsonAdapter<T> extends JsonAdapter<T> {
static final JsonAdapter.Factory FACTORY =
(type, annotations, moshi) -> {
if (!annotations.isEmpty()) {
return null;
}
if (!(type instanceof Class) && !(type instanceof ParameterizedType)) {
return null;
}
var rawType = Types.getRawType(type);
if (!rawType.isRecord()) {
return null;
}
Map<String, Type> mappedTypeArgs = null;
if (type instanceof ParameterizedType parameterizedType) {
Type[] typeArgs = parameterizedType.getActualTypeArguments();
var typeVars = rawType.getTypeParameters();
mappedTypeArgs = new LinkedHashMap<>(typeArgs.length);
for (int i = 0; i < typeArgs.length; ++i) {
var typeVarName = typeVars[i].getName();
var materialized = typeArgs[i];
mappedTypeArgs.put(typeVarName, materialized);
}
}
var components = rawType.getRecordComponents();
var bindings = new LinkedHashMap<String, ComponentBinding<?>>();
var constructorParams = new Class<?>[components.length];
var lookup = MethodHandles.lookup();
for (int i = 0, componentsLength = components.length; i < componentsLength; i++) {
RecordComponent component = components[i];
constructorParams[i] = component.getType();
var name = component.getName();
var componentType = component.getGenericType();
if (componentType instanceof TypeVariable<?> typeVariable) {
var typeVarName = typeVariable.getName();
if (mappedTypeArgs == null) {
throw new AssertionError(
"No mapped type arguments found for type '" + typeVarName + "'");
}
var mappedType = mappedTypeArgs.get(typeVarName);
if (mappedType == null) {
throw new AssertionError(
"No materialized type argument found for type '" + typeVarName + "'");
}
componentType = mappedType;
}
var jsonName = name;
Set<Annotation> qualifiers = null;
for (var annotation : component.getDeclaredAnnotations()) {
if (annotation instanceof Json jsonAnnotation) {
jsonName = jsonAnnotation.name();
} else {
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
if (qualifiers == null) {
qualifiers = new LinkedHashSet<>();
}
qualifiers.add(annotation);
}
}
}
if (qualifiers == null) {
qualifiers = Collections.emptySet();
}
var adapter = moshi.adapter(componentType, qualifiers);
MethodHandle accessor;
try {
accessor = lookup.unreflect(component.getAccessor());
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
var componentBinding = new ComponentBinding<>(name, jsonName, adapter, accessor);
var replaced = bindings.put(jsonName, componentBinding);
if (replaced != null) {
throw new IllegalArgumentException(
"Conflicting components:\n"
+ " "
+ replaced.name
+ "\n"
+ " "
+ componentBinding.name);
}
}
MethodHandle constructor;
try {
constructor = lookup.findConstructor(rawType, methodType(void.class, constructorParams));
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new AssertionError(e);
}
return new RecordJsonAdapter<>(constructor, rawType.getSimpleName(), bindings).nullSafe();
};
private static record ComponentBinding<T>(
String name, String jsonName, JsonAdapter<T> adapter, MethodHandle accessor) {}
private final String targetClass;
private final MethodHandle constructor;
private final ComponentBinding<Object>[] componentBindingsArray;
private final JsonReader.Options options;
@SuppressWarnings("ToArrayCallWithZeroLengthArrayArgument")
public RecordJsonAdapter(
MethodHandle constructor,
String targetClass,
Map<String, ComponentBinding<?>> componentBindings) {
this.constructor = constructor;
this.targetClass = targetClass;
//noinspection unchecked
this.componentBindingsArray =
componentBindings.values().toArray(new ComponentBinding[componentBindings.size()]);
this.options =
JsonReader.Options.of(
componentBindings.keySet().toArray(new String[componentBindings.size()]));
}
@Override
public T fromJson(JsonReader reader) throws IOException {
var resultsArray = new Object[componentBindingsArray.length];
reader.beginObject();
while (reader.hasNext()) {
int index = reader.selectName(options);
if (index == -1) {
reader.skipName();
reader.skipValue();
continue;
}
var result = componentBindingsArray[index].adapter.fromJson(reader);
resultsArray[index] = result;
}
reader.endObject();
try {
//noinspection unchecked
return (T) constructor.invokeWithArguments(resultsArray);
} catch (Throwable e) {
if (e instanceof InvocationTargetException ite) {
Throwable cause = ite.getCause();
if (cause instanceof RuntimeException) throw (RuntimeException) cause;
if (cause instanceof Error) throw (Error) cause;
throw new RuntimeException(cause);
} else {
throw new AssertionError(e);
}
}
}
@Override
public void toJson(JsonWriter writer, T value) throws IOException {
writer.beginObject();
for (var binding : componentBindingsArray) {
writer.name(binding.jsonName);
try {
binding.adapter.toJson(writer, binding.accessor.invoke(value));
} catch (Throwable e) {
if (e instanceof InvocationTargetException ite) {
Throwable cause = ite.getCause();
if (cause instanceof RuntimeException) throw (RuntimeException) cause;
if (cause instanceof Error) throw (Error) cause;
throw new RuntimeException(cause);
} else {
throw new AssertionError(e);
}
}
}
writer.endObject();
}
@Override
public String toString() {
return "JsonAdapter(" + targetClass + ")";
}
}

View File

@@ -126,7 +126,7 @@ public final class JsonAdapterTest {
fail(); fail();
} catch (JsonDataException expected) { } catch (JsonDataException expected) {
assertThat(expected).hasMessageThat().isEqualTo("Unexpected null at $[1]"); assertThat(expected).hasMessageThat().isEqualTo("Unexpected null at $[1]");
assertThat(reader.nextNull()).isNull(); assertThat(reader.<Object>nextNull()).isNull();
} }
assertThat(toUpperCase.fromJson(reader)).isEqualTo("C"); assertThat(toUpperCase.fromJson(reader)).isEqualTo("C");
reader.endArray(); reader.endArray();

View File

@@ -565,7 +565,7 @@ public final class JsonReaderTest {
assertThat(reader2.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); assertThat(reader2.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
JsonReader reader3 = newReader("null"); JsonReader reader3 = newReader("null");
assertThat(reader3.nextNull()).isNull(); assertThat(reader3.<Object>nextNull()).isNull();
assertThat(reader3.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); assertThat(reader3.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
JsonReader reader4 = newReader("123"); JsonReader reader4 = newReader("123");

View File

@@ -59,7 +59,7 @@ public final class JsonValueReaderTest {
assertThat(reader.hasNext()).isTrue(); assertThat(reader.hasNext()).isTrue();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.NULL); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NULL);
assertThat(reader.nextNull()).isNull(); assertThat(reader.<Object>nextNull()).isNull();
assertThat(reader.hasNext()).isFalse(); assertThat(reader.hasNext()).isFalse();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_ARRAY); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_ARRAY);
@@ -103,7 +103,7 @@ public final class JsonValueReaderTest {
assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME);
assertThat(reader.nextName()).isEqualTo("d"); assertThat(reader.nextName()).isEqualTo("d");
assertThat(reader.peek()).isEqualTo(JsonReader.Token.NULL); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NULL);
assertThat(reader.nextNull()).isNull(); assertThat(reader.<Object>nextNull()).isNull();
assertThat(reader.hasNext()).isFalse(); assertThat(reader.hasNext()).isFalse();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_OBJECT); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_OBJECT);

View File

@@ -152,10 +152,10 @@ public final class JsonValueWriterTest {
public void primitiveIntegerTypesEmitLong() throws Exception { public void primitiveIntegerTypesEmitLong() throws Exception {
JsonValueWriter writer = new JsonValueWriter(); JsonValueWriter writer = new JsonValueWriter();
writer.beginArray(); writer.beginArray();
writer.value(new Byte(Byte.MIN_VALUE)); writer.value(Byte.valueOf(Byte.MIN_VALUE));
writer.value(new Short(Short.MIN_VALUE)); writer.value(Short.valueOf(Short.MIN_VALUE));
writer.value(new Integer(Integer.MIN_VALUE)); writer.value(Integer.valueOf(Integer.MIN_VALUE));
writer.value(new Long(Long.MIN_VALUE)); writer.value(Long.valueOf(Long.MIN_VALUE));
writer.endArray(); writer.endArray();
List<Number> numbers = List<Number> numbers =
@@ -167,8 +167,8 @@ public final class JsonValueWriterTest {
public void primitiveFloatingPointTypesEmitDouble() throws Exception { public void primitiveFloatingPointTypesEmitDouble() throws Exception {
JsonValueWriter writer = new JsonValueWriter(); JsonValueWriter writer = new JsonValueWriter();
writer.beginArray(); writer.beginArray();
writer.value(new Float(0.5f)); writer.value(Float.valueOf(0.5f));
writer.value(new Double(0.5d)); writer.value(Double.valueOf(0.5d));
writer.endArray(); writer.endArray();
List<Number> numbers = Arrays.<Number>asList(0.5d, 0.5d); List<Number> numbers = Arrays.<Number>asList(0.5d, 0.5d);

View File

@@ -236,17 +236,17 @@ public final class JsonWriterTest {
JsonWriter writer = factory.newWriter(); JsonWriter writer = factory.newWriter();
writer.beginArray(); writer.beginArray();
try { try {
writer.value(new Double(Double.NaN)); writer.value(Double.valueOf(Double.NaN));
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (IllegalArgumentException expected) {
} }
try { try {
writer.value(new Double(Double.NEGATIVE_INFINITY)); writer.value(Double.valueOf(Double.NEGATIVE_INFINITY));
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (IllegalArgumentException expected) {
} }
try { try {
writer.value(new Double(Double.POSITIVE_INFINITY)); writer.value(Double.valueOf(Double.POSITIVE_INFINITY));
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (IllegalArgumentException expected) {
} }

View File

@@ -24,6 +24,7 @@ pluginManagement {
rootProject.name = "moshi-root" rootProject.name = "moshi-root"
include(":moshi") include(":moshi")
include(":moshi:japicmp") include(":moshi:japicmp")
include(":moshi:records-tests")
include(":adapters") include(":adapters")
include(":adapters:japicmp") include(":adapters:japicmp")
include(":examples") include(":examples")