Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Codegenerate Python application example and add explicit cast during JSON deserialization #1520

Merged
merged 10 commits into from
Jul 26, 2022
8 changes: 8 additions & 0 deletions CHANGELOG.next.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ message = "Re-export aws_types::SdkConfig in aws_config"
references = ["smithy-rs#1457"]
meta = { "breaking" = false, "tada" = true, "bug" = false }
author = "calavera"

[[smithy-rs]]
message = """
Codegenerate Python application example and add explicit cast during JSON deserialization.
"""
references = ["smithy-rs#1520"]
meta = { "breaking" = false, "tada" = false, "bug" = false, "sdk" = "both" }
author = "crisidev"
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@
package software.amazon.smithy.rust.codegen.server.python.smithy.generators

import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.traits.DocumentationTrait
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.asType
import software.amazon.smithy.rust.codegen.rustlang.rust
import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCargoDependency
import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency
import software.amazon.smithy.rust.codegen.smithy.CoreCodegenContext
import software.amazon.smithy.rust.codegen.smithy.Errors
import software.amazon.smithy.rust.codegen.smithy.Inputs
import software.amazon.smithy.rust.codegen.smithy.Outputs
import software.amazon.smithy.rust.codegen.util.getTrait
import software.amazon.smithy.rust.codegen.util.inputShape
import software.amazon.smithy.rust.codegen.util.outputShape
import software.amazon.smithy.rust.codegen.util.toSnakeCase

/**
Expand Down Expand Up @@ -56,8 +64,10 @@ class PythonApplicationGenerator(
coreCodegenContext: CoreCodegenContext,
private val operations: List<OperationShape>,
) {
private val crateName = coreCodegenContext.settings.moduleName
private val symbolProvider = coreCodegenContext.symbolProvider
private val runtimeConfig = coreCodegenContext.runtimeConfig
private val model = coreCodegenContext.model
private val codegenScope =
arrayOf(
"SmithyPython" to PythonServerCargoDependency.SmithyHttpServerPython(runtimeConfig).asType(),
Expand All @@ -73,10 +83,9 @@ class PythonApplicationGenerator(
)

fun render(writer: RustWriter) {
renderPyApplicationRustDocs(writer)
writer.rustTemplate(
"""
/// Main Python application, used to register operations and context and start multiple
/// workers on the same shared socket.
##[#{pyo3}::pyclass]
##[derive(Debug, Clone)]
pub struct App {
Expand Down Expand Up @@ -127,7 +136,7 @@ class PythonApplicationGenerator(
)
rustBlockTemplate(
"""
/// Dynamically codegenerate the routes, allowing to build the Smithy [Router].
/// Dynamically codegenerate the routes, allowing to build the Smithy [#{SmithyServer}::Router].
pub fn build_router(&mut self, py: #{pyo3}::Python) -> #{pyo3}::PyResult<()>
""",
*codegenScope
Expand Down Expand Up @@ -179,4 +188,69 @@ class PythonApplicationGenerator(
}
}
}

private fun renderPyApplicationRustDocs(writer: RustWriter) {
writer.rust(
"""
##[allow(clippy::tabs_in_doc_comments)]
/// Main Python application, used to register operations and context and start multiple
/// workers on the same shared socket.
///
/// Operations can be registrered using the application object as a decorator (`@app.operation_name`).
///
/// Here's a full example to get you started:
///
/// ```python
${ if (operations.any { it.errors.isNotEmpty() }) {
"""/// from $crateName import ${Inputs.namespace}
/// from $crateName import ${Outputs.namespace}
/// from $crateName import ${Errors.namespace}"""
} else {
"""/// from $crateName import ${Inputs.namespace}
/// from $crateName import ${Outputs.namespace}"""
} }
/// from $crateName import App
///
/// @dataclass
/// class Context:
/// counter: int = 0
///
/// app = App()
/// app.context(Context())
///
${operationImplementationStubs(operations)}
///
/// app.run()
/// ```
///
/// Any of operations above can be written as well prepending the `async` keyword and
/// the Python application will automatically handle it and schedule it on the event loop for you.
"""
)
}

private fun operationImplementationStubs(operations: List<OperationShape>): String =
operations.joinToString("\n///\n") {
val operationDocumentation = it.getTrait<DocumentationTrait>()?.value
val ret = if (!operationDocumentation.isNullOrBlank()) {
operationDocumentation.replace("#", "##").prependIndent("/// ## ") + "\n"
} else ""
ret +
"""
/// ${it.signature()}:
/// raise NotImplementedError
""".trimIndent()
}

/**
* Returns the function signature for an operation handler implementation. Used in the documentation.
*/
private fun OperationShape.signature(): String {
val inputSymbol = symbolProvider.toSymbol(inputShape(model))
val outputSymbol = symbolProvider.toSymbol(outputShape(model))
val inputT = "${Inputs.namespace}::${inputSymbol.name}"
val outputT = "${Outputs.namespace}::${outputSymbol.name}"
val operationName = symbolProvider.toSymbol(this).name.toSnakeCase()
return "@app.$operationName\n/// def $operationName(input: $inputT, ctx: Context) -> $outputT"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class JsonParserGenerator(
private val target = coreCodegenContext.target
private val smithyJson = CargoDependency.smithyJson(runtimeConfig).asType()
private val jsonDeserModule = RustModule.private("json_deser")
private val util = ParserUtil(symbolProvider, runtimeConfig)
private val codegenScope = arrayOf(
"Error" to smithyJson.member("deserialize::Error"),
"ErrorReason" to smithyJson.member("deserialize::ErrorReason"),
Expand Down Expand Up @@ -221,8 +222,8 @@ class JsonParserGenerator(
is StringShape -> deserializeString(target)
is BooleanShape -> rustTemplate("#{expect_bool_or_null}(tokens.next())?", *codegenScope)
is NumberShape -> deserializeNumber(target)
is BlobShape -> rustTemplate("#{expect_blob_or_null}(tokens.next())?", *codegenScope)
is TimestampShape -> deserializeTimestamp(memberShape)
is BlobShape -> deserializeBlob(target)
is TimestampShape -> deserializeTimestamp(target, memberShape)
is CollectionShape -> deserializeCollection(target)
is MapShape -> deserializeMap(target)
is StructureShape -> deserializeStruct(target)
Expand All @@ -236,6 +237,14 @@ class JsonParserGenerator(
}
}

private fun RustWriter.deserializeBlob(target: BlobShape) {
rustTemplate(
"#{expect_blob_or_null}(tokens.next())?#{ConvertFrom:W}",
"ConvertFrom" to util.convertViaFrom(target),
*codegenScope
)
}

private fun RustWriter.deserializeStringInner(target: StringShape, escapedStrName: String) {
withBlock("$escapedStrName.to_unescaped().map(|u|", ")") {
when (target.hasTrait<EnumTrait>()) {
Expand Down Expand Up @@ -266,14 +275,17 @@ class JsonParserGenerator(
rustTemplate("#{expect_number_or_null}(tokens.next())?.map(|v| v.to_#{T}())", "T" to symbol, *codegenScope)
}

private fun RustWriter.deserializeTimestamp(member: MemberShape) {
private fun RustWriter.deserializeTimestamp(shape: TimestampShape, member: MemberShape) {
val timestampFormat =
httpBindingResolver.timestampFormat(
member, HttpLocation.DOCUMENT,
TimestampFormatTrait.Format.EPOCH_SECONDS
)
val timestampFormatType = RuntimeType.TimestampFormat(runtimeConfig, timestampFormat)
rustTemplate("#{expect_timestamp_or_null}(tokens.next(), #{T})?", "T" to timestampFormatType, *codegenScope)
rustTemplate(
"#{expect_timestamp_or_null}(tokens.next(), #{T})?#{ConvertFrom:W}",
"T" to timestampFormatType, "ConvertFrom" to util.convertViaFrom(shape), *codegenScope
)
}

private fun RustWriter.deserializeCollection(shape: CollectionShape) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rust.codegen.smithy.protocols.parse

import software.amazon.smithy.model.shapes.BlobShape
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.shapes.TimestampShape
import software.amazon.smithy.rust.codegen.rustlang.Writable
import software.amazon.smithy.rust.codegen.rustlang.rust
import software.amazon.smithy.rust.codegen.rustlang.writable
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider
import software.amazon.smithy.rust.codegen.smithy.rustType

/*
* Utility class used to force casting a non primitive type into one overriden by a new symbol provider,
* by explicitly calling `from()`.
*
* For example we use this in the server Python implementation, where we override types like [Blob] and [DateTime]
* with wrappers compatibile with Python, without touching the original implementation coming from `aws-smithy-types`.
crisidev marked this conversation as resolved.
Show resolved Hide resolved
*/
class ParserUtil(private val symbolProvider: RustSymbolProvider, private val runtimeConfig: RuntimeConfig) {
crisidev marked this conversation as resolved.
Show resolved Hide resolved
fun convertViaFrom(shape: Shape): Writable =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How this is written now assumes the conversion will always be a .something() suffix, which may not always be the case. I think this would be a lot more flexible if it took a String or Writable as an argument that represents the value/expression being converted, and then places that in the correct place itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I understand properly what you are suggesting here...

What do you mean with expect .something() to always exists?

I thought that passing a shape and adding the suffix to it in case it differs from the original shape was the most general way of achieving this.

Can you expand a little on your proposal?

writable {
val oldSymbol = when (shape) {
// TODO(understand what needs to be done for ByteStream)
is BlobShape -> RuntimeType.Blob(runtimeConfig).toSymbol()
is TimestampShape -> RuntimeType.DateTime(runtimeConfig).toSymbol()
else -> symbolProvider.toSymbol(shape)
}
val newSymbol = symbolProvider.toSymbol(shape)
if (oldSymbol.rustType() != newSymbol.rustType()) {
rust(".map($newSymbol::from)")
}
}
}