[go: nahoru, domu]

Serialization enum coder generation

This CL adds code generation for enum coders to SchemaProcessor. It
includes unit and integration tests for the code generation and an
initial pass at a data model for the code generation environment as well
as some JavaPoet extensions to enable idomatic Kotlin.

Test: ./gradlew :serialization:serialiation-compiler:test
Bug: 144724751
Change-Id: I32286139096ee644e98fdab4e0a8701450ccc11a
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/SchemaProcessor.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/SchemaProcessor.kt
index 45c481e..f0ed964 100644
--- a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/SchemaProcessor.kt
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/SchemaProcessor.kt
@@ -16,7 +16,8 @@
 
 package androidx.serialization.compiler
 
-import androidx.serialization.compiler.processing.steps.EnumCompilationStep
+import androidx.serialization.compiler.codegen.CodeGenEnvironment
+import androidx.serialization.compiler.processing.steps.EnumProcessingStep
 import com.google.auto.common.BasicAnnotationProcessor
 import com.google.auto.service.AutoService
 import com.google.common.collect.ImmutableList
@@ -31,9 +32,12 @@
 @AutoService(Processor::class)
 @IncrementalAnnotationProcessor(ISOLATING)
 class SchemaProcessor : BasicAnnotationProcessor() {
-    override fun initSteps(): List<ProcessingStep> = ImmutableList.of(
-        EnumCompilationStep(processingEnv)
-    )
+    override fun initSteps(): List<ProcessingStep> {
+        val codeGenEnv = CodeGenEnvironment(processingEnv, this::class.qualifiedName)
+        return ImmutableList.of(
+            EnumProcessingStep(processingEnv, codeGenEnv)
+        )
+    }
 
     override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
 }
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/CodeGenEnvironment.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/CodeGenEnvironment.kt
new file mode 100644
index 0000000..d60d2d4
--- /dev/null
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/CodeGenEnvironment.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 androidx.serialization.compiler.codegen
+
+import androidx.serialization.compiler.nullability.NULLABILITY_ANNOTATIONS
+import androidx.serialization.compiler.nullability.Nullability
+import androidx.serialization.compiler.processing.isClassPresent
+import androidx.serialization.compiler.processing.packageElement
+import com.google.auto.common.GeneratedAnnotations
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.TypeElement
+
+/**
+ * Details about the code generation environment, including the generating class and the
+ * availability of annotations.
+ */
+internal data class CodeGenEnvironment(
+    val generatingClassName: String? = null,
+    val generated: Generated? = null,
+    val nullability: Nullability? = null
+) {
+    constructor(
+        processingEnv: ProcessingEnvironment,
+        generatingClass: String? = null
+    ) : this(
+        generatingClass,
+
+        generated = GeneratedAnnotations
+            .generatedAnnotation(processingEnv.elementUtils, processingEnv.sourceVersion)
+            .map { Generated(it) }
+            .orElse(null),
+
+        nullability = NULLABILITY_ANNOTATIONS.find { nullability ->
+            processingEnv.isClassPresent(nullability.qualifiedNonNullName) &&
+                    processingEnv.isClassPresent(nullability.qualifiedNullableName)
+        }
+    )
+
+    data class Generated(
+        val packageName: String,
+        val simpleName: String = "Generated"
+    ) {
+        constructor(typeElement: TypeElement) : this(
+            packageName = typeElement.packageElement.qualifiedName.toString(),
+            simpleName = typeElement.simpleName.toString()
+        )
+    }
+}
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/ComplexTypeExt.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/ComplexTypeExt.kt
new file mode 100644
index 0000000..f883ac2
--- /dev/null
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/ComplexTypeExt.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 androidx.serialization.compiler.codegen
+
+import androidx.serialization.compiler.schema.ProcessingType
+import androidx.serialization.schema.ComplexType
+import javax.lang.model.element.TypeElement
+
+/**
+ * The originating type element if present or null.
+ *
+ * This makes it easier for code generators to work with the base interface types for testing in
+ * isolation.
+ */
+internal val ComplexType.originatingElement: TypeElement?
+    get() = when (this) {
+        is ProcessingType -> this.element
+        else -> null
+    }
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/StringExt.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/StringExt.kt
new file mode 100644
index 0000000..54236f4
--- /dev/null
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/StringExt.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 androidx.serialization.compiler.codegen
+
+import java.util.Locale
+
+/** Convert a string to lower camel case, treating '_' as a delimiter. */
+internal fun String.toLowerCamelCase(): String {
+    return if (contains('_')) {
+        split('_').joinToString(separator = "") { it.safeCapitalize() }.safeDecapitalize()
+    } else {
+        safeDecapitalize()
+    }
+}
+
+/** Replacement for [String.capitalize] that does not use experimental APIs. */
+private fun String.safeCapitalize(): String {
+    return when (length) {
+        0 -> this
+        1 -> toUpperCase(Locale.ENGLISH)
+        else -> substring(0, 1).toLowerCase(Locale.ENGLISH) + substring(1)
+    }
+}
+
+/** Replacement for [String.decapitalize] that does not use experimental APIs. */
+private fun String.safeDecapitalize(): String {
+    return when (length) {
+        0 -> this
+        1 -> toLowerCase(Locale.ENGLISH)
+        else -> substring(0, 1).toLowerCase(Locale.ENGLISH) + substring(1)
+    }
+}
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/java/EnumCoder.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/java/EnumCoder.kt
new file mode 100644
index 0000000..ac59842
--- /dev/null
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/java/EnumCoder.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 androidx.serialization.compiler.codegen.java
+
+import androidx.serialization.EnumValue
+import androidx.serialization.compiler.codegen.originatingElement
+import androidx.serialization.compiler.codegen.toLowerCamelCase
+import androidx.serialization.schema.Enum
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.Modifier.FINAL
+import javax.lang.model.element.Modifier.PRIVATE
+import javax.lang.model.element.Modifier.PUBLIC
+import javax.lang.model.element.Modifier.STATIC
+
+/** Get the name used for an enum coder for the supplied class name. */
+internal fun enumCoderName(enumName: ClassName): ClassName {
+    return ClassName.get(
+        enumName.packageName(),
+        enumName.simpleNames().joinToString(
+            prefix = "\$Serialization",
+            separator = "_",
+            postfix = "EnumCoder"
+        )
+    )
+}
+
+/** Generate the Java source file for an enum coder. */
+internal fun generateEnumCoder(enum: Enum, javaGenEnv: JavaGenEnvironment): JavaFile {
+    val enumClass = enum.name.toClassName()
+    val variableName = nameAllocatorOf("encode", "decode")
+        .newName(enum.name.simpleName.toLowerCamelCase())
+
+    val default = enum.values.first { it.id == EnumValue.DEFAULT }
+    val values = enum.values.filter { it.id != EnumValue.DEFAULT }.sortedBy { it.id }
+
+    return buildClass(enumCoderName(enumClass), javaGenEnv, enum.originatingElement) {
+        addModifiers(PUBLIC, FINAL)
+        addJavadoc("Serialization of enum {@link $T}.\n", enumClass)
+
+        constructor { addModifiers(PRIVATE) }
+
+        method("encode") {
+            addModifiers(PUBLIC, STATIC)
+            parameter(variableName, enumClass, javaGenEnv.nullable)
+            returns(TypeName.INT)
+
+            controlFlow("if ($N != null)", variableName) {
+                controlFlow("switch ($N)", variableName) {
+                    for (value in values) {
+                        switchCase("\$N", value.name) {
+                            addStatement("return $L", value.id)
+                        }
+                    }
+                }
+            }
+
+            addCode("return $L; // $N\n", EnumValue.DEFAULT, default.name)
+        }
+
+        method("decode") {
+            addModifiers(PUBLIC, STATIC)
+            parameter("value", TypeName.INT)
+            returns(enumClass, javaGenEnv.nonNull)
+
+            controlFlow("switch (value)") {
+                for (value in values) {
+                    switchCase("\$L", value.id) {
+                        addStatement("return $T.$N", enumClass, value.name)
+                    }
+                }
+
+                switchDefault {
+                    addStatement("return $T.$N", enumClass, default.name)
+                }
+            }
+        }
+    }
+}
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/java/JavaGenEnvironment.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/java/JavaGenEnvironment.kt
new file mode 100644
index 0000000..1527353
--- /dev/null
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/java/JavaGenEnvironment.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 androidx.serialization.compiler.codegen.java
+
+import androidx.serialization.compiler.codegen.CodeGenEnvironment
+import com.squareup.javapoet.AnnotationSpec
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeSpec
+
+/** Code generation environment with [AnnotationSpec] wrappers. */
+internal data class JavaGenEnvironment(
+    val generatingClassName: String? = null,
+    val generated: AnnotationSpec? = null,
+    val nonNull: AnnotationSpec? = null,
+    val nullable: AnnotationSpec? = null
+) {
+    fun applyGenerated(typeSpec: TypeSpec.Builder) {
+        when {
+            generated != null -> typeSpec.addAnnotation(generated)
+            generatingClassName != null -> {
+                typeSpec.addJavadoc("\nGenerated by \$L. Do not modify.\n", generatingClassName)
+            }
+        }
+    }
+
+    constructor(codeGenEnv: CodeGenEnvironment) : this(
+        generatingClassName = codeGenEnv.generatingClassName,
+
+        generated = codeGenEnv.generated?.let { generated ->
+            codeGenEnv.generatingClassName?.let { className ->
+                AnnotationSpec.builder(ClassName.get(generated.packageName, generated.simpleName))
+                    .addMember("value", "\$S", className)
+                    .build()
+            }
+        },
+
+        nonNull = codeGenEnv.nullability?.let {
+            AnnotationSpec.builder(ClassName.get(it.packageName, it.nonNull)).build()
+        },
+
+        nullable = codeGenEnv.nullability?.let {
+            AnnotationSpec.builder(ClassName.get(it.packageName, it.nullable)).build()
+        }
+    )
+}
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/java/JavaPoetExt.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/java/JavaPoetExt.kt
new file mode 100644
index 0000000..0a2f2f3
--- /dev/null
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/java/JavaPoetExt.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 androidx.serialization.compiler.codegen.java
+
+import com.squareup.javapoet.AnnotationSpec
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.NameAllocator
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Element
+
+internal const val L = "\$L"
+internal const val N = "\$N"
+internal const val S = "\$S"
+internal const val T = "\$T"
+
+internal fun nameAllocatorOf(vararg names: String): NameAllocator {
+    return NameAllocator().apply { names.forEach { newName(it, it) } }
+}
+
+internal inline fun buildClass(
+    className: ClassName,
+    javaGenEnv: JavaGenEnvironment,
+    vararg originatingElements: Element?,
+    init: TypeSpec.Builder.() -> Unit
+): JavaFile {
+    return TypeSpec.classBuilder(className).apply {
+        init()
+        afterInit(javaGenEnv, originatingElements)
+    }.toJavaFile(className)
+}
+
+internal fun TypeSpec.Builder.toJavaFile(className: ClassName): JavaFile {
+    return JavaFile.builder(className.packageName(), build()).indent("    ").build()
+}
+
+internal inline fun TypeSpec.Builder.constructor(init: MethodSpec.Builder.() -> Unit) {
+    addMethod(MethodSpec.constructorBuilder().apply(init).build())
+}
+
+internal inline fun TypeSpec.Builder.method(name: String, init: MethodSpec.Builder.() -> Unit) {
+    addMethod(MethodSpec.methodBuilder(name).apply(init).build())
+}
+
+internal fun MethodSpec.Builder.parameter(
+    name: String,
+    type: TypeName,
+    vararg annotations: AnnotationSpec?
+) {
+    addParameter(ParameterSpec.builder(type, name).run {
+        annotations.forEach { if (it != null) addAnnotation(it) }
+        build()
+    })
+}
+
+internal fun MethodSpec.Builder.returns(
+    type: TypeName,
+    vararg annotations: AnnotationSpec?
+) {
+    returns(type)
+    annotations.forEach { if (it != null) addAnnotation(it) }
+}
+
+internal inline fun MethodSpec.Builder.controlFlow(
+    format: String,
+    vararg args: Any,
+    body: MethodSpec.Builder.() -> Unit
+) {
+    beginControlFlow(format, *args)
+    body()
+    endControlFlow()
+}
+
+internal inline fun MethodSpec.Builder.switchCase(
+    format: String,
+    vararg args: Any,
+    body: MethodSpec.Builder.() -> Unit
+) {
+    addCode("case $format:\n$>", *args)
+    body()
+    addCode("$<")
+}
+
+internal inline fun MethodSpec.Builder.switchDefault(
+    body: MethodSpec.Builder.() -> Unit
+) {
+    addCode("default:\n$>")
+    body()
+    addCode("$<")
+}
+
+internal fun TypeSpec.Builder.afterInit(
+    javaGenEnv: JavaGenEnvironment,
+    originatingElements: Array<out Element?>
+) {
+    javaGenEnv.applyGenerated(this)
+    originatingElements.forEach { if (it != null) addOriginatingElement(it) }
+}
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/java/TypeNameExt.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/java/TypeNameExt.kt
new file mode 100644
index 0000000..387bedb
--- /dev/null
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/codegen/java/TypeNameExt.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 androidx.serialization.compiler.codegen.java
+
+import androidx.serialization.schema.TypeName
+import com.squareup.javapoet.ClassName
+
+/** Convert a type name to a JavaPoet class name */
+internal fun TypeName.toClassName(): ClassName {
+    return when (names.size) {
+        1 -> ClassName.get(packageName.orEmpty(), names.single())
+        else -> ClassName.get(
+            packageName.orEmpty(),
+            names.first(),
+            *names.drop(1).toTypedArray()
+        )
+    }
+}
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/nullability/KnownAnnotations.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/nullability/KnownAnnotations.kt
new file mode 100644
index 0000000..00f798a
--- /dev/null
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/nullability/KnownAnnotations.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 androidx.serialization.compiler.nullability
+
+/**
+ * A list of [Nullability] annotations ordered by preference.
+ *
+ * This is used for code generation, as `androidx.annotation` may not be present in the
+ * compile-time class path and the code generator may fall back to other annotation packages.
+ * These are derived from the set of annotations recognized by the Kotlin compiler.
+ *
+ * These will also be used for determining nullability of nested messages.
+ */
+internal val NULLABILITY_ANNOTATIONS = listOf(
+    Nullability("androidx.annotation"),
+    Nullability("org.jetbrains.annotations", nonNull = "NotNull"),
+    Nullability("javax.annotation", nonNull = "Nonnull"),
+    Nullability("android.support.annotation"),
+    Nullability("android.annotation"),
+    Nullability("com.android.annotations"),
+    Nullability("org.eclipse.jdt.annotation"),
+    Nullability("org.checkerframework.checker.nullness.qual"),
+    Nullability("io.reactivex.annotations")
+)
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/nullability/Nullability.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/nullability/Nullability.kt
new file mode 100644
index 0000000..8bbe9a8
--- /dev/null
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/nullability/Nullability.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 androidx.serialization.compiler.nullability
+
+/** A pair of non-null and nullable annotations */
+internal data class Nullability(
+    val packageName: String,
+    val nonNull: String = "NonNull",
+    val nullable: String = "Nullable"
+) {
+    val qualifiedNonNullName = "$packageName.$nonNull"
+    val qualifiedNullableName = "$packageName.$nullable"
+}
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/ElementExt.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/ElementExt.kt
index e0ffbe2..643661f 100644
--- a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/ElementExt.kt
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/ElementExt.kt
@@ -20,8 +20,10 @@
 import javax.lang.model.element.AnnotationMirror
 import javax.lang.model.element.Element
 import javax.lang.model.element.Modifier
+import javax.lang.model.element.PackageElement
 import javax.lang.model.element.TypeElement
 import javax.lang.model.element.VariableElement
+import javax.lang.model.util.SimpleElementVisitor6
 import kotlin.reflect.KClass
 
 /** Casts this element to a [TypeElement] using [MoreElements.asType]. */
@@ -39,6 +41,33 @@
     return Modifier.PRIVATE in modifiers
 }
 
+/**
+ * Determines if this element is visible to its own package.
+ *
+ * A private element or an element enclosed within a private element is not visible to its package.
+ */
+internal fun Element.isVisibleToPackage(): Boolean {
+    return accept(IsVisibleToPackageVisitor, null)
+}
+
+private object IsVisibleToPackageVisitor : SimpleElementVisitor6<Boolean, Nothing?>() {
+    override fun visitPackage(e: PackageElement, p: Nothing?): Boolean {
+        return true
+    }
+
+    override fun defaultAction(e: Element, p: Nothing?): Boolean {
+        return if (e.isPrivate()) {
+            false
+        } else {
+            e.enclosingElement.accept(this, null)
+        }
+    }
+}
+
+/** Gets the enclosing package element using [MoreElements.getPackage]. */
+internal val Element.packageElement: PackageElement
+    get() = MoreElements.getPackage(this)
+
 /** Get an annotation mirror on this element, throwing if it is not directly present. */
 internal operator fun Element.get(annotationClass: KClass<out Annotation>): AnnotationMirror {
     return requireNotNull(getAnnotationMirror(annotationClass)) {
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/MessagerExt.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/ProcessingExt.kt
similarity index 86%
rename from serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/MessagerExt.kt
rename to serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/ProcessingExt.kt
index 2418978..6c5a561 100644
--- a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/MessagerExt.kt
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/ProcessingExt.kt
@@ -17,6 +17,7 @@
 package androidx.serialization.compiler.processing
 
 import javax.annotation.processing.Messager
+import javax.annotation.processing.ProcessingEnvironment
 import javax.lang.model.element.AnnotationMirror
 import javax.lang.model.element.AnnotationValue
 import javax.lang.model.element.Element
@@ -24,6 +25,11 @@
 import javax.tools.Diagnostic.Kind.WARNING
 import kotlin.reflect.KClass
 
+/** Determine if a qualified class name is present in the processing environment. */
+internal fun ProcessingEnvironment.isClassPresent(qualifiedName: String): Boolean {
+    return elementUtils.getTypeElement(qualifiedName) != null
+}
+
 /** Print [message] as a warning with optional positional information. */
 internal inline fun Messager.warn(
     element: Element? = null,
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/TypeMirrorExt.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/TypeMirrorExt.kt
deleted file mode 100644
index 92173a5..0000000
--- a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/TypeMirrorExt.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * 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
- *
- *      http://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 androidx.serialization.compiler.processing
-
-import com.google.auto.common.MoreTypes
-import javax.lang.model.element.Element
-import javax.lang.model.element.TypeElement
-import javax.lang.model.type.TypeMirror
-
-/** Get the element represented by this type using [MoreTypes.asElement]. */
-internal fun TypeMirror.asElement(): Element {
-    return MoreTypes.asElement(this)
-}
-
-/** Convenience wrapper for [asElement] that casts to type element. */
-internal fun TypeMirror.asTypeElement(): TypeElement {
-    return asElement().asTypeElement()
-}
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/steps/AbstractStep.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/steps/AbstractProcessingStep.kt
similarity index 97%
rename from serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/steps/AbstractStep.kt
rename to serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/steps/AbstractProcessingStep.kt
index 6e50b3d..09be6e4 100644
--- a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/steps/AbstractStep.kt
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/steps/AbstractProcessingStep.kt
@@ -24,7 +24,7 @@
 import kotlin.reflect.KClass
 
 /** Wrapper for [ProcessingStep] for more idiomatic Kotlin. */
-internal abstract class AbstractStep(
+internal abstract class AbstractProcessingStep(
     vararg annotations: KClass<out Annotation>
 ) : ProcessingStep {
     private val javaAnnotations = ImmutableSet
diff --git a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/steps/EnumCompilationStep.kt b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/steps/EnumProcessingStep.kt
similarity index 71%
rename from serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/steps/EnumCompilationStep.kt
rename to serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/steps/EnumProcessingStep.kt
index 7153645..d8dd73b 100644
--- a/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/steps/EnumCompilationStep.kt
+++ b/serialization/serialization-compiler/src/main/kotlin/androidx/serialization/compiler/processing/steps/EnumProcessingStep.kt
@@ -18,11 +18,15 @@
 
 import androidx.serialization.EnumValue
 import androidx.serialization.Reserved
+import androidx.serialization.compiler.codegen.CodeGenEnvironment
+import androidx.serialization.compiler.codegen.java.JavaGenEnvironment
+import androidx.serialization.compiler.codegen.java.generateEnumCoder
 import androidx.serialization.compiler.processing.asTypeElement
 import androidx.serialization.compiler.processing.asVariableElement
 import androidx.serialization.compiler.processing.error
 import androidx.serialization.compiler.processing.get
 import androidx.serialization.compiler.processing.getAnnotationMirror
+import androidx.serialization.compiler.processing.isVisibleToPackage
 import androidx.serialization.compiler.processing.isPrivate
 import androidx.serialization.compiler.processing.processReserved
 import androidx.serialization.compiler.schema.Enum
@@ -35,16 +39,18 @@
 import kotlin.reflect.KClass
 
 /** Processing step that parses and validates enums, and generates enum coders. */
-internal class EnumCompilationStep(
+internal class EnumProcessingStep(
     private val processingEnv: ProcessingEnvironment,
+    private val codeGenEnv: CodeGenEnvironment,
     private val onEnum: ((Enum) -> Unit)? = null
-) : AbstractStep(EnumValue::class, Reserved::class) {
+) : AbstractProcessingStep(EnumValue::class, Reserved::class) {
+    private val javaGenEnv = JavaGenEnvironment(codeGenEnv)
     private val messager: Messager = processingEnv.messager
 
     override fun process(elementsByAnnotation: Map<KClass<out Annotation>, Set<Element>>) {
         elementsByAnnotation[EnumValue::class]
             ?.let(::processEnumValues)
-            ?.forEach(::processEnumType)
+            ?.forEach(::processEnumClass)
     }
 
     /**
@@ -57,11 +63,11 @@
     private fun processEnumValues(elements: Set<Element>): Set<TypeElement> {
         if (elements.isEmpty()) return emptySet()
 
-        val types = mutableSetOf<TypeElement>()
+        val enumClasses = mutableSetOf<TypeElement>()
 
         for (element in elements) {
             if (element.kind == ENUM_CONSTANT) {
-                types += element.enclosingElement.asTypeElement()
+                enumClasses += element.enclosingElement.asTypeElement()
             } else {
                 messager.error(element, EnumValue::class) {
                     "@${EnumValue::class.simpleName} must annotate an enum constant"
@@ -69,7 +75,7 @@
             }
         }
 
-        return types
+        return enumClasses
     }
 
     /**
@@ -80,23 +86,31 @@
      * reads [EnumValue.id] and constructs an [Enum] and dispatches it to [onEnum]. It fills
      * [Enum.reserved] using [processReserved].
      */
-    private fun processEnumType(typeElement: TypeElement) {
-        check(typeElement.kind == ElementKind.ENUM) {
-            "Expected $typeElement to be an enum class"
+    private fun processEnumClass(enumClass: TypeElement) {
+        check(enumClass.kind == ElementKind.ENUM) {
+            "Expected $enumClass to be an enum class"
         }
 
         var hasError = false
 
-        if (typeElement.isPrivate()) {
-            messager.error(typeElement) {
-                "Enum ${typeElement.qualifiedName} is private and cannot be serialized"
+        if (!enumClass.isVisibleToPackage()) {
+            if (enumClass.isPrivate()) {
+                messager.error(enumClass) {
+                    "Enum ${enumClass.qualifiedName} is private and cannot be serialized"
+                }
+            } else {
+                messager.error(enumClass) {
+                    "Enum ${enumClass.qualifiedName} is not visible to its package and cannot " +
+                            "be serialized"
+                }
             }
+
             hasError = true
         }
 
         val values = mutableSetOf<Enum.Value>()
 
-        for (element in typeElement.enclosedElements) {
+        for (element in enumClass.enclosedElements) {
             if (element.kind == ENUM_CONSTANT) {
                 val annotation = element.getAnnotationMirror(EnumValue::class)
 
@@ -112,6 +126,10 @@
             }
         }
 
-        if (!hasError) onEnum?.invoke(Enum(typeElement, values, processReserved(typeElement)))
+        if (!hasError) {
+            val enum = Enum(enumClass, values, processReserved(enumClass))
+            generateEnumCoder(enum, javaGenEnv).writeTo(processingEnv.filer)
+            onEnum?.invoke(enum)
+        }
     }
 }
diff --git a/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/SchemaProcessorTest.kt b/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/SchemaProcessorTest.kt
index d602752..4ba5dc8 100644
--- a/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/SchemaProcessorTest.kt
+++ b/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/SchemaProcessorTest.kt
@@ -23,10 +23,10 @@
 import org.junit.Test
 import javax.tools.JavaFileObject
 
-/** Integration tests for [SchemaProcessor]. */
+/** Unit tests for [SchemaProcessor]. */
 class SchemaProcessorTest {
     @Test
-    fun testSucceedsWithoutWarningsa() {
+    fun testSucceedsWithoutWarnings() {
         val testClass = JavaFileObjects.forSourceString("Test", "public class Test {}")
         assertThat(compile(testClass)).succeededWithoutWarnings()
     }
diff --git a/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/TestEnum.kt b/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/TestEnum.kt
new file mode 100644
index 0000000..aa0c509
--- /dev/null
+++ b/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/TestEnum.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 androidx.serialization.compiler
+
+import androidx.serialization.schema.Enum
+import androidx.serialization.schema.Reserved
+import androidx.serialization.schema.TypeName
+
+/** Customizable [Enum] implementation with default values scaffolding. */
+internal data class TestEnum(
+    override val name: TypeName = TypeName("com.example", "TestEnum"),
+    override val values: Set<Value> = setOf(
+        Value(0, "DEFAULT"),
+        Value(1, "ONE"),
+        Value(2, "TWO")
+    ),
+    override val reserved: Reserved = Reserved.empty()
+) : Enum {
+    data class Value(
+        override val id: Int,
+        override val name: String
+    ) : Enum.Value
+}
diff --git a/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/codegen/Environment.kt b/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/codegen/Environment.kt
new file mode 100644
index 0000000..b3a875c
--- /dev/null
+++ b/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/codegen/Environment.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 androidx.serialization.compiler.codegen
+
+import androidx.serialization.compiler.codegen.CodeGenEnvironment.Generated
+import androidx.serialization.compiler.codegen.java.JavaGenEnvironment
+import androidx.serialization.compiler.nullability.Nullability
+import kotlin.reflect.KClass
+
+internal fun codeGenEnv(
+    testClass: KClass<*>,
+    generated: Generated? = DEFAULT_GENERATED,
+    nullability: Nullability? = DEFAULT_NULLABILITY
+): CodeGenEnvironment {
+    return CodeGenEnvironment(testClass.qualifiedName, generated, nullability)
+}
+
+internal fun javaGenEnv(
+    testClass: KClass<*>,
+    generated: Generated? = DEFAULT_GENERATED,
+    nullability: Nullability? = DEFAULT_NULLABILITY
+): JavaGenEnvironment {
+    return JavaGenEnvironment(codeGenEnv(testClass, generated, nullability))
+}
+
+internal val DEFAULT_GENERATED = Generated(packageName = "javax.annotation")
+internal val DEFAULT_NULLABILITY = Nullability("androidx.annotation")
diff --git a/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/codegen/java/EnumCoderTest.kt b/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/codegen/java/EnumCoderTest.kt
new file mode 100644
index 0000000..57c12f8
--- /dev/null
+++ b/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/codegen/java/EnumCoderTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 androidx.serialization.compiler.codegen.java
+
+import androidx.serialization.compiler.TestEnum
+import androidx.serialization.compiler.codegen.javaGenEnv
+import com.google.common.truth.Truth.assertThat
+import com.squareup.javapoet.ClassName
+import org.junit.Test
+
+/** Unit tests for [generateEnumCoder] and [enumCoderName]. */
+class EnumCoderTest {
+    @Test
+    fun testEnumCoderName() {
+        assertThat(enumCoderName(ClassName.get("com.example", "Test")))
+            .isEqualTo(ClassName.get("com.example", "\$SerializationTestEnumCoder"))
+
+        assertThat(enumCoderName(ClassName.get("", "Outer", "Inner", "TestEnum")))
+            .isEqualTo(ClassName.get("", "\$SerializationOuter_Inner_TestEnumEnumCoder"))
+    }
+
+    @Test
+    fun testGenerateEnumCoder() {
+        assertThat(generateEnumCoder(TestEnum(), javaGenEnv(this::class)).toString()).contains("""
+            package com.example;
+
+            import androidx.annotation.NonNull;
+            import androidx.annotation.Nullable;
+            import javax.annotation.Generated;
+
+            /**
+             * Serialization of enum {@link TestEnum}.
+             */
+            @Generated("androidx.serialization.compiler.codegen.java.EnumCoderTest")
+            public final class ${'$'}SerializationTestEnumEnumCoder {
+                private ${'$'}SerializationTestEnumEnumCoder() {
+                }
+
+                public static int encode(@Nullable TestEnum testEnum) {
+                    if (testEnum != null) {
+                        switch (testEnum) {
+                            case ONE:
+                                return 1;
+                            case TWO:
+                                return 2;
+                        }
+                    }
+                    return 0; // DEFAULT
+                }
+
+                @NonNull
+                public static TestEnum decode(int value) {
+                    switch (value) {
+                        case 1:
+                            return TestEnum.ONE;
+                        case 2:
+                            return TestEnum.TWO;
+                        default:
+                            return TestEnum.DEFAULT;
+                    }
+                }
+            }
+        """.trimIndent())
+    }
+}
diff --git a/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/processing/ProcessReservedTest.kt b/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/processing/ProcessReservedTest.kt
index 02dcb83..a6c517d 100644
--- a/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/processing/ProcessReservedTest.kt
+++ b/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/processing/ProcessReservedTest.kt
@@ -16,7 +16,7 @@
 
 package androidx.serialization.compiler.processing
 
-import androidx.serialization.compiler.processing.steps.AbstractStep
+import androidx.serialization.compiler.processing.steps.AbstractProcessingStep
 import androidx.serialization.schema.Reserved
 import com.google.auto.common.BasicAnnotationProcessor
 import com.google.common.truth.Truth.assertThat
@@ -76,9 +76,9 @@
         return processor.reserved
     }
 
-    private class ReservedStep(
+    private class ReservedProcessingStep(
         private val onReserved: (Reserved) -> Unit
-    ) : AbstractStep(androidx.serialization.Reserved::class) {
+    ) : AbstractProcessingStep(androidx.serialization.Reserved::class) {
         override fun process(elementsByAnnotation: Map<KClass<out Annotation>, Set<Element>>) {
             elementsByAnnotation[androidx.serialization.Reserved::class]?.forEach {
                 onReserved(processReserved(it.asTypeElement()))
@@ -90,7 +90,7 @@
         lateinit var reserved: Reserved
 
         override fun initSteps(): List<ProcessingStep> = listOf(
-            ReservedStep { reserved = it }
+            ReservedProcessingStep { reserved = it }
         )
 
         override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
diff --git a/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/processing/steps/EnumCompilationStepTest.kt b/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/processing/steps/EnumProcessingStepTest.kt
similarity index 70%
rename from serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/processing/steps/EnumCompilationStepTest.kt
rename to serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/processing/steps/EnumProcessingStepTest.kt
index d0b7919..d83a3ee 100644
--- a/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/processing/steps/EnumCompilationStepTest.kt
+++ b/serialization/serialization-compiler/src/test/kotlin/androidx/serialization/compiler/processing/steps/EnumProcessingStepTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.serialization.compiler.processing.steps
 
+import androidx.serialization.compiler.codegen.CodeGenEnvironment
 import androidx.serialization.compiler.schema.Enum
 import androidx.serialization.schema.Reserved
 import com.google.auto.common.BasicAnnotationProcessor
@@ -29,15 +30,15 @@
 import javax.lang.model.SourceVersion
 import javax.tools.JavaFileObject
 
-/** Unit tests for [EnumCompilationStep]. */
-class EnumCompilationStepTest {
+/** Unit tests for [EnumProcessingStep]. */
+class EnumProcessingStepTest {
     private val enumValueCorrespondence = Correspondence.from({
             actual: Enum.Value?, expected: Pair<Int, String>? ->
         actual?.id == expected?.first && actual?.name == expected?.second
     }, "has ID and name")
 
     @Test
-    fun testEnum() {
+    fun testParsing() {
         val enum = compileEnum(JavaFileObjects.forSourceString("TestEnum", """
             import androidx.serialization.EnumValue;
             
@@ -70,8 +71,25 @@
         """.trimIndent())
 
         assertThat(compile(testEnum)).hadErrorContaining(
-            "Enum com.example.PrivateEnumTest.PrivateEnum is private and cannot be serialized"
-        )
+            "Enum com.example.PrivateEnumTest.PrivateEnum is private and cannot be serialized")
+    }
+
+    @Test
+    fun testInvalidPrivateNestedEnum() {
+        val testEnum = JavaFileObjects.forSourceString("PrivateNestedEnumTest", """
+            import androidx.serialization.EnumValue;
+            
+            public class PrivateNestedEnumTest {
+                private static class NestedClass {
+                    public enum NestedEnum {
+                        @EnumValue(EnumValue.DEFAULT) TEST
+                    }
+                }
+            }
+        """.trimIndent())
+
+        assertThat(compile(testEnum)).hadErrorContaining(
+            "Enum PrivateNestedEnumTest.NestedClass.NestedEnum is not visible to its package")
     }
 
     @Test
@@ -106,6 +124,26 @@
                     "annotated with @EnumValue")
     }
 
+    @Test
+    fun testCoderGeneration() {
+        val testEnum = JavaFileObjects.forSourceString("com.example.Test", """
+            package com.example;
+            import androidx.serialization.EnumValue;
+            
+            public enum Test {
+                @EnumValue(EnumValue.DEFAULT)
+                DEFAULT,
+                @EnumValue(1)
+                ONE,
+                @EnumValue(2)
+                TWO
+            }
+        """.trimIndent())
+
+        assertThat(compile(testEnum))
+            .generatedSourceFile("com.example.\$SerializationTestEnumCoder")
+    }
+
     private fun compile(vararg sources: JavaFileObject): Compilation {
         return javac().withProcessors(SchemaCompilationProcessor()).compile(*sources)
     }
@@ -115,18 +153,16 @@
         assertThat(javac().withProcessors(processor).compile(source))
             .succeededWithoutWarnings()
 
-        val enums = processor.enums
-        assertThat(enums).hasSize(1)
-
-        return enums.single()
+        return processor.enum
     }
 
     private class SchemaCompilationProcessor : BasicAnnotationProcessor() {
-        val enums = mutableSetOf<Enum>()
+        lateinit var enum: Enum
 
-        override fun initSteps(): List<ProcessingStep> = listOf(
-            EnumCompilationStep(processingEnv) { enums += it }
-        )
+        override fun initSteps(): List<ProcessingStep> {
+            val codeGenEnv = CodeGenEnvironment(EnumProcessingStepTest::class.qualifiedName)
+            return listOf(EnumProcessingStep(processingEnv, codeGenEnv) { enum = it })
+    }
 
         override fun getSupportedSourceVersion(): SourceVersion {
             return SourceVersion.latest()