diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AddFIPSDualStackDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AddFIPSDualStackDecorator.kt deleted file mode 100644 index b89a48379d217d40970a8c491ccc381bdbfb669d..0000000000000000000000000000000000000000 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AddFIPSDualStackDecorator.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.rustsdk - -import software.amazon.smithy.model.Model -import software.amazon.smithy.model.shapes.ServiceShape -import software.amazon.smithy.model.shapes.ShapeId -import software.amazon.smithy.model.shapes.ShapeType -import software.amazon.smithy.model.transform.ModelTransformer -import software.amazon.smithy.rulesengine.language.EndpointRuleSet -import software.amazon.smithy.rulesengine.language.syntax.parameters.Builtins -import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameter -import software.amazon.smithy.rulesengine.language.syntax.parameters.ParameterType -import software.amazon.smithy.rulesengine.traits.ClientContextParamDefinition -import software.amazon.smithy.rulesengine.traits.ClientContextParamsTrait -import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext -import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator -import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointRulesetIndex -import software.amazon.smithy.rust.codegen.client.smithy.endpoint.rustName -import software.amazon.smithy.rust.codegen.core.rustlang.Writable -import software.amazon.smithy.rust.codegen.core.rustlang.rust -import software.amazon.smithy.rust.codegen.core.smithy.customize.AdHocSection -import software.amazon.smithy.rust.codegen.core.smithy.customize.Section -import software.amazon.smithy.rust.codegen.core.util.getTrait - -fun EndpointRuleSet.getBuiltIn(builtIn: Parameter) = parameters.toList().find { it.builtIn == builtIn.builtIn } -fun ClientCodegenContext.getBuiltIn(builtIn: Parameter): Parameter? { - val idx = EndpointRulesetIndex.of(model) - val rules = idx.endpointRulesForService(serviceShape) ?: return null - return rules.getBuiltIn(builtIn) -} - -/** - * For legacy SDKs, there are builtIn parameters that cannot be automatically used as context parameters. - * - * However, for the Rust SDK, these parameters can be used directly. - */ -fun Model.promoteBuiltInToContextParam(serviceId: ShapeId, builtInSrc: Parameter): Model { - val model = this - // load the builtIn with a matching name from the ruleset allowing for any docs updates - val builtIn = this.loadBuiltIn(serviceId, builtInSrc) ?: return model - - return ModelTransformer.create().mapShapes(model) { shape -> - if (shape !is ServiceShape || shape.id != serviceId) { - shape - } else { - val traitBuilder = shape.getTrait() - // there is a bug in the return type of the toBuilder method - ?.let { ClientContextParamsTrait.builder().parameters(it.parameters) } - ?: ClientContextParamsTrait.builder() - val contextParamsTrait = - traitBuilder.putParameter( - builtIn.name.asString(), - ClientContextParamDefinition.builder().documentation(builtIn.documentation.get()).type( - when (builtIn.type!!) { - ParameterType.STRING -> ShapeType.STRING - ParameterType.BOOLEAN -> ShapeType.BOOLEAN - }, - ).build(), - ).build() - shape.toBuilder().removeTrait(ClientContextParamsTrait.ID).addTrait(contextParamsTrait).build() - } - } -} - -fun Model.loadBuiltIn(serviceId: ShapeId, builtInSrc: Parameter): Parameter? { - val model = this - val idx = EndpointRulesetIndex.of(model) - val service = model.expectShape(serviceId, ServiceShape::class.java) - val rules = idx.endpointRulesForService(service) ?: return null - // load the builtIn with a matching name from the ruleset allowing for any docs updates - return rules.getBuiltIn(builtInSrc) -} - -fun Model.sdkConfigSetter(serviceId: ShapeId, builtInSrc: Parameter): Pair, (Section) -> Writable>? { - val builtIn = loadBuiltIn(serviceId, builtInSrc) ?: return null - val fieldName = builtIn.name.rustName() - - return SdkConfigSection.create { section -> - { - rust("${section.serviceConfigBuilder}.set_$fieldName(${section.sdkConfig}.$fieldName());") - } - } -} - -class AddFIPSDualStackDecorator : ClientCodegenDecorator { - override val name: String = "AddFipsDualStack" - override val order: Byte = 0 - - override fun transformModel(service: ServiceShape, model: Model): Model { - return model - .promoteBuiltInToContextParam(service.id, Builtins.FIPS) - .promoteBuiltInToContextParam(service.id, Builtins.DUALSTACK) - } - - override fun extraSections(codegenContext: ClientCodegenContext): List, (Section) -> Writable>> { - return listOfNotNull( - codegenContext.model.sdkConfigSetter(codegenContext.serviceShape.id, Builtins.FIPS), - codegenContext.model.sdkConfigSetter(codegenContext.serviceShape.id, Builtins.DUALSTACK), - ) - } -} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt index b1b4a862968c55f114fae491d6c69909306eeb96..75d6fd7cbdb81c4744fc900b49aed9c7a3c11b1f 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt @@ -17,6 +17,9 @@ import software.amazon.smithy.rustsdk.customize.route53.Route53Decorator import software.amazon.smithy.rustsdk.customize.s3.S3Decorator import software.amazon.smithy.rustsdk.customize.s3control.S3ControlDecorator import software.amazon.smithy.rustsdk.customize.sts.STSDecorator +import software.amazon.smithy.rustsdk.endpoints.AwsEndpointDecorator +import software.amazon.smithy.rustsdk.endpoints.AwsEndpointsStdLib +import software.amazon.smithy.rustsdk.endpoints.OperationInputTestDecorator val DECORATORS: List = listOf( // General AWS Decorators @@ -38,8 +41,9 @@ val DECORATORS: List = listOf( AwsReadmeDecorator(), HttpConnectorDecorator(), AwsEndpointsStdLib(), - AddFIPSDualStackDecorator(), + *PromotedBuiltInsDecorators, GenericSmithySdkConfigSettings(), + OperationInputTestDecorator(), // Service specific decorators ApiGatewayDecorator(), diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeType.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeType.kt index c0a92d097017c814d84356dc5e097775f13d01c2..f4b05b7bafdbf4070933dd3825b46b01dc3dc4e4 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeType.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeType.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.rustsdk import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.DependencyScope import software.amazon.smithy.rust.codegen.core.rustlang.Visibility import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeCrateLocation @@ -60,10 +61,16 @@ object AwsRuntimeType { ).resolve("DefaultMiddleware") fun awsCredentialTypes(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsCredentialTypes(runtimeConfig).toType() + + fun awsCredentialTypesTestUtil(runtimeConfig: RuntimeConfig) = + AwsCargoDependency.awsCredentialTypes(runtimeConfig).copy(scope = DependencyScope.Dev).withFeature("test-util").toType() + fun awsEndpoint(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsEndpoint(runtimeConfig).toType() fun awsHttp(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsHttp(runtimeConfig).toType() fun awsSigAuth(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsSigAuth(runtimeConfig).toType() - fun awsSigAuthEventStream(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsSigAuthEventStream(runtimeConfig).toType() + fun awsSigAuthEventStream(runtimeConfig: RuntimeConfig) = + AwsCargoDependency.awsSigAuthEventStream(runtimeConfig).toType() + fun awsSigv4(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsSigv4(runtimeConfig).toType() fun awsTypes(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsTypes(runtimeConfig).toType() } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialProviders.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialProviders.kt index b584b14c6ef12f1bacf3424e2177e18e4aed53ed..79b534ccd6db3e78b7e49f842d9a27b605cf2db6 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialProviders.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialProviders.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.rustsdk import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.customize.TestUtilFeature import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig import software.amazon.smithy.rust.codegen.core.rustlang.Writable @@ -15,6 +16,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.customize.AdHocSection import software.amazon.smithy.rust.codegen.core.smithy.customize.Section import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsCustomization @@ -46,6 +48,10 @@ class CredentialsProviderDecorator : ClientCodegenDecorator { } }, ) + + override fun extras(codegenContext: ClientCodegenContext, rustCrate: RustCrate) { + rustCrate.mergeFeature(TestUtilFeature.copy(deps = listOf("aws-credential-types/test-util"))) + } } /** @@ -54,13 +60,19 @@ class CredentialsProviderDecorator : ClientCodegenDecorator { class CredentialProviderConfig(runtimeConfig: RuntimeConfig) : ConfigCustomization() { private val codegenScope = arrayOf( "provider" to AwsRuntimeType.awsCredentialTypes(runtimeConfig).resolve("provider"), + "Credentials" to AwsRuntimeType.awsCredentialTypes(runtimeConfig).resolve("Credentials"), + "TestCredentials" to AwsRuntimeType.awsCredentialTypesTestUtil(runtimeConfig).resolve("Credentials"), "DefaultProvider" to defaultProvider(), ) override fun section(section: ServiceConfig) = writable { when (section) { ServiceConfig.BuilderStruct -> - rustTemplate("credentials_provider: Option>,", *codegenScope) + rustTemplate( + "credentials_provider: Option>,", + *codegenScope, + ) + ServiceConfig.BuilderImpl -> { rustTemplate( """ @@ -80,6 +92,11 @@ class CredentialProviderConfig(runtimeConfig: RuntimeConfig) : ConfigCustomizati ) } + is ServiceConfig.DefaultForTests -> rustTemplate( + "${section.configBuilderRef}.set_credentials_provider(Some(std::sync::Arc::new(#{TestCredentials}::for_tests())));", + *codegenScope, + ) + else -> emptySection } } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/EndpointBuiltInsDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/EndpointBuiltInsDecorator.kt new file mode 100644 index 0000000000000000000000000000000000000000..0b869e7c6f54d7ccff8dcb1e2d3f4b87799ff2ac --- /dev/null +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/EndpointBuiltInsDecorator.kt @@ -0,0 +1,179 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rustsdk + +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.node.BooleanNode +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.node.StringNode +import software.amazon.smithy.model.shapes.ServiceShape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.rulesengine.language.EndpointRuleSet +import software.amazon.smithy.rulesengine.language.syntax.parameters.Builtins +import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameter +import software.amazon.smithy.rulesengine.language.syntax.parameters.ParameterType +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointCustomization +import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointRulesetIndex +import software.amazon.smithy.rust.codegen.client.smithy.endpoint.rustName +import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigParam +import software.amazon.smithy.rust.codegen.client.smithy.generators.config.standardConfigParam +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.docs +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.customize.AdHocSection +import software.amazon.smithy.rust.codegen.core.smithy.customize.Section +import software.amazon.smithy.rust.codegen.core.util.PANIC +import software.amazon.smithy.rust.codegen.core.util.dq +import software.amazon.smithy.rust.codegen.core.util.extendIf +import software.amazon.smithy.rust.codegen.core.util.orNull +import java.util.Optional + +/** load a builtIn parameter from a ruleset by name */ +fun EndpointRuleSet.getBuiltIn(builtIn: String) = parameters.toList().find { it.builtIn == Optional.of(builtIn) } + +/** load a builtIn parameter from a ruleset. The returned builtIn is the one defined in the ruleset (including latest docs, etc.) */ +fun EndpointRuleSet.getBuiltIn(builtIn: Parameter) = getBuiltIn(builtIn.builtIn.orNull()!!) +fun ClientCodegenContext.getBuiltIn(builtIn: Parameter): Parameter? = getBuiltIn(builtIn.builtIn.orNull()!!) +fun ClientCodegenContext.getBuiltIn(builtIn: String): Parameter? { + val idx = EndpointRulesetIndex.of(model) + val rules = idx.endpointRulesForService(serviceShape) ?: return null + return rules.getBuiltIn(builtIn) +} + +private fun toConfigParam(parameter: Parameter): ConfigParam = ConfigParam( + parameter.name.rustName(), + when (parameter.type!!) { + ParameterType.STRING -> RuntimeType.String.toSymbol() + ParameterType.BOOLEAN -> RuntimeType.Bool.toSymbol() + }, + parameter.documentation.orNull()?.let { writable { docs(it) } }, +) + +fun Model.loadBuiltIn(serviceId: ShapeId, builtInSrc: Parameter): Parameter? { + val model = this + val idx = EndpointRulesetIndex.of(model) + val service = model.expectShape(serviceId, ServiceShape::class.java) + val rules = idx.endpointRulesForService(service) ?: return null + // load the builtIn with a matching name from the ruleset allowing for any docs updates + return rules.getBuiltIn(builtInSrc) +} + +fun Model.sdkConfigSetter( + serviceId: ShapeId, + builtInSrc: Parameter, + configParameterNameOverride: String?, +): Pair, (Section) -> Writable>? { + val builtIn = loadBuiltIn(serviceId, builtInSrc) ?: return null + val fieldName = configParameterNameOverride ?: builtIn.name.rustName() + + val map = when (builtIn.type!!) { + ParameterType.STRING -> writable { rust("|s|s.to_string()") } + ParameterType.BOOLEAN -> null + } + return SdkConfigSection.copyField(fieldName, map) +} + +/** + * Create a client codegen decorator that creates bindings for a builtIn parameter. Optionally, you can provide [clientParam] + * which allows control over the config parameter that will be generated. + */ +fun decoratorForBuiltIn( + builtIn: Parameter, + clientParam: ConfigParam? = null, +): ClientCodegenDecorator { + val nameOverride = clientParam?.name + val name = nameOverride ?: builtIn.name.rustName() + return object : ClientCodegenDecorator { + override val name: String = "Auto${builtIn.builtIn.get()}" + override val order: Byte = 0 + + private fun rulesetContainsBuiltIn(codegenContext: ClientCodegenContext) = + codegenContext.getBuiltIn(builtIn) != null + + override fun extraSections(codegenContext: ClientCodegenContext): List, (Section) -> Writable>> { + return listOfNotNull( + codegenContext.model.sdkConfigSetter(codegenContext.serviceShape.id, builtIn, clientParam?.name), + ) + } + + override fun configCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List { + return baseCustomizations.extendIf(rulesetContainsBuiltIn(codegenContext)) { + standardConfigParam( + clientParam ?: toConfigParam(builtIn), + ) + } + } + + override fun endpointCustomizations(codegenContext: ClientCodegenContext): List = listOf( + object : EndpointCustomization { + override fun loadBuiltInFromServiceConfig(parameter: Parameter, configRef: String): Writable? = + when (parameter.builtIn) { + builtIn.builtIn -> writable { + rust("$configRef.$name") + if (parameter.type == ParameterType.STRING) { + rust(".clone()") + } + } + else -> null + } + + override fun setBuiltInOnServiceConfig(name: String, value: Node, configBuilderRef: String): Writable? { + if (name != builtIn.builtIn.get()) { + return null + } + return writable { + rustTemplate( + "let $configBuilderRef = $configBuilderRef.${nameOverride ?: builtIn.name.rustName()}(#{value});", + "value" to value.toWritable(), + ) + } + } + }, + ) + } +} + +private val endpointUrlDocs = writable { + rust( + """ + /// Sets the endpoint url used to communicate with this service + + /// Note: this is used in combination with other endpoint rules, e.g. an API that applies a host-label prefix + /// will be prefixed onto this URL. To fully override the endpoint resolver, use + /// [`Builder::endpoint_resolver`]. + """.trimIndent(), + ) +} + +fun Node.toWritable(): Writable { + val node = this + return writable { + when (node) { + is StringNode -> rust(node.value.dq()) + is BooleanNode -> rust("${node.value}") + else -> PANIC("unsupported value for a default: $node") + } + } +} + +val PromotedBuiltInsDecorators = + listOf( + decoratorForBuiltIn(Builtins.FIPS), + decoratorForBuiltIn(Builtins.DUALSTACK), + decoratorForBuiltIn( + Builtins.SDK_ENDPOINT, + ConfigParam("endpoint_url", RuntimeType.String.toSymbol(), endpointUrlDocs), + ), + ).toTypedArray() diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RegionDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RegionDecorator.kt index e6a76bd6c6cd89a7086653f44133aab7949f0783..fef37957bb8accc6c4ce1fec9f1f416874898ab1 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RegionDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RegionDecorator.kt @@ -12,7 +12,6 @@ import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameter import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointCustomization -import software.amazon.smithy.rust.codegen.client.smithy.endpoint.generators.CustomRuntimeFunction import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig import software.amazon.smithy.rust.codegen.core.rustlang.Writable @@ -130,14 +129,14 @@ class RegionDecorator : ClientCodegenDecorator { } return listOf( object : EndpointCustomization { - override fun builtInDefaultValue(parameter: Parameter, configRef: String): Writable? { + override fun loadBuiltInFromServiceConfig(parameter: Parameter, configRef: String): Writable? { return when (parameter.builtIn) { Builtins.REGION.builtIn -> writable { rust("$configRef.region.as_ref().map(|r|r.as_ref().to_owned())") } else -> null } } - override fun setBuiltInOnConfig(name: String, value: Node, configBuilderRef: String): Writable? { + override fun setBuiltInOnServiceConfig(name: String, value: Node, configBuilderRef: String): Writable? { if (name != Builtins.REGION.builtIn.get()) { return null } @@ -148,9 +147,6 @@ class RegionDecorator : ClientCodegenDecorator { ) } } - - override fun customRuntimeFunctions(codegenContext: ClientCodegenContext): List = - listOf() }, ) } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt index 571a339326351accd0e339a76a7d09e90da94619..2d1ff40a1fec50af120d218eae7494b03d8e9e19 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt @@ -35,6 +35,26 @@ object SdkConfigSection : AdHocSection + { + val mapBlock = map?.let { writable { rust(".map(#W)", it) } } ?: writable { } + rustTemplate( + "${section.serviceConfigBuilder}.set_$fieldName(${section.sdkConfig}.$fieldName()#{map});", + "map" to mapBlock, + ) + } + } } /** diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt index 9c56a24cfe56501c93644d9ac9fc4e61b1bd0b8b..d6c0f8257f4099c12eb1e7bc550ca01734c63515 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.rustsdk.customize.s3 import software.amazon.smithy.aws.traits.protocols.RestXmlTrait import software.amazon.smithy.model.Model +import software.amazon.smithy.model.node.Node import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.Shape @@ -15,6 +16,8 @@ import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.transform.ModelTransformer import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointCustomization +import software.amazon.smithy.rust.codegen.client.smithy.endpoint.rustName import software.amazon.smithy.rust.codegen.client.smithy.generators.protocol.ClientProtocolGenerator import software.amazon.smithy.rust.codegen.client.smithy.protocols.ClientRestXmlFactory import software.amazon.smithy.rust.codegen.core.rustlang.RustModule @@ -32,6 +35,9 @@ import software.amazon.smithy.rust.codegen.core.smithy.protocols.RestXml import software.amazon.smithy.rust.codegen.core.smithy.traits.AllowInvalidXmlRoot import software.amazon.smithy.rust.codegen.core.util.letIf import software.amazon.smithy.rustsdk.AwsRuntimeType +import software.amazon.smithy.rustsdk.endpoints.stripEndpointTrait +import software.amazon.smithy.rustsdk.getBuiltIn +import software.amazon.smithy.rustsdk.toWritable import java.util.logging.Logger /** @@ -68,7 +74,7 @@ class S3Decorator : ClientCodegenDecorator { logger.info("Adding AllowInvalidXmlRoot trait to $it") (it as StructureShape).toBuilder().addTrait(AllowInvalidXmlRoot()).build() } - }.let(StripBucketFromHttpPath()::transform) + }.let(StripBucketFromHttpPath()::transform).let(stripEndpointTrait("RequestRoute")) } } @@ -79,6 +85,24 @@ class S3Decorator : ClientCodegenDecorator { it + S3PubUse() } + override fun endpointCustomizations(codegenContext: ClientCodegenContext): List { + return listOf(object : EndpointCustomization { + override fun setBuiltInOnServiceConfig(name: String, value: Node, configBuilderRef: String): Writable? { + if (!name.startsWith("AWS::S3")) { + return null + } + val builtIn = codegenContext.getBuiltIn(name) ?: return null + return writable { + rustTemplate( + "let $configBuilderRef = $configBuilderRef.${builtIn.name.rustName()}(#{value});", + "value" to value.toWritable(), + ) + } + } + }, + ) + } + private fun isInInvalidXmlRootAllowList(shape: Shape): Boolean { return shape.isStructureShape && invalidXmlRootAllowList.contains(shape.id) } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3control/S3ControlDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3control/S3ControlDecorator.kt index 1266d080a33f94e12a03b731abe0a7f24bc0adfc..39e85d789881743ce6788237092e9bc5643a8cd1 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3control/S3ControlDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3control/S3ControlDecorator.kt @@ -8,9 +8,8 @@ package software.amazon.smithy.rustsdk.customize.s3control import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.ShapeId -import software.amazon.smithy.model.traits.EndpointTrait -import software.amazon.smithy.model.transform.ModelTransformer import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rustsdk.endpoints.stripEndpointTrait class S3ControlDecorator : ClientCodegenDecorator { override val name: String = "S3Control" @@ -22,11 +21,6 @@ class S3ControlDecorator : ClientCodegenDecorator { if (!applies(service)) { return model } - return ModelTransformer.create() - .removeTraitsIf(model) { _, trait -> - trait is EndpointTrait && trait.hostPrefix.labels.any { - it.isLabel && it.content == "AccountId" - } - } + return stripEndpointTrait("AccountId")(model) } } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsEndpointDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/AwsEndpointDecorator.kt similarity index 93% rename from aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsEndpointDecorator.kt rename to aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/AwsEndpointDecorator.kt index b4715db7d6ae371f2d0cd34ee4d626d127f11492..5973d5e3c5fdabfcf3fbc30af7108af1f71d5f3f 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsEndpointDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/AwsEndpointDecorator.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.rustsdk +package software.amazon.smithy.rustsdk.endpoints import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.model.Model @@ -12,12 +12,10 @@ import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.transform.ModelTransformer import software.amazon.smithy.rulesengine.language.EndpointRuleSet import software.amazon.smithy.rulesengine.language.syntax.parameters.Builtins -import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameter import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameters import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator -import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointCustomization import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointTypesGenerator import software.amazon.smithy.rust.codegen.client.smithy.endpoint.generators.EndpointsModule import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization @@ -38,6 +36,9 @@ import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsSection import software.amazon.smithy.rust.codegen.core.util.extendIf import software.amazon.smithy.rust.codegen.core.util.letIf import software.amazon.smithy.rust.codegen.core.util.thenSingletonListOf +import software.amazon.smithy.rustsdk.AwsRuntimeType +import software.amazon.smithy.rustsdk.SdkConfigSection +import software.amazon.smithy.rustsdk.getBuiltIn class AwsEndpointDecorator : ClientCodegenDecorator { override val name: String = "AwsEndpoint" @@ -87,9 +88,7 @@ class AwsEndpointDecorator : ClientCodegenDecorator { ): List { return baseCustomizations.extendIf(codegenContext.isRegionalized()) { AwsEndpointShimCustomization(codegenContext) - } + SdkEndpointCustomization( - codegenContext, - ) + } } override fun libRsCustomizations( @@ -141,7 +140,6 @@ class AwsEndpointDecorator : ClientCodegenDecorator { rust( """ ${section.serviceConfigBuilder}.set_aws_endpoint_resolver(${section.sdkConfig}.endpoint_resolver().clone()); - ${section.serviceConfigBuilder}.set_endpoint_url(${section.sdkConfig}.endpoint_url().map(|url|url.to_string())); """, ) } @@ -149,19 +147,6 @@ class AwsEndpointDecorator : ClientCodegenDecorator { } } - override fun endpointCustomizations(codegenContext: ClientCodegenContext): List { - return listOf( - object : EndpointCustomization { - override fun builtInDefaultValue(parameter: Parameter, configRef: String): Writable? { - return when (parameter.builtIn) { - Builtins.SDK_ENDPOINT.builtIn -> writable { rust("$configRef.endpoint_url().map(|url|url.to_string())") } - else -> null - } - } - }, - ) - } - class AwsEndpointShimCustomization(codegenContext: ClientCodegenContext) : ConfigCustomization() { private val moduleUseName = codegenContext.moduleUseName() private val runtimeConfig = codegenContext.runtimeConfig diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsEndpointsStdLib.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/AwsEndpointsStdLib.kt similarity index 95% rename from aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsEndpointsStdLib.kt rename to aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/AwsEndpointsStdLib.kt index 1c05afda42d94212193ad4d39189ee6235229ddf..5d3fb010f9eb3724031777c9fabeef12ce6ceadd 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsEndpointsStdLib.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/AwsEndpointsStdLib.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.rustsdk +package software.amazon.smithy.rustsdk.endpoints import software.amazon.smithy.model.node.Node import software.amazon.smithy.model.node.ObjectNode @@ -12,6 +12,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegen import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointCustomization import software.amazon.smithy.rust.codegen.client.smithy.endpoint.generators.CustomRuntimeFunction import software.amazon.smithy.rust.codegen.client.smithy.endpoint.rulesgen.awsStandardLib +import software.amazon.smithy.rustsdk.SdkSettings import kotlin.io.path.readText /** diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/OperationInputTestGenerator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/OperationInputTestGenerator.kt new file mode 100644 index 0000000000000000000000000000000000000000..411d03540a01e1c2194028da95a07718596c3d48 --- /dev/null +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/OperationInputTestGenerator.kt @@ -0,0 +1,218 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rustsdk.endpoints + +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.rulesengine.language.syntax.parameters.Builtins +import software.amazon.smithy.rulesengine.traits.EndpointTestCase +import software.amazon.smithy.rulesengine.traits.EndpointTestOperationInput +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointTypesGenerator +import software.amazon.smithy.rust.codegen.client.smithy.generators.clientInstantiator +import software.amazon.smithy.rust.codegen.core.rustlang.Attribute +import software.amazon.smithy.rust.codegen.core.rustlang.AttributeKind +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.escape +import software.amazon.smithy.rust.codegen.core.rustlang.join +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RustCrate +import software.amazon.smithy.rust.codegen.core.smithy.generators.setterName +import software.amazon.smithy.rust.codegen.core.testutil.integrationTest +import software.amazon.smithy.rust.codegen.core.testutil.tokioTest +import software.amazon.smithy.rust.codegen.core.util.dq +import software.amazon.smithy.rust.codegen.core.util.expectMember +import software.amazon.smithy.rust.codegen.core.util.inputShape +import software.amazon.smithy.rust.codegen.core.util.orNull +import software.amazon.smithy.rust.codegen.core.util.orNullIfEmpty +import software.amazon.smithy.rust.codegen.core.util.toSnakeCase +import java.util.logging.Logger + +class OperationInputTestDecorator : ClientCodegenDecorator { + override val name: String = "OperationInputTest" + override val order: Byte = 0 + + override fun extras(codegenContext: ClientCodegenContext, rustCrate: RustCrate) { + val endpointTests = EndpointTypesGenerator.fromContext(codegenContext).tests.orNullIfEmpty() ?: return + rustCrate.integrationTest("endpoint_tests") { + Attribute(Attribute.cfg(Attribute.feature("test-util"))).render(this, AttributeKind.Inner) + val tests = endpointTests.flatMap { test -> + val generator = OperationInputTestGenerator(codegenContext, test) + test.operationInputs.filterNot { usesDeprecatedBuiltIns(it) }.map { operationInput -> + generator.generateInput(operationInput) + } + } + tests.join("\n")(this) + } + } +} + +private val deprecatedBuiltins = + setOf( + // The Rust SDK DOES NOT support the S3 global endpoint because we do not support bucket redirects + Builtins.S3_USE_GLOBAL_ENDPOINT, + // STS global endpoint was deprecated after STS regionalization + Builtins.STS_USE_GLOBAL_ENDPOINT, + ).map { it.builtIn.get() } + +fun usesDeprecatedBuiltIns(testOperationInput: EndpointTestOperationInput): Boolean { + return testOperationInput.builtInParams.members.map { it.key.value }.any { deprecatedBuiltins.contains(it) } +} + +/** + * Generate `operationInputTests` for EP2 tests. + * + * These are `tests/` style integration tests that run as a public SDK user against a complete client. `capture_request` + * is used to retrieve the URL. + * + * Example generated test: + * ```rust + * #[tokio::test] + * async fn operation_input_test_get_object_119() { + * /* builtIns: { + * "AWS::Region": "us-west-2", + * "AWS::S3::UseArnRegion": false + * } */ + * /* clientParams: {} */ + * let (conn, rcvr) = aws_smithy_client::test_connection::capture_request(None); + * let conf = { + * #[allow(unused_mut)] + * let mut builder = aws_sdk_s3::Config::builder() + * .with_test_defaults() + * .http_connector(conn); + * let builder = builder.region(aws_types::region::Region::new("us-west-2")); + * let builder = builder.use_arn_region(false); + * builder.build() + * }; + * let client = aws_sdk_s3::Client::from_conf(conf); + * let _result = dbg!(client.get_object() + * .set_bucket(Some( + * "arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint".to_owned() + * )) + * .set_key(Some( + * "key".to_owned() + * )) + * .send().await); + * rcvr.expect_no_request(); + * let error = _result.expect_err("expected error: Invalid configuration: region from ARN `us-east-1` does not match client region `us-west-2` and UseArnRegion is `false` [outposts arn with region mismatch and UseArnRegion=false]"); + * assert!(format!("{:?}", error).contains("Invalid configuration: region from ARN `us-east-1` does not match client region `us-west-2` and UseArnRegion is `false`"), "expected error to contain `Invalid configuration: region from ARN `us-east-1` does not match client region `us-west-2` and UseArnRegion is `false`` but it was {}", format!("{:?}", error)); + * } + * ``` + * + * Eventually, we need to pull this test into generic smithy. However, this relies on generic smithy clients + * supporting middleware and being instantiable from config (https://github.com/awslabs/smithy-rs/issues/2194) + * + * Doing this in AWS codegen allows us to actually integration test generated clients. + */ + +class OperationInputTestGenerator(private val ctx: ClientCodegenContext, private val test: EndpointTestCase) { + private val runtimeConfig = ctx.runtimeConfig + private val moduleName = ctx.moduleUseName() + private val endpointCustomizations = ctx.rootDecorator.endpointCustomizations(ctx) + private val model = ctx.model + private val instantiator = clientInstantiator(ctx) + + private fun EndpointTestOperationInput.operationId() = + ShapeId.fromOptionalNamespace(ctx.serviceShape.id.namespace, operationName) + + /** the Rust SDK doesn't support SigV4a — search endpoint.properties.authSchemes[].name */ + private fun EndpointTestCase.isSigV4a() = + expect.endpoint.orNull()?.properties?.get("authSchemes")?.asArrayNode()?.orNull() + ?.map { it.expectObjectNode().expectStringMember("name").value }?.contains("sigv4a") == true + + fun generateInput(testOperationInput: EndpointTestOperationInput) = writable { + val operationName = testOperationInput.operationName.toSnakeCase() + if (test.isSigV4a()) { + Attribute.shouldPanic("no request was received").render(this) + } + tokioTest(safeName("operation_input_test_$operationName")) { + rustTemplate( + """ + /* builtIns: ${escape(Node.prettyPrintJson(testOperationInput.builtInParams))} */ + /* clientParams: ${escape(Node.prettyPrintJson(testOperationInput.clientParams))} */ + let (conn, rcvr) = #{capture_request}(None); + let conf = #{conf}; + let client = $moduleName::Client::from_conf(conf); + let _result = dbg!(#{invoke_operation}); + #{assertion} + """, + "capture_request" to CargoDependency.smithyClient(runtimeConfig) + .withFeature("test-util").toType().resolve("test_connection::capture_request"), + "conf" to config(testOperationInput), + "invoke_operation" to operationInvocation(testOperationInput), + "assertion" to writable { + test.expect.endpoint.ifPresent { endpoint -> + val uri = escape(endpoint.url) + rustTemplate( + """ + let req = rcvr.expect_request(); + let uri = req.uri().to_string(); + assert!(uri.starts_with(${uri.dq()}), "expected URI to start with `$uri` but it was `{}`", uri); + """, + ) + } + test.expect.error.ifPresent { error -> + val expectedError = + escape("expected error: $error [${test.documentation.orNull() ?: "no docs"}]") + val escapedError = escape(error) + rustTemplate( + """ + rcvr.expect_no_request(); + let error = _result.expect_err(${expectedError.dq()}); + assert!( + format!("{:?}", error).contains(${escapedError.dq()}), + "expected error to contain `$escapedError` but it was {:?}", error + ); + """, + ) + } + }, + ) + } + } + + private fun operationInvocation(testOperationInput: EndpointTestOperationInput) = writable { + rust("client.${testOperationInput.operationName.toSnakeCase()}()") + val operationInput = + model.expectShape(testOperationInput.operationId(), OperationShape::class.java).inputShape(model) + testOperationInput.operationParams.members.forEach { (key, value) -> + val member = operationInput.expectMember(key.value) + rustTemplate( + ".${member.setterName()}(#{value})", + "value" to instantiator.generate(member, value), + ) + } + rust(".send().await") + } + + /** initialize service config for test */ + private fun config(operationInput: EndpointTestOperationInput) = writable { + rustBlock("") { + Attribute.AllowUnusedMut.render(this) + rust("let mut builder = $moduleName::Config::builder().with_test_defaults().http_connector(conn);") + operationInput.builtInParams.members.forEach { (builtIn, value) -> + val setter = endpointCustomizations.firstNotNullOfOrNull { + it.setBuiltInOnServiceConfig( + builtIn.value, + value, + "builder", + ) + } + if (setter != null) { + setter(this) + } else { + Logger.getLogger("OperationTestGenerator").warning("No provider for ${builtIn.value}") + } + } + rust("builder.build()") + } + } +} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/StripEndpointTrait.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/StripEndpointTrait.kt new file mode 100644 index 0000000000000000000000000000000000000000..6dccf3f6b3f8e15ebb86b9a71a89db97e2e10157 --- /dev/null +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/StripEndpointTrait.kt @@ -0,0 +1,21 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rustsdk.endpoints + +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.traits.EndpointTrait +import software.amazon.smithy.model.transform.ModelTransformer + +fun stripEndpointTrait(hostPrefix: String): (Model) -> Model { + return { model: Model -> + ModelTransformer.create() + .removeTraitsIf(model) { _, trait -> + trait is EndpointTrait && trait.hostPrefix.labels.any { + it.isLabel && it.content == hostPrefix + } + } + } +} diff --git a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/TestPromoteEndpointBuiltin.kt b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/TestPromoteEndpointBuiltin.kt deleted file mode 100644 index a8218c26124da2a54770ba8409e13c35ab0ab583..0000000000000000000000000000000000000000 --- a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/TestPromoteEndpointBuiltin.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.rustsdk - -import org.junit.jupiter.api.Test -import software.amazon.smithy.model.shapes.ShapeId -import software.amazon.smithy.rulesengine.language.syntax.parameters.Builtins -import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate -import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel -import software.amazon.smithy.rust.codegen.core.testutil.integrationTest -import software.amazon.smithy.rust.codegen.core.testutil.unitTest - -class TestPromoteEndpointBuiltin { - private val model = """ - namespace aws.testEndpointBuiltIn - - use aws.api#service - use aws.protocols#restJson1 - use smithy.rules#endpointRuleSet - use smithy.rules#staticContextParams - use smithy.rules#clientContextParams - - @service(sdkId: "Some Value") - @title("Test Auth Service") - @endpointRuleSet({ - parameters: { - CustomEndpoint: { "type": "string", "builtIn": "SDK::Endpoint", "documentation": "Sdk endpoint" } - }, - version: "1.0", - rules: [ - { - "type": "endpoint", - "conditions": [], - "endpoint": { - "url": "https://foo.com" - } - } - ] - }) - @restJson1 - service FooBaz { - version: "2018-03-17", - operations: [NoOp] - } - - @http(uri: "/blah", method: "GET") - operation NoOp {} - """.asSmithyModel() - - @Test - fun promoteStringBuiltIn() { - awsSdkIntegrationTest( - model.promoteBuiltInToContextParam( - ShapeId.from("aws.testEndpointBuiltIn#FooBaz"), - Builtins.SDK_ENDPOINT, - ), - ) { context, rustCrate -> - - val moduleName = context.moduleUseName() - rustCrate.integrationTest("builtin_as_string") { - // assert that a rule with no default auth works properly - unitTest("set_endpoint") { - rustTemplate( - """ - let _ = $moduleName::Config::builder().custom_endpoint("asdf").build(); - """, - ) - } - } - } - } -} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/IdempotencyTokenGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/IdempotencyTokenGenerator.kt index 049b92a25d415303a57d56878646ba71036051c3..8dd67bb9fe97286cf2800bdecf8e1e3431c3e817 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/IdempotencyTokenGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/IdempotencyTokenGenerator.kt @@ -16,7 +16,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationSectio import software.amazon.smithy.rust.codegen.core.util.findMemberWithTrait import software.amazon.smithy.rust.codegen.core.util.inputShape -class IdempotencyTokenGenerator(codegenContext: CodegenContext, private val operationShape: OperationShape) : +class IdempotencyTokenGenerator(codegenContext: CodegenContext, operationShape: OperationShape) : OperationCustomization() { private val model = codegenContext.model private val symbolProvider = codegenContext.symbolProvider diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/RequiredCustomizations.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/RequiredCustomizations.kt index be75256799b3d979ca1e4a07deb6c0262bbd9ea2..f65e042d445d852e4c702b9ae7963c4acbdc3a0d 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/RequiredCustomizations.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/RequiredCustomizations.kt @@ -22,6 +22,8 @@ import software.amazon.smithy.rust.codegen.core.smithy.customizations.pubUseSmit import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationCustomization import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsCustomization +val TestUtilFeature = Feature("test-util", false, listOf()) + /** * A set of customizations that are included in all protocols. * @@ -58,6 +60,8 @@ class RequiredCustomizations : ClientCodegenDecorator { // Add rt-tokio feature for `ByteStream::from_path` rustCrate.mergeFeature(Feature("rt-tokio", true, listOf("aws-smithy-http/rt-tokio"))) + rustCrate.mergeFeature(TestUtilFeature) + // Re-export resiliency types ResiliencyReExportCustomization(codegenContext.runtimeConfig).extras(rustCrate) diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/ClientContextParamDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/ClientContextParamDecorator.kt index b5957deda2ee0a9117a7c41cd3001112733088ef..05be47f0f547a412ee8f3350ce830bbb260adbe4 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/ClientContextParamDecorator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/ClientContextParamDecorator.kt @@ -12,16 +12,16 @@ import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.rulesengine.traits.ClientContextParamDefinition import software.amazon.smithy.rulesengine.traits.ClientContextParamsTrait import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigParam import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig +import software.amazon.smithy.rust.codegen.client.smithy.generators.config.standardConfigParam import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.docs -import software.amazon.smithy.rust.codegen.core.rustlang.docsOrFallback -import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.join import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider -import software.amazon.smithy.rust.codegen.core.smithy.makeOptional import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rust.codegen.core.util.orNull import software.amazon.smithy.rust.codegen.core.util.toSnakeCase @@ -32,78 +32,35 @@ import software.amazon.smithy.rust.codegen.core.util.toSnakeCase * This handles injecting parameters like `s3::Accelerate` or `s3::ForcePathStyle`. The resulting parameters become * setters on the config builder object. */ -internal class ClientContextDecorator(ctx: CodegenContext) : ConfigCustomization() { - private val contextParams = ctx.serviceShape.getTrait()?.parameters.orEmpty().toList() - .map { (key, value) -> ContextParam.fromClientParam(key, value, ctx.symbolProvider) } +class ClientContextConfigCustomization(ctx: CodegenContext) : ConfigCustomization() { + private val configParams = ctx.serviceShape.getTrait()?.parameters.orEmpty().toList() + .map { (key, value) -> fromClientParam(key, value, ctx.symbolProvider) } + private val decorators = configParams.map { standardConfigParam(it) } - data class ContextParam(val name: String, val type: Symbol, val docs: String?) { - companion object { - private fun toSymbol(shapeType: ShapeType, symbolProvider: RustSymbolProvider): Symbol = - symbolProvider.toSymbol( - when (shapeType) { - ShapeType.STRING -> StringShape.builder().id("smithy.api#String").build() - ShapeType.BOOLEAN -> BooleanShape.builder().id("smithy.api#Boolean").build() - else -> TODO("unsupported type") - }, - ) + companion object { + fun toSymbol(shapeType: ShapeType, symbolProvider: RustSymbolProvider): Symbol = + symbolProvider.toSymbol( + when (shapeType) { + ShapeType.STRING -> StringShape.builder().id("smithy.api#String").build() + ShapeType.BOOLEAN -> BooleanShape.builder().id("smithy.api#Boolean").build() + else -> TODO("unsupported type") + }, + ) - fun fromClientParam( - name: String, - definition: ClientContextParamDefinition, - symbolProvider: RustSymbolProvider, - ): ContextParam { - return ContextParam( - RustReservedWords.escapeIfNeeded(name.toSnakeCase()), - toSymbol(definition.type, symbolProvider), - definition.documentation.orNull(), - ) - } + fun fromClientParam( + name: String, + definition: ClientContextParamDefinition, + symbolProvider: RustSymbolProvider, + ): ConfigParam { + return ConfigParam( + RustReservedWords.escapeIfNeeded(name.toSnakeCase()), + toSymbol(definition.type, symbolProvider), + definition.documentation.orNull()?.let { writable { docs(it) } }, + ) } } override fun section(section: ServiceConfig): Writable { - return when (section) { - is ServiceConfig.ConfigStruct -> writable { - contextParams.forEach { param -> - rust("pub (crate) ${param.name}: #T,", param.type.makeOptional()) - } - } - ServiceConfig.ConfigImpl -> emptySection - ServiceConfig.BuilderStruct -> writable { - contextParams.forEach { param -> - rust("${param.name}: #T,", param.type.makeOptional()) - } - } - ServiceConfig.BuilderImpl -> writable { - contextParams.forEach { param -> - docsOrFallback(param.docs) - rust( - """ - pub fn ${param.name}(mut self, ${param.name}: impl Into<#T>) -> Self { - self.${param.name} = Some(${param.name}.into()); - self - }""", - param.type, - ) - - docsOrFallback(param.docs) - rust( - """ - pub fn set_${param.name}(&mut self, ${param.name}: Option<#T>) -> &mut Self { - self.${param.name} = ${param.name}; - self - } - """, - param.type, - ) - } - } - ServiceConfig.BuilderBuild -> writable { - contextParams.forEach { param -> - rust("${param.name}: self.${param.name},") - } - } - else -> emptySection - } + return decorators.map { it.section(section) }.join("\n") } } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointTypesGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointTypesGenerator.kt index 13848310b73ce303a25bc90a1e2eda4cf7e8c244..2f6e496e69fb482c4d98e8ec9830ab18c8d45590 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointTypesGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointTypesGenerator.kt @@ -22,9 +22,9 @@ import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType * This exposes [RuntimeType]s for the individual components of endpoints 2.0 */ class EndpointTypesGenerator( - codegenContext: ClientCodegenContext, + private val codegenContext: ClientCodegenContext, private val rules: EndpointRuleSet?, - private val tests: List, + val tests: List, ) { val params: Parameters = rules?.parameters ?: Parameters.builder().build() private val runtimeConfig = codegenContext.runtimeConfig @@ -45,7 +45,16 @@ class EndpointTypesGenerator( rules?.let { EndpointResolverGenerator(stdlib, runtimeConfig).defaultEndpointResolver(it) } fun testGenerator(): Writable = - defaultResolver()?.let { EndpointTestGenerator(tests, paramsStruct(), it, params, runtimeConfig).generate() } + defaultResolver()?.let { + EndpointTestGenerator( + tests, + paramsStruct(), + it, + params, + codegenContext = codegenContext, + endpointCustomizations = codegenContext.rootDecorator.endpointCustomizations(codegenContext), + ).generate() + } ?: {} /** @@ -56,7 +65,7 @@ class EndpointTypesGenerator( */ fun builtInFor(parameter: Parameter, config: String): Writable? { val defaultProviders = customizations - .mapNotNull { it.builtInDefaultValue(parameter, config) } + .mapNotNull { it.loadBuiltInFromServiceConfig(parameter, config) } if (defaultProviders.size > 1) { error("Multiple providers provided a value for the builtin $parameter") } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointsDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointsDecorator.kt index 3febf750e827bdd129a99970c9c7ad6e5af3d7a8..3b8edb69ad4e3f1565c476c5e4966263bf8b4295 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointsDecorator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointsDecorator.kt @@ -43,18 +43,44 @@ interface EndpointCustomization { * Provide the default value for [parameter] given a reference to the service config struct ([configRef]) * * If this parameter is not recognized, return null. + * + * Example: + * ```kotlin + * override fun loadBuiltInFromServiceConfig(parameter: Parameter, configRef: String): Writable? { + * return when (parameter.builtIn) { + * Builtins.REGION.builtIn -> writable { rust("$configRef.region.as_ref().map(|r|r.as_ref().to_owned())") } + * else -> null + * } + * } + * ``` */ - fun builtInDefaultValue(parameter: Parameter, configRef: String): Writable? = null + fun loadBuiltInFromServiceConfig(parameter: Parameter, configRef: String): Writable? = null /** - * Provide a list of additional endpoints standard library functions that rules can use + * Set a given builtIn value on the service config builder. If this builtIn is not recognized, return null + * + * Example: + * ```kotlin + * override fun setBuiltInOnServiceConfig(name: String, value: Node, configBuilderRef: String): Writable? { + * if (name != Builtins.REGION.builtIn.get()) { + * return null + * } + * return writable { + * rustTemplate( + * "let $configBuilderRef = $configBuilderRef.region(#{Region}::new(${value.expectStringNode().value.dq()}));", + * "Region" to region(codegenContext.runtimeConfig).resolve("Region"), + * ) + * } + * } + * ``` */ - fun customRuntimeFunctions(codegenContext: ClientCodegenContext): List = listOf() + + fun setBuiltInOnServiceConfig(name: String, value: Node, configBuilderRef: String): Writable? = null /** - * Set a given builtIn value on the service config builder. If this builtIn is not recognized, return null + * Provide a list of additional endpoints standard library functions that rules can use */ - fun setBuiltInOnConfig(name: String, value: Node, configBuilderRef: String): Writable? = null + fun customRuntimeFunctions(codegenContext: ClientCodegenContext): List = listOf() } /** @@ -101,7 +127,7 @@ class EndpointsDecorator : ClientCodegenDecorator { codegenContext: ClientCodegenContext, baseCustomizations: List, ): List { - return baseCustomizations + ClientContextDecorator(codegenContext) + + return baseCustomizations + ClientContextConfigCustomization(codegenContext) + EndpointConfigCustomization(codegenContext, EndpointTypesGenerator.fromContext(codegenContext)) } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointTestGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointTestGenerator.kt index a8130d449180d79a4bd9d7f266ac760357b983f2..183e25d33e7ed59c5e7dcdde23cf913d0cd647b8 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointTestGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointTestGenerator.kt @@ -10,8 +10,11 @@ import software.amazon.smithy.rulesengine.language.syntax.Identifier import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameters import software.amazon.smithy.rulesengine.traits.EndpointTestCase import software.amazon.smithy.rulesengine.traits.ExpectedEndpoint +import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointCustomization import software.amazon.smithy.rust.codegen.client.smithy.endpoint.Types import software.amazon.smithy.rust.codegen.client.smithy.endpoint.rustName +import software.amazon.smithy.rust.codegen.client.smithy.generators.clientInstantiator +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.docs import software.amazon.smithy.rust.codegen.core.rustlang.escape @@ -20,7 +23,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable -import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig +import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.util.PANIC import software.amazon.smithy.rust.codegen.core.util.dq @@ -31,8 +34,13 @@ internal class EndpointTestGenerator( private val paramsType: RuntimeType, private val resolverType: RuntimeType, private val params: Parameters, - runtimeConfig: RuntimeConfig, + private val endpointCustomizations: List, + codegenContext: CodegenContext, + ) { + private val runtimeConfig = codegenContext.runtimeConfig + private val serviceShape = codegenContext.serviceShape + private val model = codegenContext.model private val types = Types(runtimeConfig) private val codegenScope = arrayOf( "Endpoint" to types.smithyEndpoint, @@ -40,52 +48,64 @@ internal class EndpointTestGenerator( "Error" to types.resolveEndpointError, "Document" to RuntimeType.document(runtimeConfig), "HashMap" to RuntimeType.HashMap, + "capture_request" to CargoDependency.smithyClient(runtimeConfig) + .withFeature("test-util").toType().resolve("test_connection::capture_request"), ) + private val instantiator = clientInstantiator(codegenContext) + + private fun EndpointTestCase.docs(): Writable { + val self = this + return writable { docs(self.documentation.orElse("no docs")) } + } + + private fun generateBaseTest(testCase: EndpointTestCase, id: Int): Writable = writable { + rustTemplate( + """ + #{docs:W} + ##[test] + fn test_$id() { + use #{ResolveEndpoint}; + let params = #{params:W}; + let resolver = #{resolver}::new(); + let endpoint = resolver.resolve_endpoint(¶ms); + #{assertion:W} + } + """, + *codegenScope, + "docs" to testCase.docs(), + "params" to params(testCase), + "resolver" to resolverType, + "assertion" to writable { + testCase.expect.endpoint.ifPresent { endpoint -> + rustTemplate( + """ + let endpoint = endpoint.expect("Expected valid endpoint: ${escape(endpoint.url)}"); + assert_eq!(endpoint, #{expected:W}); + """, + *codegenScope, "expected" to generateEndpoint(endpoint), + ) + } + testCase.expect.error.ifPresent { error -> + val expectedError = + escape("expected error: $error [${testCase.documentation.orNull() ?: "no docs"}]") + rustTemplate( + """ + let error = endpoint.expect_err(${expectedError.dq()}); + assert_eq!(format!("{}", error), ${escape(error).dq()}) + """, + *codegenScope, + ) + } + }, + ) + } + fun generate(): Writable = writable { var id = 0 testCases.forEach { testCase -> id += 1 - - rustTemplate( - """ - #{docs:W} - ##[test] - fn test_$id() { - use #{ResolveEndpoint}; - let params = #{params:W}; - let resolver = #{resolver}::new(); - let endpoint = resolver.resolve_endpoint(¶ms); - #{assertion:W} - } - """, - *codegenScope, - "docs" to writable { docs(testCase.documentation.orNull() ?: "no docs") }, - "params" to params(testCase), - "resolver" to resolverType, - "assertion" to writable { - testCase.expect.endpoint.ifPresent { endpoint -> - rustTemplate( - """ - let endpoint = endpoint.expect("Expected valid endpoint: ${escape(endpoint.url)}"); - assert_eq!(endpoint, #{expected:W}); - """, - *codegenScope, "expected" to generateEndpoint(endpoint), - ) - } - testCase.expect.error.ifPresent { error -> - val expectedError = - escape("expected error: $error [${testCase.documentation.orNull() ?: "no docs"}]") - rustTemplate( - """ - let error = endpoint.expect_err(${expectedError.dq()}); - assert_eq!(format!("{}", error), ${escape(error).dq()}) - """, - *codegenScope, - ) - } - }, - ) + generateBaseTest(testCase, id)(this) } } @@ -118,6 +138,7 @@ internal class EndpointTestGenerator( }.join(","), ) } + is Value.Integer -> rust(value.expectInteger().toString()) is Value.Record -> @@ -140,6 +161,7 @@ internal class EndpointTestGenerator( } rustTemplate("out") } + else -> PANIC("unexpected type: $value") } } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/IdempotencyTokenProviderCustomization.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/IdempotencyTokenProviderCustomization.kt index 320c1ee0114c599507c5ee86c7904f641ff82c55..eb7057c32c64724c460b1b03fd45547b1974616c 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/IdempotencyTokenProviderCustomization.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/IdempotencyTokenProviderCustomization.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.rust.codegen.client.smithy.generators.config import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.customize.NamedSectionGenerator @@ -20,6 +21,7 @@ class IdempotencyTokenProviderCustomization : NamedSectionGenerator writable { rust("pub (crate) make_token: #T::IdempotencyTokenProvider,", RuntimeType.IdempotencyToken) } + ServiceConfig.ConfigImpl -> writable { rust( """ @@ -33,24 +35,36 @@ class IdempotencyTokenProviderCustomization : NamedSectionGenerator writable { rust("make_token: Option<#T::IdempotencyTokenProvider>,", RuntimeType.IdempotencyToken) } + ServiceConfig.BuilderImpl -> writable { - rust( + rustTemplate( """ /// Sets the idempotency token provider to use for service calls that require tokens. - pub fn make_token(mut self, make_token: impl Into<#T::IdempotencyTokenProvider>) -> Self { - self.make_token = Some(make_token.into()); + pub fn make_token(mut self, make_token: impl Into<#{TokenProvider}>) -> Self { + self.set_make_token(Some(make_token.into())); + self + } + + /// Sets the idempotency token provider to use for service calls that require tokens. + pub fn set_make_token(&mut self, make_token: Option<#{TokenProvider}>) -> &mut Self { + self.make_token = make_token; self } """, - RuntimeType.IdempotencyToken, + "TokenProvider" to RuntimeType.IdempotencyToken.resolve("IdempotencyTokenProvider"), ) } + ServiceConfig.BuilderBuild -> writable { rust("make_token: self.make_token.unwrap_or_else(#T::default_provider),", RuntimeType.IdempotencyToken) } + + is ServiceConfig.DefaultForTests -> writable { rust("""${section.configBuilderRef}.set_make_token(Some("00000000-0000-4000-8000-000000000000".into()));""") } + else -> writable { } } } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/ServiceConfigGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/ServiceConfigGenerator.kt index 00179b3216c4e89e10f80e78790861730f8cdbe6..5966c401c33466c343a46e965bd2bb002dacfd49 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/ServiceConfigGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/config/ServiceConfigGenerator.kt @@ -5,19 +5,27 @@ package software.amazon.smithy.rust.codegen.client.smithy.generators.config +import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.Model import software.amazon.smithy.model.knowledge.OperationIndex import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.traits.IdempotencyTokenTrait +import software.amazon.smithy.rust.codegen.client.smithy.customize.TestUtilFeature +import software.amazon.smithy.rust.codegen.core.rustlang.Attribute import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.docs +import software.amazon.smithy.rust.codegen.core.rustlang.docsOrFallback import software.amazon.smithy.rust.codegen.core.rustlang.raw +import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.customize.NamedSectionGenerator import software.amazon.smithy.rust.codegen.core.smithy.customize.Section +import software.amazon.smithy.rust.codegen.core.smithy.makeOptional import software.amazon.smithy.rust.codegen.core.util.hasTrait /** @@ -81,12 +89,71 @@ sealed class ServiceConfig(name: String) : Section(name) { * A section for extra functionality that needs to be defined with the config module */ object Extras : ServiceConfig("Extras") + + /** + * The set default value of a field for use in tests, e.g `${configBuilderRef}.set_credentials(Credentials::for_tests())` + */ + data class DefaultForTests(val configBuilderRef: String) : ServiceConfig("DefaultForTests") +} + +data class ConfigParam(val name: String, val type: Symbol, val setterDocs: Writable?, val getterDocs: Writable? = null) + +/** + * Config customization for a config param with no special behavior: + * 1. `pub(crate)` field + * 2. convenience setter (non-optional) + * 3. standard setter (&mut self) + */ +fun standardConfigParam(param: ConfigParam): ConfigCustomization = object : ConfigCustomization() { + override fun section(section: ServiceConfig): Writable { + return when (section) { + is ServiceConfig.ConfigStruct -> writable { + docsOrFallback(param.getterDocs) + rust("pub (crate) ${param.name}: #T,", param.type.makeOptional()) + } + + ServiceConfig.ConfigImpl -> emptySection + ServiceConfig.BuilderStruct -> writable { + rust("${param.name}: #T,", param.type.makeOptional()) + } + + ServiceConfig.BuilderImpl -> writable { + docsOrFallback(param.setterDocs) + rust( + """ + pub fn ${param.name}(mut self, ${param.name}: impl Into<#T>) -> Self { + self.${param.name} = Some(${param.name}.into()); + self + }""", + param.type, + ) + + docsOrFallback(param.setterDocs) + rust( + """ + pub fn set_${param.name}(&mut self, ${param.name}: Option<#T>) -> &mut Self { + self.${param.name} = ${param.name}; + self + } + """, + param.type, + ) + } + + ServiceConfig.BuilderBuild -> writable { + rust("${param.name}: self.${param.name},") + } + + else -> emptySection + } + } } fun ServiceShape.needsIdempotencyToken(model: Model): Boolean { val operationIndex = OperationIndex.of(model) val topDownIndex = TopDownIndex.of(model) - return topDownIndex.getContainedOperations(this.id).flatMap { operationIndex.getInputMembers(it).values }.any { it.hasTrait() } + return topDownIndex.getContainedOperations(this.id).flatMap { operationIndex.getInputMembers(it).values } + .any { it.hasTrait() } } typealias ConfigCustomization = NamedSectionGenerator @@ -111,7 +178,10 @@ typealias ConfigCustomization = NamedSectionGenerator class ServiceConfigGenerator(private val customizations: List = listOf()) { companion object { - fun withBaseBehavior(codegenContext: CodegenContext, extraCustomizations: List): ServiceConfigGenerator { + fun withBaseBehavior( + codegenContext: CodegenContext, + extraCustomizations: List, + ): ServiceConfigGenerator { val baseFeatures = mutableListOf() if (codegenContext.serviceShape.needsIdempotencyToken(codegenContext.model)) { baseFeatures.add(IdempotencyTokenProviderCustomization()) @@ -168,6 +238,25 @@ class ServiceConfigGenerator(private val customizations: List &mut Self") { + customizations.forEach { it.section(ServiceConfig.DefaultForTests("self"))(this) } + rust("self") + } + + testUtilOnly.render(this) + Attribute.AllowUnusedMut.render(this) + docs("Apply test defaults to the builder") + rustBlock("pub fn with_test_defaults(mut self) -> Self") { + rust("self.set_test_defaults(); self") + } + docs("Builds a [`Config`].") rustBlock("pub fn build(self) -> Config") { rustBlock("Config") { diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ProtocolTestGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ProtocolTestGenerator.kt index 98d229558e5153735bff09c3470245e363d8a8a3..476e67ef5a0d5e25f42050ec99a222651950d37a 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ProtocolTestGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ProtocolTestGenerator.kt @@ -12,7 +12,6 @@ import software.amazon.smithy.model.shapes.FloatShape import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.traits.ErrorTrait -import software.amazon.smithy.model.traits.IdempotencyTokenTrait import software.amazon.smithy.protocoltests.traits.AppliesTo import software.amazon.smithy.protocoltests.traits.HttpMessageTestCase import software.amazon.smithy.protocoltests.traits.HttpRequestTestCase @@ -38,7 +37,6 @@ import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.generators.error.errorSymbol import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ProtocolSupport import software.amazon.smithy.rust.codegen.core.util.dq -import software.amazon.smithy.rust.codegen.core.util.findMemberWithTrait import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.core.util.inputShape @@ -167,20 +165,17 @@ class ProtocolTestGenerator( rust("/* test case disabled for this protocol (not yet supported) */") return } - val customToken = if (inputShape.findMemberWithTrait(codegenContext.model) != null) { - """.make_token("00000000-0000-4000-8000-000000000000")""" - } else "" val customParams = httpRequestTestCase.vendorParams.getObjectMember("endpointParams").orNull()?.let { params -> writable { val customizations = codegenContext.rootDecorator.endpointCustomizations(codegenContext) params.getObjectMember("builtInParams").orNull()?.members?.forEach { (name, value) -> - customizations.firstNotNullOf { it.setBuiltInOnConfig(name.value, value, "builder") }(this) + customizations.firstNotNullOf { it.setBuiltInOnServiceConfig(name.value, value, "builder") }(this) } } } ?: writable { } rustTemplate( """ - let builder = #{Config}::Config::builder().endpoint_resolver("https://example.com")$customToken; + let builder = #{Config}::Config::builder().with_test_defaults().endpoint_resolver("https://example.com"); #{customParams} let config = builder.build(); diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/ClientContextParamsDecoratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/ClientContextParamsDecoratorTest.kt index bda2d808171c5b3fa79b806ed507ebf8d080caa4..cb9649020970a4ffcd9949696df7443d9e446e3b 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/ClientContextParamsDecoratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/ClientContextParamsDecoratorTest.kt @@ -6,7 +6,7 @@ package software.amazon.smithy.rust.codegen.client.endpoint import org.junit.jupiter.api.Test -import software.amazon.smithy.rust.codegen.client.smithy.endpoint.ClientContextDecorator +import software.amazon.smithy.rust.codegen.client.smithy.endpoint.ClientContextConfigCustomization import software.amazon.smithy.rust.codegen.client.testutil.testCodegenContext import software.amazon.smithy.rust.codegen.client.testutil.validateConfigCustomizations import software.amazon.smithy.rust.codegen.core.rustlang.rust @@ -52,6 +52,6 @@ class ClientContextParamsDecoratorTest { """, ) } - validateConfigCustomizations(ClientContextDecorator(testCodegenContext(model)), project) + validateConfigCustomizations(ClientContextConfigCustomization(testCodegenContext(model)), project) } } diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointResolverGeneratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointResolverGeneratorTest.kt index 3f50acb6f8f52166bce7ba853265df93368c82b0..ccd028ebcb8f736d002413c0a40e817572d300de 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointResolverGeneratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointResolverGeneratorTest.kt @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import software.amazon.smithy.codegen.core.CodegenException +import software.amazon.smithy.model.Model import software.amazon.smithy.model.node.Node import software.amazon.smithy.rulesengine.language.Endpoint import software.amazon.smithy.rulesengine.language.eval.Scope @@ -22,6 +23,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.endpoint.generators.End import software.amazon.smithy.rust.codegen.client.smithy.endpoint.generators.EndpointTestGenerator import software.amazon.smithy.rust.codegen.client.smithy.endpoint.rulesgen.SmithyEndpointsStdLib import software.amazon.smithy.rust.codegen.client.smithy.endpoint.rulesgen.awsStandardLib +import software.amazon.smithy.rust.codegen.client.testutil.testCodegenContext import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.testutil.TestRuntimeConfig import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace @@ -62,7 +64,8 @@ class EndpointResolverGeneratorTest { paramsType = EndpointParamsGenerator(suite.ruleSet().parameters).paramsStruct(), resolverType = ruleset, suite.ruleSet().parameters, - TestRuntimeConfig, + codegenContext = testCodegenContext(model = Model.builder().build()), + endpointCustomizations = listOf(), ) testGenerator.generate()(this) } @@ -87,7 +90,8 @@ class EndpointResolverGeneratorTest { paramsType = EndpointParamsGenerator(suite.ruleSet().parameters).paramsStruct(), resolverType = ruleset, suite.ruleSet().parameters, - TestRuntimeConfig, + codegenContext = testCodegenContext(Model.builder().build()), + endpointCustomizations = listOf(), ) testGenerator.generate()(this) } diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointsDecoratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointsDecoratorTest.kt index 9e47cf6618478e2024ebd6d5bfdc77e6e283fc52..15493f9135d83b3aaa53fa0c8e94535bcc5fa317 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointsDecoratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointsDecoratorTest.kt @@ -80,6 +80,9 @@ class EndpointsDecoratorTest { "params": { "Region": "test-region" }, + "operationInputs": [ + { "operationName": "TestOperation" } + ], "expect": { "endpoint": { "url": "https://failingtest.com" diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt index 56ff3d7ef58ba3026a4c136ebb1c5675e6ae19d8..f4fbfd5b70a0457860b1bead5143282ee40f2eb9 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt @@ -465,6 +465,9 @@ class Attribute(val inner: Writable) { val DenyMissingDocs = Attribute(deny("missing_docs")) val DocHidden = Attribute(doc("hidden")) val DocInline = Attribute(doc("inline")) + fun shouldPanic(expectedMessage: String) = + Attribute(macroWithArgs("should_panic", "expected = ${expectedMessage.dq()}")) + val Test = Attribute("test") val TokioTest = Attribute(RuntimeType.Tokio.resolve("test").writable) @@ -506,6 +509,8 @@ class Attribute(val inner: Writable) { fun doc(str: String): Writable = macroWithArgs("doc", writable(str)) fun not(vararg attrMacros: Writable): Writable = macroWithArgs("not", *attrMacros) + fun feature(feature: String) = writable("feature = ${feature.dq()}") + fun deprecated(since: String? = null, note: String? = null): Writable { val optionalFields = mutableListOf() if (!note.isNullOrEmpty()) { diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt index 59f9c5c40d0e6228218207d50139a93ecf56de79..2b4a17c193b7cd95942b79c3b02a3846ab887edc 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt @@ -262,27 +262,36 @@ fun > T.documentShape( } fun > T.docsOrFallback( - docs: String? = null, + docString: String? = null, autoSuppressMissingDocs: Boolean = true, note: String? = null, ): T { - when (docs?.isNotBlank()) { + val htmlDocs: (T.() -> Unit)? = when (docString?.isNotBlank()) { + true -> { { docs(normalizeHtml(escape(docString))) } } + else -> null + } + return docsOrFallback(htmlDocs, autoSuppressMissingDocs, note) +} + +fun > T.docsOrFallback( + docsWritable: (T.() -> Unit)? = null, + autoSuppressMissingDocs: Boolean = true, + note: String? = null, +): T { + if (docsWritable != null) { // If docs are modeled, then place them on the code generated shape - true -> { - this.docs(normalizeHtml(escape(docs))) - note?.also { - // Add a blank line between the docs and the note to visually differentiate - write("///") - docs("_Note: ${it}_") - } - } - // Otherwise, suppress the missing docs lint for this shape since - // the lack of documentation is a modeling issue rather than a codegen issue. - else -> if (autoSuppressMissingDocs) { - rust("##[allow(missing_docs)] // documentation missing in model") + + docsWritable(this) + note?.also { + // Add a blank line between the docs and the note to visually differentiate + write("///") + docs("_Note: ${it}_") } + } else if (autoSuppressMissingDocs) { + rust("##[allow(missing_docs)] // documentation missing in model") } - + // Otherwise, suppress the missing docs lint for this shape since + // the lack of documentation is a modeling issue rather than a codegen issue. return this } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt index 1ad33d9e952e733e1f1402fca0be5122194c1475..1c489f188f7fc0ffdb7ed01f4d0c93a3c6ebf62e 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt @@ -210,6 +210,7 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null) val Phantom = std.resolve("marker::PhantomData") val StdError = std.resolve("error::Error") val String = std.resolve("string::String") + val Bool = std.resolve("primitive::bool") val TryFrom = stdConvert.resolve("TryFrom") val Vec = std.resolve("vec::Vec") diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/Instantiator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/Instantiator.kt index 4da5b667841ad98e60308075e3a3b489762a0593..04e93f97b68d9f3df0b76cf9e1336fdd651ed46e 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/Instantiator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/Instantiator.kt @@ -42,6 +42,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.stripOuter import software.amazon.smithy.rust.codegen.core.rustlang.withBlock +import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider @@ -92,6 +93,8 @@ open class Instantiator( fun doesSetterTakeInOption(memberShape: MemberShape): Boolean } + fun generate(shape: Shape, data: Node, ctx: Ctx = Ctx()) = writable { render(this, shape, data, ctx) } + fun render(writer: RustWriter, shape: Shape, data: Node, ctx: Ctx = Ctx()) { when (shape) { // Compound Shapes diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/LetIf.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/LetIf.kt index c18cc56f746c7419c7b76b1fd2152e3769629012..2ac4da683d79019ddeedd3a3f0183a851500fe6d 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/LetIf.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/LetIf.kt @@ -24,3 +24,8 @@ fun Boolean.thenSingletonListOf(f: () -> T): List = if (this) { } else { listOf() } + +/** + * Returns this list if it is non-empty otherwise, it returns null + */ +fun List.orNullIfEmpty(): List? = this.ifEmpty { null } diff --git a/rust-runtime/aws-smithy-client/src/test_connection.rs b/rust-runtime/aws-smithy-client/src/test_connection.rs index f45c90a9365a9d3307f09b88a031bb586caa54ae..d7b0b15ecef4903b022d665358499344fe06005d 100644 --- a/rust-runtime/aws-smithy-client/src/test_connection.rs +++ b/rust-runtime/aws-smithy-client/src/test_connection.rs @@ -40,9 +40,25 @@ pub struct CaptureRequestReceiver { } impl CaptureRequestReceiver { + /// Expect that a request was sent. Returns the captured request. + /// + /// # Panics + /// If no request was received + #[track_caller] pub fn expect_request(mut self) -> http::Request { self.receiver.try_recv().expect("no request was received") } + + /// Expect that no request was captured. Panics if a request was received. + /// + /// # Panics + /// If a request was received + #[track_caller] + pub fn expect_no_request(mut self) { + self.receiver + .try_recv() + .expect_err("expected no request to be received!"); + } } impl tower::Service> for CaptureRequestHandler {