Skip to content

Commit

Permalink
Codegenerate StructureShape, BlobShape, application, server and Pytho…
Browse files Browse the repository at this point in the history
…n runtime (#1403)

We are introducing code-generation for Python bindings of StructureShape, BlobShape, EnumShape, OperationShape.

This PR also add a runtime server implementation to support serving Python business logic directly and with an idiomatic experience.

Co-authored-by: david-perez <d@vidp.dev>
  • Loading branch information
crisidev and david-perez committed Jun 14, 2022
1 parent 8911e86 commit 8e84ee2
Show file tree
Hide file tree
Showing 37 changed files with 1,436 additions and 99 deletions.
9 changes: 1 addition & 8 deletions codegen-server-test/python/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ val defaultRustFlags: String by project
val defaultRustDocFlags: String by project
val properties = PropertyRetriever(rootProject, project)

val pluginName = "rust-server-codegen"
val pluginName = "rust-server-codegen-python"
val workingDirUnderBuildDir = "smithyprojections/codegen-server-test-python/"

configure<software.amazon.smithy.gradle.SmithyExtension> {
Expand All @@ -39,13 +39,6 @@ dependencies {

val allCodegenTests = listOf(
CodegenTest("com.amazonaws.simple#SimpleService", "simple"),
CodegenTest("aws.protocoltests.restjson#RestJson", "rest_json"),
CodegenTest("aws.protocoltests.restjson.validation#RestJsonValidation", "rest_json_validation"),
CodegenTest("aws.protocoltests.json10#JsonRpc10", "json_rpc10"),
CodegenTest("aws.protocoltests.json#JsonProtocol", "json_rpc11"),
CodegenTest("aws.protocoltests.misc#MiscService", "misc"),
CodegenTest("com.amazonaws.ebs#Ebs", "ebs"),
CodegenTest("com.amazonaws.s3#AmazonS3", "s3"),
CodegenTest("com.aws.example#PokemonService", "pokemon_service_sdk")
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.rust.codegen.rustlang.RustReservedWordSymbolProvider
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenVisitor
import software.amazon.smithy.rust.codegen.smithy.BaseSymbolMetadataProvider
import software.amazon.smithy.rust.codegen.smithy.DefaultConfig
import software.amazon.smithy.rust.codegen.smithy.EventStreamSymbolProvider
Expand All @@ -23,12 +22,13 @@ import software.amazon.smithy.rust.codegen.smithy.customize.CombinedCodegenDecor
import java.util.logging.Level
import java.util.logging.Logger

/** Rust with Python bindings Codegen Plugin.
* This is the entrypoint for code generation, triggered by the smithy-build plugin.
* `resources/META-INF.services/software.amazon.smithy.build.SmithyBuildPlugin` refers to this class by name which
* enables the smithy-build plugin to invoke `execute` with all of the Smithy plugin context + models.
/**
* Rust with Python bindings Codegen Plugin.
* This is the entrypoint for code generation, triggered by the smithy-build plugin.
* `resources/META-INF.services/software.amazon.smithy.build.SmithyBuildPlugin` refers to this class by name which
* enables the smithy-build plugin to invoke `execute` with all of the Smithy plugin context + models.
*/
class RustCodegenServerPlugin : SmithyBuildPlugin {
class PythonCodegenServerPlugin : SmithyBuildPlugin {
private val logger = Logger.getLogger(javaClass.name)

override fun getName(): String = "rust-server-codegen-python"
Expand All @@ -43,9 +43,9 @@ class RustCodegenServerPlugin : SmithyBuildPlugin {
// - writer: The active RustWriter at the given location
val codegenDecorator = CombinedCodegenDecorator.fromClasspath(context)

// ServerCodegenVisitor is the main driver of code generation that traverses the model and generates code
logger.info("Loaded plugin to generate Rust/Python bindings for the server SSDK")
ServerCodegenVisitor(context, codegenDecorator).execute()
// PythonServerCodegenVisitor is the main driver of code generation that traverses the model and generates code
logger.info("Loaded plugin to generate Rust/Python bindings for the server SSDK for projection ${context.projectionName}")
PythonServerCodegenVisitor(context, codegenDecorator).execute()
}

companion object {
Expand All @@ -61,6 +61,9 @@ class RustCodegenServerPlugin : SmithyBuildPlugin {
symbolVisitorConfig: SymbolVisitorConfig = DefaultConfig
) =
SymbolVisitor(model, serviceShape = serviceShape, config = symbolVisitorConfig)
// Rename a set of symbols that do not implement `PyClass` and have been wrapped in
// `aws_smithy_http_server_python::types`.
.let { PythonServerSymbolProvider(it) }
// Generate different types for EventStream shapes (e.g. transcribe streaming)
.let {
EventStreamSymbolProvider(symbolVisitorConfig.runtimeConfig, it, model)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rust.codegen.server.python.smithy

import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.rustlang.CratesIo
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig

/**
* Object used *exclusively* in the runtime of the Python server, for separation concerns.
* Analogous to the companion object in [CargoDependency] and [ServerCargoDependency]; see its documentation for details.
* For a dependency that is used in the client, or in both the client and the server, use [CargoDependency] directly.
*/
object PythonServerCargoDependency {
val PyO3: CargoDependency = CargoDependency("pyo3", CratesIo("0.16"), features = setOf("extension-module"))
val PyO3Asyncio: CargoDependency = CargoDependency("pyo3-asyncio", CratesIo("0.16"), features = setOf("attributes", "tokio-runtime"))
val Tokio: CargoDependency = CargoDependency("tokio", CratesIo("1.0"), features = setOf("full"))
val Tracing: CargoDependency = CargoDependency("tracing", CratesIo("0.1"))
val Tower: CargoDependency = CargoDependency("tower", CratesIo("0.4"))
val TowerHttp: CargoDependency = CargoDependency("tower-http", CratesIo("0.3"), features = setOf("trace"))
val Hyper: CargoDependency = CargoDependency("hyper", CratesIo("0.14"), features = setOf("server", "http1", "http2", "tcp", "stream"))
val NumCpus: CargoDependency = CargoDependency("num_cpus", CratesIo("1.13"))

fun SmithyHttpServer(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("http-server")
fun SmithyHttpServerPython(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("http-server-python")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@

/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rust.codegen.server.python.smithy

import software.amazon.smithy.build.PluginContext
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.StringShape
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.traits.EnumTrait
import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerEnumGenerator
import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerServiceGenerator
import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerStructureGenerator
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenVisitor
import software.amazon.smithy.rust.codegen.server.smithy.protocols.ServerProtocolLoader
import software.amazon.smithy.rust.codegen.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.smithy.DefaultPublicModules
import software.amazon.smithy.rust.codegen.smithy.RustCrate
import software.amazon.smithy.rust.codegen.smithy.SymbolVisitorConfig
import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
import software.amazon.smithy.rust.codegen.smithy.generators.BuilderGenerator
import software.amazon.smithy.rust.codegen.smithy.generators.CodegenTarget
import software.amazon.smithy.rust.codegen.smithy.generators.implBlock
import software.amazon.smithy.rust.codegen.util.getTrait

/**
* Entrypoint for Python server-side code generation. This class will walk the in-memory model and
* generate all the needed types by calling the accept() function on the available shapes.
*
* This class inherits from [ServerCodegenVisitor] since it uses most of the functionlities of the super class
* and have to override the symbol provider with [PythonServerSymbolProvider].
*/
class PythonServerCodegenVisitor(context: PluginContext, codegenDecorator: RustCodegenDecorator) :
ServerCodegenVisitor(context, codegenDecorator) {

init {
val symbolVisitorConfig =
SymbolVisitorConfig(
runtimeConfig = settings.runtimeConfig,
codegenConfig = settings.codegenConfig,
handleRequired = true
)
val baseModel = baselineTransform(context.model)
val service = settings.getService(baseModel)
val (protocol, generator) =
ServerProtocolLoader(
codegenDecorator.protocols(
service.id,
ServerProtocolLoader.DefaultProtocols
)
)
.protocolFor(context.model, service)
protocolGeneratorFactory = generator
model = generator.transformModel(codegenDecorator.transformModel(service, baseModel))
val baseProvider = PythonCodegenServerPlugin.baseSymbolProvider(model, service, symbolVisitorConfig)
// Override symbolProvider.
symbolProvider =
codegenDecorator.symbolProvider(generator.symbolProvider(model, baseProvider))

// Override `codegenContext` which carries the symbolProvider.
codegenContext = CodegenContext(model, symbolProvider, service, protocol, settings, target = CodegenTarget.SERVER)

// Override `rustCrate` which carries the symbolProvider.
rustCrate = RustCrate(context.fileManifest, symbolProvider, DefaultPublicModules, settings.codegenConfig)
// Override `protocolGenerator` which carries the symbolProvider.
protocolGenerator = protocolGeneratorFactory.buildProtocolGenerator(codegenContext)
}

/**
* Structure Shape Visitor
*
* For each structure shape, generate:
* - A Rust structure for the shape ([StructureGenerator]).
* - `pyo3::PyClass` trait implementation.
* - A builder for the shape.
*
* This function _does not_ generate any serializers.
*/
override fun structureShape(shape: StructureShape) {
logger.info("[python-server-codegen] Generating a structure $shape")
rustCrate.useShapeWriter(shape) { writer ->
// Use Python specific structure generator that adds the #[pyclass] attribute
// and #[pymethods] implementation.
PythonServerStructureGenerator(model, symbolProvider, writer, shape).render(CodegenTarget.SERVER)
val builderGenerator =
BuilderGenerator(codegenContext.model, codegenContext.symbolProvider, shape)
builderGenerator.render(writer)
writer.implBlock(shape, symbolProvider) {
builderGenerator.renderConvenienceMethod(this)
}
}
}

/**
* String Shape Visitor
*
* Although raw strings require no code generation, enums are actually [EnumTrait] applied to string shapes.
*/
override fun stringShape(shape: StringShape) {
logger.info("[rust-server-codegen] Generating an enum $shape")
shape.getTrait<EnumTrait>()?.also { enum ->
rustCrate.useShapeWriter(shape) { writer ->
PythonServerEnumGenerator(model, symbolProvider, writer, shape, enum, codegenContext.runtimeConfig).render()
}
}
}

/**
* Generate service-specific code for the model:
* - Serializers
* - Deserializers
* - Trait implementations
* - Protocol tests
* - Operation structures
* - Python operation handlers
*/
override fun serviceShape(shape: ServiceShape) {
logger.info("[python-server-codegen] Generating a service $shape")
PythonServerServiceGenerator(
rustCrate,
protocolGenerator,
protocolGeneratorFactory.support(),
protocolGeneratorFactory.protocol(codegenContext).httpBindingResolver,
codegenContext,
)
.render()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rust.codegen.server.python.smithy

import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.smithy.RuntimeType

/**
* Object used *exclusively* in the runtime of the Python server, for separation concerns.
* Analogous to the companion object in [RuntimeType] and [ServerRuntimeType]; see its documentation for details.
* For a runtime type that is used in the client, or in both the client and the server, use [RuntimeType] directly.
*/
object PythonServerRuntimeType {

fun SharedSocket(runtimeConfig: RuntimeConfig) =
RuntimeType("SharedSocket", PythonServerCargoDependency.SmithyHttpServerPython(runtimeConfig), "${runtimeConfig.crateSrcPrefix}_http_server_python")

fun Blob(runtimeConfig: RuntimeConfig) =
RuntimeType("Blob", PythonServerCargoDependency.SmithyHttpServerPython(runtimeConfig), "${runtimeConfig.crateSrcPrefix}_http_server_python::types")

fun PyError(runtimeConfig: RuntimeConfig) =
RuntimeType("Error", PythonServerCargoDependency.SmithyHttpServerPython(runtimeConfig), "${runtimeConfig.crateSrcPrefix}_http_server_python")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rust.codegen.server.python.smithy

import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider
import software.amazon.smithy.rust.codegen.smithy.WrappingSymbolProvider
import software.amazon.smithy.rust.codegen.smithy.rustType

/**
* Input / output / error structures can refer to complex types like the ones implemented inside
* `aws_smithy_types` (a good example is `aws_smithy_types::Blob`).
* `aws_smithy_http_server_python::types` wraps those types that do not implement directly the
* `pyo3::PyClass` trait and cannot be shared safely with Python, providing an idiomatic Python / Rust API.
*
* This symbol provider ensures types not implementing `pyo3::PyClass` are swapped with their wrappers from
* `aws_smithy_http_server_python::types`.
*/
class PythonServerSymbolProvider(private val base: RustSymbolProvider) :
WrappingSymbolProvider(base) {

private val runtimeConfig = config().runtimeConfig

/**
* Convert a shape to a Symbol.
*
* Swap the shape's symbol if its associated type does not implement `pyo3::PyClass`.
*/
override fun toSymbol(shape: Shape): Symbol {
return when (base.toSymbol(shape).rustType()) {
RuntimeType.Blob(runtimeConfig).toSymbol().rustType() -> {
PythonServerRuntimeType.Blob(runtimeConfig).toSymbol()
}
else -> {
base.toSymbol(shape)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rust.codegen.server.python.smithy.customizations

import software.amazon.smithy.rust.codegen.rustlang.Writable
import software.amazon.smithy.rust.codegen.rustlang.docs
import software.amazon.smithy.rust.codegen.rustlang.rust
import software.amazon.smithy.rust.codegen.rustlang.rustBlock
import software.amazon.smithy.rust.codegen.rustlang.writable
import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerRuntimeType
import software.amazon.smithy.rust.codegen.server.smithy.customizations.AddInternalServerErrorToAllOperationsDecorator
import software.amazon.smithy.rust.codegen.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.smithy.customize.CombinedCodegenDecorator
import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
import software.amazon.smithy.rust.codegen.smithy.generators.LibRsCustomization
import software.amazon.smithy.rust.codegen.smithy.generators.LibRsSection
import software.amazon.smithy.rust.codegen.smithy.generators.ManifestCustomizations

/**
* Configure the [lib] section of `Cargo.toml`.
*
* [lib]
* name = "$CRATE_NAME"
* crate-type = ["cdylib"]
*/
class CdylibManifestDecorator : RustCodegenDecorator {
override val name: String = "CdylibDecorator"
override val order: Byte = 0

override fun crateManifestCustomizations(
codegenContext: CodegenContext
): ManifestCustomizations =
mapOf("lib" to mapOf("name" to codegenContext.settings.moduleName, "crate-type" to listOf("cdylib")))
}

/**
* Add `pub use aws_smithy_http_server_python::types::$TYPE` to lib.rs.
*/
class PubUsePythonTypes(private val runtimeConfig: RuntimeConfig) : LibRsCustomization() {
override fun section(section: LibRsSection): Writable {
return when (section) {
is LibRsSection.Body -> writable {
docs("Re-exported Python types from supporting crates.")
rustBlock("pub mod python_types") {
rust("pub use #T;", PythonServerRuntimeType.Blob(runtimeConfig).toSymbol())
}
}
else -> emptySection
}
}
}

/**
* Decorator applying the customization from [PubUsePythonTypes] class.
*/
class PubUsePythonTypesDecorator : RustCodegenDecorator {
override val name: String = "PubUsePythonTypesDecorator"
override val order: Byte = 0

override fun libRsCustomizations(
codegenContext: CodegenContext,
baseCustomizations: List<LibRsCustomization>
): List<LibRsCustomization> {
return baseCustomizations + PubUsePythonTypes(codegenContext.runtimeConfig)
}
}

val DECORATORS = listOf(
/**
* Add the [InternalServerError] error to all operations.
* This is done because the Python interpreter can raise exceptions during execution
*/
AddInternalServerErrorToAllOperationsDecorator(),
// Add the [lib] section to Cargo.toml to configure the generation of the shared library:
CdylibManifestDecorator(),
// Add `pub use` of `aws_smithy_http_server_python::types`.
PubUsePythonTypesDecorator()
)

// Combined codegen decorator for Python services.
class PythonServerCodegenDecorator : CombinedCodegenDecorator(DECORATORS) {
override val name: String = "PythonServerCodegenDecorator"
override val order: Byte = -1
}
Loading

0 comments on commit 8e84ee2

Please sign in to comment.