Skip to content

Commit c7b97dc

Browse files
crisidevjdisanti
andauthored
Codegenerate Python application example and add explicit cast during JSON deserialization (smithy-lang#1520)
Co-authored-by: John DiSanti <[email protected]>
1 parent 2ad87c0 commit c7b97dc

File tree

4 files changed

+141
-7
lines changed

4 files changed

+141
-7
lines changed

CHANGELOG.next.toml

+8
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,11 @@ message = "SDK crate READMEs now include an example of creating a client"
205205
references = ["smithy-rs#1571", "smithy-rs#1385"]
206206
meta = { "breaking" = false, "tada" = true, "bug" = false }
207207
author = "jdisanti"
208+
209+
[[smithy-rs]]
210+
message = """
211+
Add explicit cast during JSON deserialization in case of custom Symbol providers.
212+
"""
213+
references = ["smithy-rs#1520"]
214+
meta = { "breaking" = false, "tada" = false, "bug" = false }
215+
author = "crisidev"

codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt

+77-3
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,21 @@
66
package software.amazon.smithy.rust.codegen.server.python.smithy.generators
77

88
import software.amazon.smithy.model.shapes.OperationShape
9+
import software.amazon.smithy.model.traits.DocumentationTrait
910
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
1011
import software.amazon.smithy.rust.codegen.rustlang.asType
12+
import software.amazon.smithy.rust.codegen.rustlang.rust
1113
import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate
1214
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
1315
import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCargoDependency
1416
import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency
1517
import software.amazon.smithy.rust.codegen.smithy.CoreCodegenContext
18+
import software.amazon.smithy.rust.codegen.smithy.Errors
19+
import software.amazon.smithy.rust.codegen.smithy.Inputs
20+
import software.amazon.smithy.rust.codegen.smithy.Outputs
21+
import software.amazon.smithy.rust.codegen.util.getTrait
22+
import software.amazon.smithy.rust.codegen.util.inputShape
23+
import software.amazon.smithy.rust.codegen.util.outputShape
1624
import software.amazon.smithy.rust.codegen.util.toSnakeCase
1725

1826
/**
@@ -56,8 +64,10 @@ class PythonApplicationGenerator(
5664
coreCodegenContext: CoreCodegenContext,
5765
private val operations: List<OperationShape>,
5866
) {
67+
private val crateName = coreCodegenContext.settings.moduleName
5968
private val symbolProvider = coreCodegenContext.symbolProvider
6069
private val runtimeConfig = coreCodegenContext.runtimeConfig
70+
private val model = coreCodegenContext.model
6171
private val codegenScope =
6272
arrayOf(
6373
"SmithyPython" to PythonServerCargoDependency.SmithyHttpServerPython(runtimeConfig).asType(),
@@ -73,10 +83,9 @@ class PythonApplicationGenerator(
7383
)
7484

7585
fun render(writer: RustWriter) {
86+
renderPyApplicationRustDocs(writer)
7687
writer.rustTemplate(
7788
"""
78-
/// Main Python application, used to register operations and context and start multiple
79-
/// workers on the same shared socket.
8089
##[#{pyo3}::pyclass]
8190
##[derive(Debug, Clone)]
8291
pub struct App {
@@ -127,7 +136,7 @@ class PythonApplicationGenerator(
127136
)
128137
rustBlockTemplate(
129138
"""
130-
/// Dynamically codegenerate the routes, allowing to build the Smithy [Router].
139+
/// Dynamically codegenerate the routes, allowing to build the Smithy [#{SmithyServer}::Router].
131140
pub fn build_router(&mut self, py: #{pyo3}::Python) -> #{pyo3}::PyResult<()>
132141
""",
133142
*codegenScope
@@ -179,4 +188,69 @@ class PythonApplicationGenerator(
179188
}
180189
}
181190
}
191+
192+
private fun renderPyApplicationRustDocs(writer: RustWriter) {
193+
writer.rust(
194+
"""
195+
##[allow(clippy::tabs_in_doc_comments)]
196+
/// Main Python application, used to register operations and context and start multiple
197+
/// workers on the same shared socket.
198+
///
199+
/// Operations can be registrered using the application object as a decorator (`@app.operation_name`).
200+
///
201+
/// Here's a full example to get you started:
202+
///
203+
/// ```python
204+
${ if (operations.any { it.errors.isNotEmpty() }) {
205+
"""/// from $crateName import ${Inputs.namespace}
206+
/// from $crateName import ${Outputs.namespace}
207+
/// from $crateName import ${Errors.namespace}"""
208+
} else {
209+
"""/// from $crateName import ${Inputs.namespace}
210+
/// from $crateName import ${Outputs.namespace}"""
211+
} }
212+
/// from $crateName import App
213+
///
214+
/// @dataclass
215+
/// class Context:
216+
/// counter: int = 0
217+
///
218+
/// app = App()
219+
/// app.context(Context())
220+
///
221+
${operationImplementationStubs(operations)}
222+
///
223+
/// app.run()
224+
/// ```
225+
///
226+
/// Any of operations above can be written as well prepending the `async` keyword and
227+
/// the Python application will automatically handle it and schedule it on the event loop for you.
228+
"""
229+
)
230+
}
231+
232+
private fun operationImplementationStubs(operations: List<OperationShape>): String =
233+
operations.joinToString("\n///\n") {
234+
val operationDocumentation = it.getTrait<DocumentationTrait>()?.value
235+
val ret = if (!operationDocumentation.isNullOrBlank()) {
236+
operationDocumentation.replace("#", "##").prependIndent("/// ## ") + "\n"
237+
} else ""
238+
ret +
239+
"""
240+
/// ${it.signature()}:
241+
/// raise NotImplementedError
242+
""".trimIndent()
243+
}
244+
245+
/**
246+
* Returns the function signature for an operation handler implementation. Used in the documentation.
247+
*/
248+
private fun OperationShape.signature(): String {
249+
val inputSymbol = symbolProvider.toSymbol(inputShape(model))
250+
val outputSymbol = symbolProvider.toSymbol(outputShape(model))
251+
val inputT = "${Inputs.namespace}::${inputSymbol.name}"
252+
val outputT = "${Outputs.namespace}::${outputSymbol.name}"
253+
val operationName = symbolProvider.toSymbol(this).name.toSnakeCase()
254+
return "@app.$operationName\n/// def $operationName(input: $inputT, ctx: Context) -> $outputT"
255+
}
182256
}

codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt

+16-4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class JsonParserGenerator(
6565
private val target = coreCodegenContext.target
6666
private val smithyJson = CargoDependency.smithyJson(runtimeConfig).asType()
6767
private val jsonDeserModule = RustModule.private("json_deser")
68+
private val typeConversionGenerator = TypeConversionGenerator(symbolProvider, runtimeConfig)
6869
private val codegenScope = arrayOf(
6970
"Error" to smithyJson.member("deserialize::Error"),
7071
"ErrorReason" to smithyJson.member("deserialize::ErrorReason"),
@@ -221,8 +222,8 @@ class JsonParserGenerator(
221222
is StringShape -> deserializeString(target)
222223
is BooleanShape -> rustTemplate("#{expect_bool_or_null}(tokens.next())?", *codegenScope)
223224
is NumberShape -> deserializeNumber(target)
224-
is BlobShape -> rustTemplate("#{expect_blob_or_null}(tokens.next())?", *codegenScope)
225-
is TimestampShape -> deserializeTimestamp(memberShape)
225+
is BlobShape -> deserializeBlob(target)
226+
is TimestampShape -> deserializeTimestamp(target, memberShape)
226227
is CollectionShape -> deserializeCollection(target)
227228
is MapShape -> deserializeMap(target)
228229
is StructureShape -> deserializeStruct(target)
@@ -236,6 +237,14 @@ class JsonParserGenerator(
236237
}
237238
}
238239

240+
private fun RustWriter.deserializeBlob(target: BlobShape) {
241+
rustTemplate(
242+
"#{expect_blob_or_null}(tokens.next())?#{ConvertFrom:W}",
243+
"ConvertFrom" to typeConversionGenerator.convertViaFrom(target),
244+
*codegenScope
245+
)
246+
}
247+
239248
private fun RustWriter.deserializeStringInner(target: StringShape, escapedStrName: String) {
240249
withBlock("$escapedStrName.to_unescaped().map(|u|", ")") {
241250
when (target.hasTrait<EnumTrait>()) {
@@ -266,14 +275,17 @@ class JsonParserGenerator(
266275
rustTemplate("#{expect_number_or_null}(tokens.next())?.map(|v| v.to_#{T}())", "T" to symbol, *codegenScope)
267276
}
268277

269-
private fun RustWriter.deserializeTimestamp(member: MemberShape) {
278+
private fun RustWriter.deserializeTimestamp(shape: TimestampShape, member: MemberShape) {
270279
val timestampFormat =
271280
httpBindingResolver.timestampFormat(
272281
member, HttpLocation.DOCUMENT,
273282
TimestampFormatTrait.Format.EPOCH_SECONDS
274283
)
275284
val timestampFormatType = RuntimeType.TimestampFormat(runtimeConfig, timestampFormat)
276-
rustTemplate("#{expect_timestamp_or_null}(tokens.next(), #{T})?", "T" to timestampFormatType, *codegenScope)
285+
rustTemplate(
286+
"#{expect_timestamp_or_null}(tokens.next(), #{T})?#{ConvertFrom:W}",
287+
"T" to timestampFormatType, "ConvertFrom" to typeConversionGenerator.convertViaFrom(shape), *codegenScope
288+
)
277289
}
278290

279291
private fun RustWriter.deserializeCollection(shape: CollectionShape) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.rust.codegen.smithy.protocols.parse
7+
8+
import software.amazon.smithy.model.shapes.BlobShape
9+
import software.amazon.smithy.model.shapes.Shape
10+
import software.amazon.smithy.model.shapes.TimestampShape
11+
import software.amazon.smithy.rust.codegen.rustlang.Writable
12+
import software.amazon.smithy.rust.codegen.rustlang.rust
13+
import software.amazon.smithy.rust.codegen.rustlang.writable
14+
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
15+
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
16+
import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider
17+
import software.amazon.smithy.rust.codegen.smithy.rustType
18+
19+
/*
20+
* Utility class used to force casting a non primitive type into one overriden by a new symbol provider,
21+
* by explicitly calling `from()`.
22+
*
23+
* For example we use this in the server Python implementation, where we override types like [Blob] and [DateTime]
24+
* with wrappers compatible with Python, without touching the original implementation coming from `aws-smithy-types`.
25+
*/
26+
class TypeConversionGenerator(private val symbolProvider: RustSymbolProvider, private val runtimeConfig: RuntimeConfig) {
27+
fun convertViaFrom(shape: Shape): Writable =
28+
writable {
29+
val oldSymbol = when (shape) {
30+
// TODO(understand what needs to be done for ByteStream)
31+
is BlobShape -> RuntimeType.Blob(runtimeConfig).toSymbol()
32+
is TimestampShape -> RuntimeType.DateTime(runtimeConfig).toSymbol()
33+
else -> symbolProvider.toSymbol(shape)
34+
}
35+
val newSymbol = symbolProvider.toSymbol(shape)
36+
if (oldSymbol.rustType() != newSymbol.rustType()) {
37+
rust(".map($newSymbol::from)")
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)