diff --git a/aws/sdk/models/apigateway.json b/aws/sdk/models/apigateway.json index 8fdb8e3619..09fd218ffc 100644 --- a/aws/sdk/models/apigateway.json +++ b/aws/sdk/models/apigateway.json @@ -5251,7 +5251,8 @@ "parameters": { "target": "com.amazonaws.apigateway#MapOfStringToString", "traits": { - "smithy.api#documentation": "

A key-value map of query string parameters that specify properties of the export, depending on the requested exportType. For exportType oas30 and swagger, any combination of the following parameters are supported: extensions='integrations' or extensions='apigateway' will export the API with x-amazon-apigateway-integration extensions. extensions='authorizers' will export the API with x-amazon-apigateway-authorizer extensions. postman will export the API with Postman extensions, allowing for import to the Postman tool

" + "smithy.api#documentation": "

A key-value map of query string parameters that specify properties of the export, depending on the requested exportType. For exportType oas30 and swagger, any combination of the following parameters are supported: extensions='integrations' or extensions='apigateway' will export the API with x-amazon-apigateway-integration extensions. extensions='authorizers' will export the API with x-amazon-apigateway-authorizer extensions. postman will export the API with Postman extensions, allowing for import to the Postman tool

", + "smithy.api#httpQueryParams": {} } }, "accepts": { @@ -6226,7 +6227,8 @@ "parameters": { "target": "com.amazonaws.apigateway#MapOfStringToString", "traits": { - "smithy.api#documentation": "

A string-to-string key-value map of query parameters sdkType-dependent properties of the SDK. For sdkType of objectivec or swift, a parameter named classPrefix is required. For sdkType of android, parameters named groupId, artifactId, artifactVersion, and invokerPackage are required. For sdkType of java, parameters named serviceName and javaPackageName are required.

" + "smithy.api#documentation": "

A string-to-string key-value map of query parameters sdkType-dependent properties of the SDK. For sdkType of objectivec or swift, a parameter named classPrefix is required. For sdkType of android, parameters named groupId, artifactId, artifactVersion, and invokerPackage are required. For sdkType of java, parameters named serviceName and javaPackageName are required.

", + "smithy.api#httpQueryParams": {} } } }, @@ -6965,6 +6967,14 @@ "com.amazonaws.apigateway#ImportApiKeysRequest": { "type": "structure", "members": { + "body": { + "target": "com.amazonaws.apigateway#Blob", + "traits": { + "smithy.api#documentation": "

The payload of the POST request to import API keys. For the payload format, see API Key File Format.

", + "smithy.api#httpPayload": {}, + "smithy.api#required": {} + } + }, "format": { "target": "com.amazonaws.apigateway#ApiKeysFormat", "traits": { @@ -7042,6 +7052,14 @@ "smithy.api#documentation": "

A query parameter to specify whether to rollback the documentation importation (true) or not (false) when a warning is encountered. The default value is false.

", "smithy.api#httpQuery": "failonwarnings" } + }, + "body": { + "target": "com.amazonaws.apigateway#Blob", + "traits": { + "smithy.api#documentation": "

[Required] Raw byte array representing the to-be-imported documentation parts. To import from an OpenAPI file, this is a JSON object.

", + "smithy.api#httpPayload": {}, + "smithy.api#required": {} + } } }, "traits": { @@ -7095,7 +7113,16 @@ "parameters": { "target": "com.amazonaws.apigateway#MapOfStringToString", "traits": { - "smithy.api#documentation": "

A key-value map of context-specific query string parameters specifying the behavior of different API importing operations. The following shows operation-specific parameters and their supported values.

\n

To exclude DocumentationParts from the import, set parameters as ignore=documentation.

\n

To configure the endpoint type, set parameters as endpointConfigurationTypes=EDGE, endpointConfigurationTypes=REGIONAL, or endpointConfigurationTypes=PRIVATE. The default endpoint type is EDGE.

\n

To handle imported basepath, set parameters as basepath=ignore, basepath=prepend or basepath=split.

\n

For example, the AWS CLI command to exclude documentation from the imported API is:

\n
aws apigateway import-rest-api --parameters ignore=documentation --body 'file:///path/to/imported-api-body.json'
\n

The AWS CLI command to set the regional endpoint on the imported API is:

\n
aws apigateway import-rest-api --parameters endpointConfigurationTypes=REGIONAL --body 'file:///path/to/imported-api-body.json'
" + "smithy.api#documentation": "

A key-value map of context-specific query string parameters specifying the behavior of different API importing operations. The following shows operation-specific parameters and their supported values.

\n

To exclude DocumentationParts from the import, set parameters as ignore=documentation.

\n

To configure the endpoint type, set parameters as endpointConfigurationTypes=EDGE, endpointConfigurationTypes=REGIONAL, or endpointConfigurationTypes=PRIVATE. The default endpoint type is EDGE.

\n

To handle imported basepath, set parameters as basepath=ignore, basepath=prepend or basepath=split.

\n

For example, the AWS CLI command to exclude documentation from the imported API is:

\n
aws apigateway import-rest-api --parameters ignore=documentation --body 'file:///path/to/imported-api-body.json'
\n

The AWS CLI command to set the regional endpoint on the imported API is:

\n
aws apigateway import-rest-api --parameters endpointConfigurationTypes=REGIONAL --body 'file:///path/to/imported-api-body.json'
", + "smithy.api#httpQueryParams": {} + } + }, + "body": { + "target": "com.amazonaws.apigateway#Blob", + "traits": { + "smithy.api#documentation": "

[Required] The POST request body containing external API definitions. Currently, only OpenAPI definition JSON/YAML files are supported. The maximum size of the API definition file is 6MB.

", + "smithy.api#httpPayload": {}, + "smithy.api#required": {} } } }, @@ -8569,7 +8596,16 @@ "parameters": { "target": "com.amazonaws.apigateway#MapOfStringToString", "traits": { - "smithy.api#documentation": "

Custom header parameters as part of the request. For example, to exclude DocumentationParts from an imported API, set ignore=documentation as a parameters value, as in the AWS CLI command of aws apigateway import-rest-api --parameters ignore=documentation --body 'file:///path/to/imported-api-body.json'.

" + "smithy.api#documentation": "

Custom header parameters as part of the request. For example, to exclude DocumentationParts from an imported API, set ignore=documentation as a parameters value, as in the AWS CLI command of aws apigateway import-rest-api --parameters ignore=documentation --body 'file:///path/to/imported-api-body.json'.

", + "smithy.api#httpQueryParams": {} + } + }, + "body": { + "target": "com.amazonaws.apigateway#Blob", + "traits": { + "smithy.api#documentation": "

[Required] The PUT request body containing external API definitions. Currently, only OpenAPI definition JSON/YAML files are supported. The maximum size of the API definition file is 6MB.

", + "smithy.api#httpPayload": {}, + "smithy.api#required": {} } } }, diff --git a/codegen-test/model/naming-obstacle-course.smithy b/codegen-test/model/naming-obstacle-course.smithy index 2425e50778..ae230993dc 100644 --- a/codegen-test/model/naming-obstacle-course.smithy +++ b/codegen-test/model/naming-obstacle-course.smithy @@ -48,11 +48,10 @@ structure ReservedWords { protocol: awsJson1_1, params: { "regular_string": "hello!", - "punned_string": { "ps_member": true }, }, method: "POST", uri: "/", - body: "{\"regular_string\": \"hello!\", \"punned_string\": { \"ps_member\": true }}", + body: "{\"regular_string\": \"hello!\"}", bodyMediaType: "application/json" } ]) @@ -62,7 +61,6 @@ operation StructureNamePunning { structure StructureNamePunningInput { regular_string: smithy.api#String, - punned_string: crate#String, punned_vec: Vec } @@ -70,9 +68,6 @@ structure Vec { pv_member: Boolean } -structure String { - ps_member: Boolean -} operation ErrCollisions { errors: [ diff --git a/codegen-test/model/rest-json-extras.smithy b/codegen-test/model/rest-json-extras.smithy index 565f557589..8c81d2dfe6 100644 --- a/codegen-test/model/rest-json-extras.smithy +++ b/codegen-test/model/rest-json-extras.smithy @@ -7,6 +7,46 @@ use aws.api#service use smithy.test#httpRequestTests use smithy.test#httpResponseTests +apply QueryPrecedence @httpRequestTests([ + { + id: "UrlParamsKeyEncoding", + documentation: "Keys and values must be url encoded", + protocol: restJson1, + method: "POST", + uri: "/Precedence", + body: "", + queryParams: ["bar=%26%F0%9F%90%B1", "hello%20there=how's%20your%20encoding?", "a%20%26%20b%20%26%20c=better%20encode%20%3D%20this"], + params: { + foo: "&🐱", + baz: { + "hello there": "how's your encoding?", + "a & b & c": "better encode = this" + } + }, + appliesTo: "client", + }, + { + id: "RestJsonQueryPrecedenceForbid", + documentation: "Prefer named query parameters when serializing", + protocol: restJson1, + method: "POST", + uri: "/Precedence", + body: "", + queryParams: [ + "bar=named", + "qux=alsoFromMap" + ], + forbidQueryParams: ["bar=fromMap"], + params: { + foo: "named", + baz: { + bar: "fromMap", + qux: "alsoFromMap" + } + }, + appliesTo: "client", + }] +) /// A REST JSON service that sends JSON requests and responses. @service(sdkId: "Rest Json Protocol") diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustWriter.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustWriter.kt index cff9a4ce36..8cf628e63d 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustWriter.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustWriter.kt @@ -283,7 +283,7 @@ class RustWriter private constructor( } } - fun ListForEach( + fun listForEach( target: Shape, outerField: String, block: CodeWriter.(field: String, target: ShapeId) -> Unit diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt index 321c42a0d3..5d5bf3b941 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/CodegenVisitor.kt @@ -59,7 +59,7 @@ class CodegenVisitor(context: PluginContext, private val codegenDecorator: RustC ).protocolFor(context.model, service) protocolGenerator = generator model = generator.transformModel(baseModel) - val baseProvider = RustCodegenPlugin.BaseSymbolProvider(model, symbolVisitorConfig) + val baseProvider = RustCodegenPlugin.baseSymbolProvider(model, service, symbolVisitorConfig) symbolProvider = codegenDecorator.symbolProvider(generator.symbolProvider(model, baseProvider)) protocolConfig = diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustCodegenPlugin.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustCodegenPlugin.kt index a215cf0ea6..b1f9382ac1 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustCodegenPlugin.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RustCodegenPlugin.kt @@ -8,6 +8,7 @@ package software.amazon.smithy.rust.codegen.smithy import software.amazon.smithy.build.PluginContext import software.amazon.smithy.build.SmithyBuildPlugin 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.smithy.customize.CombinedCodegenDecorator @@ -20,8 +21,8 @@ class RustCodegenPlugin : SmithyBuildPlugin { } companion object { - fun BaseSymbolProvider(model: Model, symbolVisitorConfig: SymbolVisitorConfig = DefaultConfig) = - SymbolVisitor(model, config = symbolVisitorConfig) + fun baseSymbolProvider(model: Model, serviceShape: ServiceShape, symbolVisitorConfig: SymbolVisitorConfig = DefaultConfig) = + SymbolVisitor(model, serviceShape = serviceShape, config = symbolVisitorConfig) .let { BaseSymbolMetadataProvider(it) } .let { RustReservedWordSymbolProvider(it) } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt index 11901d80af..7bad1b8696 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt @@ -122,6 +122,7 @@ interface RustSymbolProvider : SymbolProvider { class SymbolVisitor( private val model: Model, + private val serviceShape: ServiceShape?, private val config: SymbolVisitorConfig = DefaultConfig ) : RustSymbolProvider, ShapeVisitor { @@ -132,6 +133,14 @@ class SymbolVisitor( return shape.accept(this) } + private fun Shape.contextName(): String { + return if (serviceShape != null) { + id.getName(serviceShape) + } else { + id.name + } + } + override fun toMemberName(shape: MemberShape): String = shape.memberName.toSnakeCase() override fun blobShape(shape: BlobShape?): Symbol { @@ -173,7 +182,7 @@ class SymbolVisitor( override fun doubleShape(shape: DoubleShape): Symbol = simpleShape(shape) override fun stringShape(shape: StringShape): Symbol { return if (shape.hasTrait(EnumTrait::class.java)) { - symbolBuilder(shape, RustType.Opaque(shape.id.name)).locatedIn(Models).build() + symbolBuilder(shape, RustType.Opaque(shape.contextName())).locatedIn(Models).build() } else { simpleShape(shape) } @@ -218,7 +227,7 @@ class SymbolVisitor( } override fun operationShape(shape: OperationShape): Symbol { - return symbolBuilder(shape, RustType.Opaque(shape.id.name.capitalize())).locatedIn(Operations).build() + return symbolBuilder(shape, RustType.Opaque(shape.contextName().capitalize())).locatedIn(Operations).build() } override fun resourceShape(shape: ResourceShape?): Symbol { @@ -234,7 +243,7 @@ class SymbolVisitor( val isInput = shape.hasTrait(SyntheticInputTrait::class.java) val isOutput = shape.hasTrait(SyntheticOutputTrait::class.java) val isBody = shape.hasTrait(InputBodyTrait::class.java) || shape.hasTrait(OutputBodyTrait::class.java) - val name = StringUtils.capitalize(shape.id.name).letIf(isError && config.codegenConfig.renameExceptions) { + val name = StringUtils.capitalize(shape.contextName()).letIf(isError && config.codegenConfig.renameExceptions) { // TODO: Do we want to do this? // https://github.com/awslabs/smithy-rs/issues/77 it.replace("Exception", "Error") @@ -250,7 +259,7 @@ class SymbolVisitor( } override fun unionShape(shape: UnionShape): Symbol { - val name = StringUtils.capitalize(shape.id.name) + val name = StringUtils.capitalize(shape.contextName()) val builder = symbolBuilder(shape, RustType.Opaque(name)).locatedIn(Models) return builder.build() @@ -284,7 +293,6 @@ class SymbolVisitor( // TODO(chore): Move this to a useful place private const val RUST_TYPE_KEY = "rusttype" private const val SHAPE_KEY = "shape" -private const val CAN_USE_DEFAULT = "canusedefault" private const val SYMBOL_DEFAULT = "symboldefault" fun Symbol.Builder.rustType(rustType: RustType): Symbol.Builder { diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolGenerator.kt index 06bc3adf40..135c29869b 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/HttpProtocolGenerator.kt @@ -71,7 +71,8 @@ abstract class HttpProtocolGenerator( val inputShape = operationShape.inputShape(model) val sdkId = protocolConfig.serviceShape.getTrait(ServiceTrait::class.java) - .map { it.sdkId.toLowerCase().replace(" ", "") }.orElse(protocolConfig.serviceShape.id.name) + .map { it.sdkId.toLowerCase().replace(" ", "") } + .orElse(protocolConfig.serviceShape.id.getName(protocolConfig.serviceShape)) val builderGenerator = BuilderGenerator(model, symbolProvider, operationShape.inputShape(model)) builderGenerator.render(inputWriter) // impl OperationInputShape { ... } @@ -158,7 +159,12 @@ abstract class HttpProtocolGenerator( } } - private fun buildOperation(implBlockWriter: RustWriter, shape: OperationShape, features: List, sdkId: String) { + private fun buildOperation( + implBlockWriter: RustWriter, + shape: OperationShape, + features: List, + sdkId: String + ) { val runtimeConfig = protocolConfig.runtimeConfig val outputSymbol = symbolProvider.toSymbol(shape) val operationT = RuntimeType.operation(runtimeConfig) @@ -175,7 +181,10 @@ abstract class HttpProtocolGenerator( val mut = features.any { it.mutSelf() } val consumes = features.any { it.consumesSelf() } val self = "self".letIf(mut) { "mut $it" }.letIf(!consumes) { "&$it" } - implBlockWriter.rustBlock("pub fn make_operation($self, _config: &#T::Config) -> $returnType", RuntimeType.Config) { + implBlockWriter.rustBlock( + "pub fn make_operation($self, _config: &#T::Config) -> $returnType", + RuntimeType.Config + ) { withBlock("Ok({", "})") { features.forEach { it.section(OperationSection.MutateInput("self", "_config"))(this) } rust("let request = Self::assemble(self.request_builder_base()?, self.build_body());") @@ -192,7 +201,9 @@ abstract class HttpProtocolGenerator( let op = #1T::Operation::new( request, #2T::new() - ).with_metadata(#1T::Metadata::new(${shape.id.name.dq()}, ${sdkId.dq()})); + ).with_metadata(#1T::Metadata::new(${ + shape.id.getName(protocolConfig.serviceShape).dq() + }, ${sdkId.dq()})); """, operationModule, symbolProvider.toSymbol(shape) ) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/error/ErrorGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/error/ErrorGenerator.kt index cc491a4686..05126dbc57 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/error/ErrorGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/error/ErrorGenerator.kt @@ -91,6 +91,7 @@ class ErrorGenerator( writer.rustBlock("impl #T for ${symbol.name}", stdfmt.member("Display")) { rustBlock("fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result") { // If the error id and the Rust name don't match, print the actual error id for easy debugging + // Note: Exceptions cannot be renamed so it is OK to not call `getName(service)` here val errorDesc = symbol.name.letIf(symbol.name != shape.id.name) { symbolName -> "$symbolName [${shape.id.name}]" } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/error/TopLevelErrorGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/error/TopLevelErrorGenerator.kt index 054c834292..6798a43fce 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/error/TopLevelErrorGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/error/TopLevelErrorGenerator.kt @@ -38,9 +38,9 @@ class TopLevelErrorGenerator(protocolConfig: ProtocolConfig, private val operati private val symbolProvider = protocolConfig.symbolProvider private val model = protocolConfig.model - private val allErrors = operations.flatMap { it.errors }.distinctBy { it.name } + private val allErrors = operations.flatMap { it.errors }.distinctBy { it.getName(protocolConfig.serviceShape) } .map { protocolConfig.model.expectShape(it, StructureShape::class.java) } - .sortedBy { it.id.name } + .sortedBy { it.id.getName(protocolConfig.serviceShape) } private val sdkError = CargoDependency.SmithyHttp(protocolConfig.runtimeConfig).asType().member("result::SdkError") fun render(crate: RustCrate) { @@ -91,7 +91,10 @@ class TopLevelErrorGenerator(protocolConfig: ProtocolConfig, private val operati } private fun RustWriter.renderDefinition() { - RustMetadata(additionalAttributes = listOf(Attribute.NonExhaustive), public = true).withDerives(RuntimeType.Debug).render(this) + RustMetadata( + additionalAttributes = listOf(Attribute.NonExhaustive), + public = true + ).withDerives(RuntimeType.Debug).render(this) rustBlock("enum Error") { allErrors.forEach { error -> val sym = symbolProvider.toSymbol(error) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RequestBindingGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RequestBindingGenerator.kt index 3fa0f12989..a65e803940 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RequestBindingGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RequestBindingGenerator.kt @@ -28,7 +28,6 @@ import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.smithy.generators.operationBuildError import software.amazon.smithy.rust.codegen.smithy.generators.redactIfNecessary -import software.amazon.smithy.rust.codegen.smithy.rustType import software.amazon.smithy.rust.codegen.util.dq import software.amazon.smithy.rust.codegen.util.expectMember @@ -70,7 +69,6 @@ class RequestBindingGenerator( private val defaultTimestampFormat = TimestampFormatTrait.Format.EPOCH_SECONDS private val index = HttpBindingIndex.of(model) private val buildError = runtimeConfig.operationBuildError() - private val instant = RuntimeType.Instant(runtimeConfig).toSymbol().rustType() /** * Generates `update_http_builder` and all necessary dependency functions into the impl block provided by @@ -152,7 +150,13 @@ class RequestBindingGenerator( let header_value = http::header::HeaderValue::try_from(header_value).map_err(|err| { #{build_error}::InvalidField { field: ${memberName.dq()}, - details: format!("`{}` cannot be used as a header value: {}", ${redactIfNecessary(memberShape, model,"v")}, err)} + details: format!("`{}` cannot be used as a header value: {}", ${ + redactIfNecessary( + memberShape, + model, + "v" + ) + }, err)} })?; builder = builder.header(header_name, header_value); } @@ -169,7 +173,7 @@ class RequestBindingGenerator( val memberSymbol = symbolProvider.toSymbol(memberShape) val memberName = symbolProvider.toMemberName(memberShape) ifSet(memberType, memberSymbol, "&self.$memberName") { field -> - ListForEach(memberType, field) { innerField, targetId -> + listForEach(memberType, field) { innerField, targetId -> val innerMemberType = model.expectShape(targetId) val formatted = headerFmtFun(innerMemberType, memberShape, innerField) val safeName = safeName("formatted") @@ -263,10 +267,12 @@ class RequestBindingGenerator( private fun uriQuery(writer: RustWriter): Boolean { // Don't bother generating the function if we aren't going to make a query string val dynamicParams = index.getRequestBindings(shape, HttpBinding.Location.QUERY) + val mapParams = index.getRequestBindings(shape, HttpBinding.Location.QUERY_PARAMS) val literalParams = httpTrait.uri.queryLiterals if (dynamicParams.isEmpty() && literalParams.isEmpty()) { return false } + val preloadedParams = literalParams.keys + dynamicParams.map { it.locationName } writer.rustBlock("fn uri_query(&self, mut output: &mut String)") { write("let mut query = #T::new(&mut output);", RuntimeType.QueryFormat(runtimeConfig, "Writer")) literalParams.forEach { (k, v) -> @@ -279,13 +285,35 @@ class RequestBindingGenerator( } } + if (mapParams.isNotEmpty()) { + rust("let protected_params = ${preloadedParams.joinToString(prefix = "[", postfix = "]") { it.dq() }};") + } + mapParams.forEach { param -> + val memberShape = param.member + val memberSymbol = symbolProvider.toSymbol(memberShape) + val memberName = symbolProvider.toMemberName(memberShape) + val targetShape = model.expectShape(memberShape.target, MapShape::class.java) + val stringFormatter = RuntimeType.QueryFormat(runtimeConfig, "fmt_string") + ifSet(model.expectShape(param.member.target), memberSymbol, "&self.$memberName") { field -> + rustBlock("for (k, v) in $field") { + // if v is a list, generate another level of iteration + listForEach(model.expectShape(targetShape.value.target), "v") { innerField, _ -> + rustBlock("if !protected_params.contains(&k.as_str())") { + rust("query.push_kv(T(k), T($innerField));", stringFormatter) + } + } + } + } + } + dynamicParams.forEach { param -> val memberShape = param.member val memberSymbol = symbolProvider.toSymbol(memberShape) val memberName = symbolProvider.toMemberName(memberShape) val outerTarget = model.expectShape(memberShape.target) ifSet(outerTarget, memberSymbol, "&self.$memberName") { field -> - ListForEach(outerTarget, field) { innerField, targetId -> + // if `param` is a list, generate another level of iteration + listForEach(outerTarget, field) { innerField, targetId -> val target = model.expectShape(targetId) rust( "query.push_kv(${param.locationName.dq()}, &${ diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt index 9b55797ea8..e56f2ec3c6 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt @@ -40,6 +40,7 @@ class ResponseBindingGenerator(protocolConfig: ProtocolConfig, private val opera private val runtimeConfig = protocolConfig.runtimeConfig private val symbolProvider = protocolConfig.symbolProvider private val model = protocolConfig.model + private val service = protocolConfig.serviceShape private val index = HttpBindingIndex.of(model) private val headerUtil = CargoDependency.SmithyHttp(runtimeConfig).asType().member("header") private val defaultTimestampFormat = TimestampFormatTrait.Format.EPOCH_SECONDS @@ -60,7 +61,7 @@ class ResponseBindingGenerator(protocolConfig: ProtocolConfig, private val opera fun generateDeserializeHeaderFn(binding: HttpBinding): RuntimeType { check(binding.location == HttpBinding.Location.HEADER) val outputT = symbolProvider.toSymbol(binding.member) - val fnName = "deser_header_${operationShape.id.name.toSnakeCase()}_${binding.memberName.toSnakeCase()}" + val fnName = "deser_header_${fnName(operationShape, binding)}" return RuntimeType.forInlineFun(fnName, "http_serde") { writer -> writer.rustBlock( "pub fn $fnName(header_map: &#T::HeaderMap) -> Result<#T, #T::ParseError>", @@ -80,7 +81,7 @@ class ResponseBindingGenerator(protocolConfig: ProtocolConfig, private val opera check(outputT.rustType().stripOuter() is RustType.HashMap) { outputT.rustType() } val target = model.expectShape(binding.member.target) check(target is MapShape) - val fnName = "deser_prefix_header_${operationShape.id.name.toSnakeCase()}_${binding.memberName.toSnakeCase()}" + val fnName = "deser_prefix_header_${fnName(operationShape, binding)}" val inner = RuntimeType.forInlineFun("${fnName}_inner", "http_serde_inner") { it.rustBlock( "pub fn ${fnName}_inner(headers: #T::header::ValueIter) -> Result, #T::ParseError>", @@ -126,7 +127,7 @@ class ResponseBindingGenerator(protocolConfig: ProtocolConfig, private val opera ): RuntimeType { check(binding.location == HttpBinding.Location.PAYLOAD) val outputT = symbolProvider.toSymbol(binding.member) - val fnName = "deser_payload_${operationShape.id.name.toSnakeCase()}_${binding.memberName.toSnakeCase()}" + val fnName = "deser_payload_${fnName(operationShape, binding)}" return RuntimeType.forInlineFun(fnName, "http_serde") { rustWriter -> rustWriter.rustBlock("pub fn $fnName(body: &[u8]) -> Result<#T, #T>", outputT, errorT) { deserializePayloadBody( @@ -268,4 +269,10 @@ class ResponseBindingGenerator(protocolConfig: ProtocolConfig, private val opera } } } + + /** + * Generate a unique name for the deserializer function for a given operationShape -> member pair + */ + // rename here technically not required, operations and members cannot be renamed + private fun fnName(operationShape: OperationShape, binding: HttpBinding) = "${operationShape.id.getName(service).toSnakeCase()}_${binding.memberName.toSnakeCase()}" } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson10.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson10.kt index 22e83ba8dc..019ca403c7 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson10.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/AwsJson10.kt @@ -175,6 +175,7 @@ class BasicAwsJsonGenerator( ) { httpBuilderFun(implBlockWriter) { write("let builder = #T::new();", RuntimeType.HttpRequestBuilder) + // rename safety: Operation shapes cannot be renamed rust( """ Ok( diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizer.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizer.kt index 42867f4526..85be8037b3 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizer.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/transformers/OperationNormalizer.kt @@ -98,6 +98,7 @@ class OperationNormalizer(private val model: Model) { companion object { // Functions to construct synthetic shape IDs—Don't rely on these in external code: The attached traits // provide shape ids via `.body` on [SyntheticInputTrait] and [SyntheticOutputTrait] + // Rename safety: Operations cannot be renamed private fun OperationShape.inputId() = ShapeId.fromParts(this.id.namespace, "${this.id.name}Input") private fun OperationShape.outputId() = ShapeId.fromParts(this.id.namespace, "${this.id.name}Output") private fun OperationShape.inputBodyId() = ShapeId.fromParts(this.id.namespace, "${this.id.name}InputBody") diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/TestHelpers.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/TestHelpers.kt index 06adfc7846..7af0d7e235 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/TestHelpers.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/testutil/TestHelpers.kt @@ -31,8 +31,12 @@ val TestSymbolVisitorConfig = SymbolVisitorConfig( handleRustBoxing = true ) -fun testSymbolProvider(model: Model): RustSymbolProvider = - RustCodegenPlugin.BaseSymbolProvider(model, TestSymbolVisitorConfig) +fun testSymbolProvider(model: Model, serviceShape: ServiceShape? = null): RustSymbolProvider = + RustCodegenPlugin.baseSymbolProvider( + model, + serviceShape ?: ServiceShape.builder().version("test").id("test#Service").build(), + TestSymbolVisitorConfig + ) fun testProtocolConfig(model: Model, serviceShape: ServiceShape? = null): ProtocolConfig = ProtocolConfig( model, diff --git a/gradle.properties b/gradle.properties index c1a81f76c1..dd300ba992 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ kotlin.code.style=official # codegen -smithyVersion=1.6.1 +smithyVersion=1.7.0 # kotlin kotlinVersion=1.4.21 diff --git a/rust-runtime/protocol-test-helpers/src/lib.rs b/rust-runtime/protocol-test-helpers/src/lib.rs index 1d5d1e51a7..d904de8231 100644 --- a/rust-runtime/protocol-test-helpers/src/lib.rs +++ b/rust-runtime/protocol-test-helpers/src/lib.rs @@ -94,16 +94,25 @@ pub fn validate_query_string( pub fn forbid_query_params( request: &Request, - forbid_keys: &[&str], + forbid_params: &[&str], ) -> Result<(), ProtocolTestFailure> { - let actual_keys: HashSet<&str> = extract_params(request.uri()) + let actual_params: HashSet = extract_params(request.uri()) .iter() - .map(|param| QueryParam::parse(param).key) + .map(|param| QueryParam::parse(param)) .collect(); - for key in forbid_keys { - if actual_keys.contains(*key) { + let actual_keys: HashSet<&str> = actual_params.iter().map(|param| param.key).collect(); + for param in forbid_params { + let parsed = QueryParam::parse(param); + // If the forbidden param is k=v, then forbid this key-value pair + if actual_params.contains(&parsed) { return Err(ProtocolTestFailure::ForbiddenQueryParam { - expected: key.to_string(), + expected: param.to_string(), + }); + } + // If the assertion is only about a key, then check keys + if parsed.value.is_none() && actual_keys.contains(parsed.key) { + return Err(ProtocolTestFailure::ForbiddenQueryParam { + expected: param.to_string(), }); } } @@ -309,8 +318,9 @@ mod tests { .unwrap(); forbid_query_params(&request, &["a"]).expect_err("a is a query param"); forbid_query_params(&request, &["not_included"]).expect("query param not included"); - forbid_query_params(&request, &["a=b"]).expect("should be matching against keys"); + forbid_query_params(&request, &["a=b"]).expect_err("if there is an `=`, match against KV"); forbid_query_params(&request, &["c"]).expect_err("c is a query param"); + forbid_query_params(&request, &["a=c"]).expect("there is no a=c query param set"); } #[test] diff --git a/rust-runtime/smithy-http/src/query.rs b/rust-runtime/smithy-http/src/query.rs index de10cb3321..410826b309 100644 --- a/rust-runtime/smithy-http/src/query.rs +++ b/rust-runtime/smithy-http/src/query.rs @@ -65,7 +65,7 @@ fn url_encode(c: char, buff: &mut String) { if is_valid_query(c) { buff.push(c) } else { - let mut inner_buff = [0; 2]; + let mut inner_buff = [0; 4]; let u8_slice = c.encode_utf8(&mut inner_buff).as_bytes(); for c in u8_slice { let upper = (c & 0xf0) >> 4; @@ -129,5 +129,6 @@ mod test { assert_eq!(fmt_string(" ").as_str(), "%20"); assert_eq!(fmt_string("foo/baz%20").as_str(), "foo/baz%2520"); assert_eq!(fmt_string("&=").as_str(), "%26%3D"); + assert_eq!(fmt_string("🐱").as_str(), "%F0%9F%90%B1"); } }