Endpoint operation input tests (#2204)

* Add support for operationInput tests

* More unfication, fix tests, docs

* Set endpoint_url only when endpoint_url is used

* Fix test-util feature

* CR feedback

* fix missing path
This commit is contained in:
Russell Cohen 2023-01-18 14:12:48 -05:00 committed by GitHub
parent 582ae85532
commit 95dc365db9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 824 additions and 376 deletions

View File

@ -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<ClientContextParamsTrait>()
// 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<AdHocSection<*>, (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<Pair<AdHocSection<*>, (Section) -> Writable>> {
return listOfNotNull(
codegenContext.model.sdkConfigSetter(codegenContext.serviceShape.id, Builtins.FIPS),
codegenContext.model.sdkConfigSetter(codegenContext.serviceShape.id, Builtins.DUALSTACK),
)
}
}

View File

@ -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<ClientCodegenDecorator> = listOf(
// General AWS Decorators
@ -38,8 +41,9 @@ val DECORATORS: List<ClientCodegenDecorator> = listOf(
AwsReadmeDecorator(),
HttpConnectorDecorator(),
AwsEndpointsStdLib(),
AddFIPSDualStackDecorator(),
*PromotedBuiltInsDecorators,
GenericSmithySdkConfigSettings(),
OperationInputTestDecorator(),
// Service specific decorators
ApiGatewayDecorator(),

View File

@ -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()
}

View File

@ -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<std::sync::Arc<dyn #{provider}::ProvideCredentials>>,", *codegenScope)
rustTemplate(
"credentials_provider: Option<std::sync::Arc<dyn #{provider}::ProvideCredentials>>,",
*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
}
}

View File

@ -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<AdHocSection<*>, (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<Pair<AdHocSection<*>, (Section) -> Writable>> {
return listOfNotNull(
codegenContext.model.sdkConfigSetter(codegenContext.serviceShape.id, builtIn, clientParam?.name),
)
}
override fun configCustomizations(
codegenContext: ClientCodegenContext,
baseCustomizations: List<ConfigCustomization>,
): List<ConfigCustomization> {
return baseCustomizations.extendIf(rulesetContainsBuiltIn(codegenContext)) {
standardConfigParam(
clientParam ?: toConfigParam(builtIn),
)
}
}
override fun endpointCustomizations(codegenContext: ClientCodegenContext): List<EndpointCustomization> = 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()

View File

@ -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<CustomRuntimeFunction> =
listOf()
},
)
}

View File

@ -35,6 +35,26 @@ object SdkConfigSection : AdHocSection<SdkConfigSection.CopySdkConfigToClientCon
*/
data class CopySdkConfigToClientConfig(val sdkConfig: String, val serviceConfigBuilder: String) :
Section("CopyConfig")
/**
* Copy a field from SDK config to service config with an optional map block.
*
* This handles the common case where the field name is identical in both cases and an accessor is used.
*
* # Examples
* ```kotlin
* SdkConfigSection.copyField("some_string_field") { rust("|s|s.to_to_string()") }
* ```
*/
fun copyField(fieldName: String, map: Writable?) = SdkConfigSection.create { section ->
{
val mapBlock = map?.let { writable { rust(".map(#W)", it) } } ?: writable { }
rustTemplate(
"${section.serviceConfigBuilder}.set_$fieldName(${section.sdkConfig}.$fieldName()#{map});",
"map" to mapBlock,
)
}
}
}
/**

View File

@ -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<EndpointCustomization> {
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)
}

View File

@ -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)
}
}

View File

@ -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<ConfigCustomization> {
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<EndpointCustomization> {
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

View File

@ -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
/**

View File

@ -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()")
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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();
""",
)
}
}
}
}
}

View File

@ -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

View File

@ -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)

View File

@ -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<ClientContextParamsTrait>()?.parameters.orEmpty().toList()
.map { (key, value) -> ContextParam.fromClientParam(key, value, ctx.symbolProvider) }
class ClientContextConfigCustomization(ctx: CodegenContext) : ConfigCustomization() {
private val configParams = ctx.serviceShape.getTrait<ClientContextParamsTrait>()?.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")
}
}

View File

@ -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<EndpointTestCase>,
val tests: List<EndpointTestCase>,
) {
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")
}

View File

@ -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
/**
* 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 setBuiltInOnServiceConfig(name: String, value: Node, configBuilderRef: String): Writable? = null
/**
* Provide a list of additional endpoints standard library functions that rules can use
*/
fun customRuntimeFunctions(codegenContext: ClientCodegenContext): List<CustomRuntimeFunction> = listOf()
/**
* Set a given builtIn value on the service config builder. If this builtIn is not recognized, return null
*/
fun setBuiltInOnConfig(name: String, value: Node, configBuilderRef: String): Writable? = null
}
/**
@ -101,7 +127,7 @@ class EndpointsDecorator : ClientCodegenDecorator {
codegenContext: ClientCodegenContext,
baseCustomizations: List<ConfigCustomization>,
): List<ConfigCustomization> {
return baseCustomizations + ClientContextDecorator(codegenContext) +
return baseCustomizations + ClientContextConfigCustomization(codegenContext) +
EndpointConfigCustomization(codegenContext, EndpointTypesGenerator.fromContext(codegenContext))
}

View File

@ -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<EndpointCustomization>,
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(&params);
#{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(&params);
#{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")
}
}

View File

@ -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<ServiceConfi
is ServiceConfig.ConfigStruct -> writable {
rust("pub (crate) make_token: #T::IdempotencyTokenProvider,", RuntimeType.IdempotencyToken)
}
ServiceConfig.ConfigImpl -> writable {
rust(
"""
@ -33,24 +35,36 @@ class IdempotencyTokenProviderCustomization : NamedSectionGenerator<ServiceConfi
RuntimeType.IdempotencyToken,
)
}
ServiceConfig.BuilderStruct -> 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 { }
}
}

View File

@ -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<IdempotencyTokenTrait>() }
return topDownIndex.getContainedOperations(this.id).flatMap { operationIndex.getInputMembers(it).values }
.any { it.hasTrait<IdempotencyTokenTrait>() }
}
typealias ConfigCustomization = NamedSectionGenerator<ServiceConfig>
@ -111,7 +178,10 @@ typealias ConfigCustomization = NamedSectionGenerator<ServiceConfig>
class ServiceConfigGenerator(private val customizations: List<ConfigCustomization> = listOf()) {
companion object {
fun withBaseBehavior(codegenContext: CodegenContext, extraCustomizations: List<ConfigCustomization>): ServiceConfigGenerator {
fun withBaseBehavior(
codegenContext: CodegenContext,
extraCustomizations: List<ConfigCustomization>,
): ServiceConfigGenerator {
val baseFeatures = mutableListOf<ConfigCustomization>()
if (codegenContext.serviceShape.needsIdempotencyToken(codegenContext.model)) {
baseFeatures.add(IdempotencyTokenProviderCustomization())
@ -168,6 +238,25 @@ class ServiceConfigGenerator(private val customizations: List<ConfigCustomizatio
customizations.forEach {
it.section(ServiceConfig.BuilderImpl)(this)
}
val testUtilOnly =
Attribute(Attribute.cfg(Attribute.any(Attribute.feature(TestUtilFeature.name), writable("test"))))
testUtilOnly.render(this)
Attribute.AllowUnusedMut.render(this)
docs("Apply test defaults to the builder")
rustBlock("pub fn set_test_defaults(&mut self) -> &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") {

View File

@ -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<IdempotencyTokenTrait>(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();

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -80,6 +80,9 @@ class EndpointsDecoratorTest {
"params": {
"Region": "test-region"
},
"operationInputs": [
{ "operationName": "TestOperation" }
],
"expect": {
"endpoint": {
"url": "https://failingtest.com"

View File

@ -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<Writable>()
if (!note.isNullOrEmpty()) {

View File

@ -262,27 +262,36 @@ fun <T : AbstractCodeWriter<T>> T.documentShape(
}
fun <T : AbstractCodeWriter<T>> T.docsOrFallback(
docs: String? = null,
docString: String? = null,
autoSuppressMissingDocs: Boolean = true,
note: String? = null,
): T {
when (docs?.isNotBlank()) {
// 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")
}
val htmlDocs: (T.() -> Unit)? = when (docString?.isNotBlank()) {
true -> { { docs(normalizeHtml(escape(docString))) } }
else -> null
}
return docsOrFallback(htmlDocs, autoSuppressMissingDocs, note)
}
fun <T : AbstractCodeWriter<T>> 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
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
}

View File

@ -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")

View File

@ -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

View File

@ -24,3 +24,8 @@ fun <T> Boolean.thenSingletonListOf(f: () -> T): List<T> = if (this) {
} else {
listOf()
}
/**
* Returns this list if it is non-empty otherwise, it returns null
*/
fun<T> List<T>.orNullIfEmpty(): List<T>? = this.ifEmpty { null }

View File

@ -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<SdkBody> {
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<http::Request<SdkBody>> for CaptureRequestHandler {