Skip to content

Commit

Permalink
Support generating external adapters (#146)
Browse files Browse the repository at this point in the history
* Update dependencies

* Gradle 6

* Add option to generate externally

* Update README

* Style

* Add generic test

* Fix key

* Share lookup of jsonclass

* Use all names

* Add nested types and properly match all names

* Style

* Style

* Style
  • Loading branch information
ZacSweers authored Jan 12, 2020
1 parent d68520d commit eece4e2
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 54 deletions.
36 changes: 32 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,31 @@ An extension for Google's [AutoValue](https://github.com/google/auto) that creat

## Usage

Simply include auto-value-moshi in your project and add a public static method with the following
signature to classes you want to get Moshi `JsonAdapter`s. You can also annotate your properties
using `@Json` to define an alternate name for de/serialization.
Simply include auto-value-moshi in your project and annotate your target autovalue class with Moshi's
`@JsonClass` annotation. `generateAdpater` must be true, and the `generator` property value should
be `"avm"`.

```java
@JsonClass(generateAdapter = true, generator = "avm")
@AutoValue
public abstract class Foo {
abstract String bar();
@Json(name="Baz") abstract String baz();

public static JsonAdapter<Foo> jsonAdapter(Moshi moshi) {
return new AutoValue_Foo.MoshiJsonAdapter(moshi);
}
}
```

Using `@JsonClass`, no further configuration is necessary. Moshi 1.9+ will automatically pick these
types up at runtime.

### _Legacy alternative_

Add a public static method with the following signature to classes you want to get Moshi
`JsonAdapter`s. You can also annotate your properties using `@Json` to define an alternate name
for de/serialization.

```java
@AutoValue public abstract class Foo {
Expand All @@ -25,6 +47,8 @@ Now build your project and de/serialize your Foo.

## Generics support

_note: this section only applies if using the legacy opt-in via static method. If using `@JsonClass`, Moshi will handle this automatically_.

If the annotated class uses generics, the static method needs a little modification. Simply add a `Type[]` parameter and pass it to the generated `MoshiJsonAdapter` class.

```java
Expand All @@ -43,7 +67,9 @@ instantiate the class. If the `@AutoValue` class has a static no-argument factor
useful for setting default values.

```java
@AutoValue public abstract class Foo {
@JsonClass(generateAdapter = true, generator = "avm")
@AutoValue
public abstract class Foo {
abstract int bar();
abstract String quux();

Expand All @@ -60,6 +86,8 @@ useful for setting default values.

## Factory

_note: this section only applies if using the legacy opt-in via static method. If using `@JsonClass`, Moshi will handle this automatically_.

Optionally, auto-value-moshi can create a single [JsonAdapter.Factory](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/JsonAdapter.Factory.html) so
that you don't have to add each generated JsonAdapter to your Moshi instance manually.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.ryanharter.auto.value.moshi.test;

import com.google.auto.value.AutoValue;
import com.squareup.moshi.JsonClass;

@JsonClass(generateAdapter = true, generator = "avm")
@AutoValue
public abstract class GenericNativeMoshiClass<T> {
public abstract T property();

@AutoValue.Builder
public interface Builder<T> {
Builder<T> property(T prop);
GenericNativeMoshiClass<T> build();
}

@JsonClass(generateAdapter = true, generator = "avm")
@AutoValue
public abstract static class Nested<T> {
public abstract T property();

@AutoValue.Builder
public interface Builder<T> {
Builder<T> property(T prop);
Nested<T> build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.ryanharter.auto.value.moshi.test;

import com.google.auto.value.AutoValue;
import com.squareup.moshi.JsonClass;

@JsonClass(generateAdapter = true, generator = "avm")
@AutoValue
public abstract class NativeMoshiClass {
public abstract String property();

@AutoValue.Builder
public interface Builder {
Builder property(String prop);
NativeMoshiClass build();
}

@JsonClass(generateAdapter = true, generator = "avm")
@AutoValue
public abstract static class Nested {
public abstract String property();

@AutoValue.Builder
public interface Builder {
Builder property(String prop);
Nested build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import com.squareup.moshi.internal.NullSafeJsonAdapter;
import org.junit.Test;

import java.lang.reflect.Type;
Expand Down Expand Up @@ -218,4 +219,41 @@ public void objectWithNullableClassAndJsonQualifierNull() throws Exception {
assertThat(fromJson.value()).isEqualTo("value");
assertThat(fromJson.list()).isNull();
}

@Test
public void nativeMoshiLookup() {
JsonAdapter<NativeMoshiClass> adapter = moshi.adapter(NativeMoshiClass.class);
if (adapter instanceof NullSafeJsonAdapter) {
adapter = ((NullSafeJsonAdapter<NativeMoshiClass>) adapter).delegate();
}
assertThat(adapter.getClass()).isSameAs(NativeMoshiClassJsonAdapter.class);

JsonAdapter<NativeMoshiClass.Nested> nestedAdapter
= moshi.adapter(NativeMoshiClass.Nested.class);
if (nestedAdapter instanceof NullSafeJsonAdapter) {
nestedAdapter = ((NullSafeJsonAdapter<NativeMoshiClass.Nested>) nestedAdapter).delegate();
}
assertThat(nestedAdapter.getClass()).isSameAs(NativeMoshiClass_NestedJsonAdapter.class);
}

@Test
public void genericNativeMoshiLookup() {
JsonAdapter<GenericNativeMoshiClass<String>> adapter = moshi.adapter(
Types.newParameterizedType(GenericNativeMoshiClass.class, String.class));
if (adapter instanceof NullSafeJsonAdapter) {
adapter = ((NullSafeJsonAdapter<GenericNativeMoshiClass<String>>) adapter).delegate();
}
assertThat(adapter.getClass()).isSameAs(GenericNativeMoshiClassJsonAdapter.class);

JsonAdapter<GenericNativeMoshiClass.Nested<String>> nestedAdapter = moshi.adapter(
Types.newParameterizedTypeWithOwner(
GenericNativeMoshiClass.class,
GenericNativeMoshiClass.Nested.class,
String.class));
if (nestedAdapter instanceof NullSafeJsonAdapter) {
nestedAdapter = ((NullSafeJsonAdapter<GenericNativeMoshiClass.Nested<String>>) nestedAdapter)
.delegate();
}
assertThat(nestedAdapter.getClass()).isSameAs(GenericNativeMoshiClass_NestedJsonAdapter.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
Expand All @@ -23,6 +24,7 @@
import com.squareup.javapoet.WildcardTypeName;
import com.squareup.moshi.Json;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonClass;
import com.squareup.moshi.JsonQualifier;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
Expand Down Expand Up @@ -67,6 +69,7 @@
@AutoService(AutoValueExtension.class)
public final class AutoValueMoshiExtension extends AutoValueExtension {
private static final ClassName ADAPTER_CLASS_NAME = ClassName.get(JsonAdapter.class);
private static final String MOSHI_GENERATOR_KEY = "avm";

private static class Property {
final String methodName;
Expand Down Expand Up @@ -122,8 +125,11 @@ public IncrementalExtensionType incrementalType(ProcessingEnvironment processing
}

@Override public boolean applicable(Context context) {
// check that the class contains a static method returning a JsonAdapter
TypeElement type = context.autoValueClass();
if (generateExternalAdapter(type)) {
return true;
}
// check that the class contains a static method returning a JsonAdapter
ParameterizedTypeName jsonAdapterType = ParameterizedTypeName.get(
ADAPTER_CLASS_NAME, TypeName.get(type.asType()));
TypeName returnedJsonAdapter = null;
Expand Down Expand Up @@ -167,6 +173,11 @@ public IncrementalExtensionType incrementalType(ProcessingEnvironment processing
return false;
}

private static boolean generateExternalAdapter(TypeElement element) {
JsonClass jsonClass = element.getAnnotation(JsonClass.class);
return jsonClass != null && jsonClass.generateAdapter() && MOSHI_GENERATOR_KEY.equals(jsonClass.generator());
}

@Override public String generateClass(Context context, String className, String classToExtend,
boolean isFinal) {
List<Property> properties = readProperties(context.properties());
Expand All @@ -191,32 +202,65 @@ public IncrementalExtensionType incrementalType(ProcessingEnvironment processing
superclass = TypeVariableName.get(classToExtend);
}

TypeSpec typeAdapter =
createTypeAdapter(classNameClass, autoValueClassName, genericTypeNames, properties,
context.builder().orElse(null), context.processingEnvironment());
boolean generateExternalAdapter = generateExternalAdapter(context.autoValueClass());

TypeSpec.Builder subclass = TypeSpec.classBuilder(className)
.superclass(superclass)
.addType(typeAdapter)
.addMethod(generateConstructor(properties));
String adapterClassName = generateExternalAdapter
? Types.generatedJsonAdapterName(Joiner.on("$").join(autoValueClassName.simpleNames()))
: "MoshiJsonAdapter";

GeneratedAnnotationSpecs.generatedAnnotationSpec(
TypeSpec.Builder typeAdapterBuilder = createTypeAdapter(classNameClass,
autoValueClassName,
genericTypeNames,
properties,
context.builder().orElse(null),
context.processingEnvironment(),
adapterClassName);

Optional<AnnotationSpec> generatedAnnotation = GeneratedAnnotationSpecs.generatedAnnotationSpec(
context.processingEnvironment().getElementUtils(),
context.processingEnvironment().getSourceVersion(),
AutoValueMoshiExtension.class
).ifPresent(subclass::addAnnotation);
);

if (generateExternalAdapter(context.autoValueClass())) {
typeAdapterBuilder.addOriginatingElement(context.autoValueClass());
generatedAnnotation.ifPresent(typeAdapterBuilder::addAnnotation);
JavaFile javaFile = JavaFile.builder(context.packageName(), typeAdapterBuilder.build())
.skipJavaLangImports(true)
.build();
try {
javaFile.writeTo(context.processingEnvironment().getFiler());
} catch (IOException e) {
context.processingEnvironment().getMessager()
.printMessage(Diagnostic.Kind.ERROR,
String.format(
"Failed to write external TypeAdapter for element \"%s\" with reason \"%s\"",
context.autoValueClass(),
e.getMessage()));
}
return null;
} else {
TypeSpec typeAdapter = typeAdapterBuilder.addModifiers(STATIC).build();

if (shouldCreateGenerics) {
subclass.addTypeVariables(Arrays.asList(genericTypeNames));
}
TypeSpec.Builder subclass = TypeSpec.classBuilder(className)
.superclass(superclass)
.addType(typeAdapter)
.addMethod(generateConstructor(properties));

if (isFinal) {
subclass.addModifiers(FINAL);
} else {
subclass.addModifiers(ABSTRACT);
}
generatedAnnotation.ifPresent(subclass::addAnnotation);

if (shouldCreateGenerics) {
subclass.addTypeVariables(Arrays.asList(genericTypeNames));
}

return JavaFile.builder(context.packageName(), subclass.build()).build().toString();
if (isFinal) {
subclass.addModifiers(FINAL);
} else {
subclass.addModifiers(ABSTRACT);
}

return JavaFile.builder(context.packageName(), subclass.build()).build().toString();
}
}

private List<Property> readProperties(Map<String, ExecutableElement> properties) {
Expand Down Expand Up @@ -266,11 +310,16 @@ private MethodSpec generateConstructor(List<Property> properties) {
return builder.build();
}

private TypeSpec createTypeAdapter(ClassName className, ClassName autoValueClassName,
TypeVariableName[] genericTypeNames, List<Property> properties, @Nullable BuilderContext builderContext,
ProcessingEnvironment processingEnvironment) {
private TypeSpec.Builder createTypeAdapter(
ClassName className,
ClassName autoValueClassName,
TypeVariableName[] genericTypeNames,
List<Property> properties,
@Nullable BuilderContext builderContext,
ProcessingEnvironment processingEnvironment,
String adapterClassName
) {

TypeName typeAdapterClass = ParameterizedTypeName.get(ADAPTER_CLASS_NAME, autoValueClassName);
final TypeName autoValueTypeName = genericTypeNames != null && genericTypeNames.length > 0
? ParameterizedTypeName.get(autoValueClassName, genericTypeNames)
: autoValueClassName;
Expand Down Expand Up @@ -360,8 +409,8 @@ prop.type, typesArray, getTypeIndexInArray(genericTypeNames, prop.type),

ClassName jsonAdapterClassName = ClassName.get(JsonAdapter.class);
ParameterizedTypeName superClass = ParameterizedTypeName.get(jsonAdapterClassName, autoValueTypeName);
TypeSpec.Builder classBuilder = TypeSpec.classBuilder("MoshiJsonAdapter")
.addModifiers(PUBLIC, STATIC, FINAL)
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(adapterClassName)
.addModifiers(PUBLIC, FINAL)
.superclass(superClass)
.addFields(adapters.values())
.addMethod(constructor.build())
Expand All @@ -388,7 +437,7 @@ prop.type, typesArray, getTypeIndexInArray(genericTypeNames, prop.type),
classBuilder.addMethod(createAdapterWithQualifierMethod(autoValueClassName));
}

return classBuilder.build();
return classBuilder;
}

private int getTypeIndexInArray(TypeVariableName[] array, TypeName typeName) {
Expand Down
8 changes: 4 additions & 4 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ ext {
versions = [
java : JavaVersion.VERSION_1_8,
// Main dependencies
javaPoet : '1.11.0',
javaPoet : '1.12.0',
autoCommon : '0.10',
autoValue : '1.7',
autoService : '1.0-rc6',
guava : '27.1-jre',
moshi : '1.7.0',
moshi : '1.9.2',
incapHelper : '0.2',
jsr305 : '3.0.2',
// For testing
junit : '4.12',
truth : '0.44',
compileTesting: '0.16',
truth : '1.0',
compileTesting: '0.18',
assertJ : '2.5.0',
]

Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
5 changes: 2 additions & 3 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#Sun Nov 03 21:45:52 IST 2019
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading

0 comments on commit eece4e2

Please sign in to comment.