mirror of https://github.com/smithy-lang/smithy-rs
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:
parent
582ae85532
commit
95dc365db9
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
/**
|
|
@ -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()")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
""",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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(¶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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -80,6 +80,9 @@ class EndpointsDecoratorTest {
|
|||
"params": {
|
||||
"Region": "test-region"
|
||||
},
|
||||
"operationInputs": [
|
||||
{ "operationName": "TestOperation" }
|
||||
],
|
||||
"expect": {
|
||||
"endpoint": {
|
||||
"url": "https://failingtest.com"
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue