Unverified Commit 2f2a9517 authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

Support Serialization via Serde Annotations (#40)

* Refactor SymbolMetadataProvider into an abstract class

* Refactor meta to expectMeta

* Refactor SymbolMetadataProvider into an abstract class

* Refactor meta to expectMeta

* Add support for inline dependencies

* Implement Json Serializers!

* CR fixes
parent 5014e625
Loading
Loading
Loading
Loading
+45 −15
Original line number Diff line number Diff line
@@ -18,12 +18,8 @@ sealed class DependencyLocation
data class CratesIo(val version: String) : DependencyLocation()
data class Local(val basePath: String) : DependencyLocation()

data class RustDependency(
    val name: String,
    val location: DependencyLocation,
    val scope: DependencyScope = Compile,
    val features: List<String> = listOf()
) : SymbolDependencyContainer {
sealed class RustDependency(open val name: String) : SymbolDependencyContainer {
    abstract fun version(): String
    override fun getDependencies(): List<SymbolDependency> {
        return listOf(
            SymbolDependency
@@ -34,7 +30,43 @@ data class RustDependency(
        )
    }

    private fun version(): String = when (location) {
    companion object {
        private const val PropertyKey = "rustdep"
        fun fromSymbolDependency(symbolDependency: SymbolDependency) =
            symbolDependency.getProperty(PropertyKey, RustDependency::class.java).get()
    }
}

/**
 * A dependency on a snippet of code
 *
 * InlineDependency should not be instantiated directly, rather, it should be constructed with
 * [software.amazon.smithy.rust.codegen.smithy.RuntimeType.forInlineFun]
 *
 * InlineDependencies are created as private modules within the main crate. This is useful for any code that
 * doesn't need to exist in a shared crate, but must still be generated exactly once during codegen.
 *
 * CodegenVisitor deduplicates inline dependencies by (module, name) during code generation.
 */
class InlineDependency(name: String, val module: String, val renderer: (RustWriter) -> Unit) : RustDependency(name) {
    override fun version(): String {
        return renderer(RustWriter.forModule("_")).hashCode().toString()
    }

    fun key() = "$module::$name"
}

/**
 * A dependency on an internal or external Cargo Crate
 */
data class CargoDependency(
    override val name: String,
    private val location: DependencyLocation,
    val scope: DependencyScope = Compile,
    private val features: List<String> = listOf()
) : RustDependency(name) {

    override fun version(): String = when (location) {
        is CratesIo -> location.version
        is Local -> "local"
    }
@@ -61,21 +93,19 @@ data class RustDependency(
    }

    companion object {
        val Http: RustDependency = RustDependency("http", CratesIo("0.2"))
        val Http: CargoDependency = CargoDependency("http", CratesIo("0.2"))
        fun SmithyTypes(runtimeConfig: RuntimeConfig) =
            RustDependency("${runtimeConfig.cratePrefix}-types", Local(runtimeConfig.relativePath))
            CargoDependency("${runtimeConfig.cratePrefix}-types", Local(runtimeConfig.relativePath))

        fun SmithyHttp(runtimeConfig: RuntimeConfig) = RustDependency(
        fun SmithyHttp(runtimeConfig: RuntimeConfig) = CargoDependency(
            "${runtimeConfig.cratePrefix}-http", Local(runtimeConfig.relativePath)
        )

        fun ProtocolTestHelpers(runtimeConfig: RuntimeConfig) = RustDependency(
        fun ProtocolTestHelpers(runtimeConfig: RuntimeConfig) = CargoDependency(
            "protocol-test-helpers", Local(runtimeConfig.relativePath), scope = Dev
        )

        private val PropertyKey = "rustdep"

        fun fromSymbolDependency(symbolDependency: SymbolDependency) =
            symbolDependency.getProperty(PropertyKey, RustDependency::class.java).get()
        val SerdeJson: CargoDependency = CargoDependency("serde_json", CratesIo("1"))
        val Serde = CargoDependency("serde", CratesIo("1"), features = listOf("derive"))
    }
}
+2 −2
Original line number Diff line number Diff line
package software.amazon.smithy.rust.codegen.lang

data class RustModule(val name: String, val meta: Meta) {
data class RustModule(val name: String, val rustMetadata: RustMetadata) {
    fun render(writer: RustWriter) {
        meta.render(writer)
        rustMetadata.render(writer)
        writer.write("mod $name;")
    }
}
+30 −3
Original line number Diff line number Diff line
@@ -84,6 +84,26 @@ fun RustType.render(): String = when (this) {
    is RustType.Opaque -> this.name
}

/**
 * Returns true if [this] contains [t] anywhere within it's tree. For example,
 * Option<Instant>.contains(Instant) would return true.
 * Option<Instant>.contains(Blob) would return false.
 */
fun <T : RustType> RustType.contains(t: T): Boolean {
    if (t == this) {
        return true
    }

    return when (this) {
        is RustType.Vec -> this.member.contains(t)
        is RustType.HashSet -> this.member.contains(t)
        is RustType.Reference -> this.value.contains(t)
        is RustType.Option -> this.value.contains(t)
        is RustType.Box -> this.value.contains(t)
        else -> false
    }
}

inline fun <reified T : Container> RustType.stripOuter(): RustType {
    return when (this) {
        is T -> this.value
@@ -94,16 +114,23 @@ inline fun <reified T : Container> RustType.stripOuter(): RustType {
/**
 * Meta information about a Rust construction (field, struct, or enum)
 */
data class Meta(val derives: Derives = Derives.Empty, val additionalAttributes: List<Attribute> = listOf(), val public: Boolean) {
data class RustMetadata(
    val derives: Derives = Derives.Empty,
    val additionalAttributes: List<Attribute> = listOf(),
    val public: Boolean
) {
    fun withDerive(newDerive: RuntimeType): RustMetadata =
        this.copy(derives = derives.copy(derives = derives.derives + newDerive))

    fun attributes(): List<Attribute> = additionalAttributes + derives
    fun renderAttributes(writer: RustWriter): Meta {
    fun renderAttributes(writer: RustWriter): RustMetadata {
        attributes().forEach {
            it.render(writer)
        }
        return this
    }

    fun renderVisibility(writer: RustWriter): Meta {
    fun renderVisibility(writer: RustWriter): RustMetadata {
        if (public) {
            writer.writeInline("pub ")
        }
+3 −8
Original line number Diff line number Diff line
@@ -96,13 +96,13 @@ class RustWriter private constructor(private val filename: String, val namespace
     *
     * The returned writer will inject any local imports into the module as needed.
     */
    fun withModule(moduleName: String, meta: Meta = Meta(public = true), moduleWriter: RustWriter.() -> Unit) {
    fun withModule(moduleName: String, rustMetadata: RustMetadata = RustMetadata(public = true), moduleWriter: RustWriter.() -> Unit) {
        // In Rust, modules must specify their own imports—they don't have access to the parent scope.
        // To easily handle this, create a new inner writer to collect imports, then dump it
        // into an inline module.
        val innerWriter = RustWriter(this.filename, "${this.namespace}::$moduleName")
        moduleWriter(innerWriter)
        meta.render(this)
        rustMetadata.render(this)
        rustBlock("mod $moduleName") {
            write(innerWriter.toString())
        }
@@ -161,12 +161,7 @@ class RustWriter private constructor(private val filename: String, val namespace
                is RuntimeType -> {
                    t.dependency?.also { addDependency(it) }
                    // for now, use the fully qualified type name
                    val prefix = if (t.namespace.startsWith("crate")) {
                        ""
                    } else {
                        "::"
                    }
                    "$prefix${t.namespace}::${t.name}"
                    t.fullyQualifiedName()
                }
                is Symbol -> {
                    if (t.namespace != namespace) {
+21 −11
Original line number Diff line number Diff line
@@ -17,8 +17,10 @@ import software.amazon.smithy.model.shapes.StringShape
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.shapes.UnionShape
import software.amazon.smithy.model.traits.EnumTrait
import software.amazon.smithy.rust.codegen.lang.Meta
import software.amazon.smithy.rust.codegen.lang.CargoDependency
import software.amazon.smithy.rust.codegen.lang.InlineDependency
import software.amazon.smithy.rust.codegen.lang.RustDependency
import software.amazon.smithy.rust.codegen.lang.RustMetadata
import software.amazon.smithy.rust.codegen.lang.RustModule
import software.amazon.smithy.rust.codegen.lang.RustWriter
import software.amazon.smithy.rust.codegen.smithy.generators.CargoTomlGenerator
@@ -36,12 +38,10 @@ import software.amazon.smithy.rust.codegen.util.CommandFailed
import software.amazon.smithy.rust.codegen.util.runCommand
import java.util.logging.Logger

private val Modules = listOf(
    RustModule("error", Meta(public = true)),
    RustModule("operation", Meta(public = true)),
    RustModule("model", Meta(public = true)),
    RustModule("serializer", Meta(public = false))
)
/**
 * Allowlist of modules that will be exposed publicly in generated crates
 */
private val PublicModules = setOf("error", "operation", "model")

class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default<Unit>() {

@@ -85,17 +85,27 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default<Unit>() {
        val service = settings.getService(model)
        val serviceShapes = Walker(model).walkShapes(service)
        serviceShapes.forEach { it.accept(this) }
        val loadDependencies = { writers.dependencies.map { dep -> RustDependency.fromSymbolDependency(dep) } }
        val inlineDependencies = loadDependencies().filterIsInstance<InlineDependency>().distinctBy { it.key() }
        inlineDependencies.forEach { dep ->
            writers.useFileWriter("src/${dep.module}.rs", "crate::${dep.module}") {
                dep.renderer(it)
            }
        }
        val cargoDependencies = loadDependencies().filterIsInstance<CargoDependency>().distinct()
        writers.useFileWriter("Cargo.toml") {
            val cargoToml = CargoTomlGenerator(
                settings,
                it,
                writers.dependencies.map { dep -> RustDependency.fromSymbolDependency(dep) }.distinct()
                cargoDependencies
            )
            cargoToml.render()
        }
        writers.useFileWriter("src/lib.rs", "crate::lib") { writer ->
            val includedModules = writers.includedModules().toSet()
            val modules = Modules.filter { module -> includedModules.contains(module.name) }
            val includedModules = writers.includedModules().toSet().filter { it != "lib" }
            val modules = includedModules.map {
                RustModule(it, RustMetadata(public = PublicModules.contains(it)))
            }
            LibRsGenerator(modules).render(writer)
        }
        writers.flushWriters()
@@ -131,6 +141,6 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default<Unit>() {
    }

    override fun serviceShape(shape: ServiceShape) {
        ServiceGenerator(writers, httpGenerator, protocolConfig).render()
        ServiceGenerator(writers, httpGenerator, protocolGenerator.support(), protocolConfig).render()
    }
}
Loading