diff --git a/.brazil.json b/.brazil.json index 2d2e217845..7de96869c6 100644 --- a/.brazil.json +++ b/.brazil.json @@ -5,10 +5,12 @@ "com.squareup.okhttp3:okhttp-coroutines:5.*": "OkHttp3Coroutines-5.x", "com.squareup.okhttp3:okhttp:5.*": "OkHttp3-5.x", + "com.squareup.okhttp3:okhttp-jvm:5.*": "OkHttp3-5.x", "com.squareup.okio:okio-jvm:3.*": "OkioJvm-3.x", "io.opentelemetry:opentelemetry-api:1.*": "Maven-io-opentelemetry_opentelemetry-api-1.x", "io.opentelemetry:opentelemetry-extension-kotlin:1.*": "Maven-io-opentelemetry_opentelemetry-extension-kotlin-1.x", "org.slf4j:slf4j-api:2.*": "Maven-org-slf4j_slf4j-api-2.x", + "aws.sdk.kotlin.crt:aws-crt-kotlin:0.10.*": "AwsCrtKotlin-0.10.x", "aws.sdk.kotlin.crt:aws-crt-kotlin:0.9.*": "AwsCrtKotlin-0.9.x", "aws.sdk.kotlin.crt:aws-crt-kotlin:0.8.*": "AwsCrtKotlin-0.8.x", "com.squareup.okhttp3:okhttp:4.*": "OkHttp3-4.x", diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..968d241bfe --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: gradle + directory: / + schedule: + interval: daily # means every _weekday_ (Monday through Friday) diff --git a/.github/workflows/artifact-size-metrics.yml b/.github/workflows/artifact-size-metrics.yml index 815662b755..fce601e0f5 100644 --- a/.github/workflows/artifact-size-metrics.yml +++ b/.github/workflows/artifact-size-metrics.yml @@ -1,6 +1,6 @@ name: Artifact Size Metrics on: - pull_request: + pull_request_target: types: [ opened, synchronize, reopened, labeled, unlabeled ] branches: - main @@ -40,7 +40,7 @@ jobs: - name: Put Artifact Size Metrics in CloudWatch run: ./gradlew putArtifactSizeMetricsInCloudWatch -Prelease=${{ github.event.release.tag_name }} size-check: - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request_target' runs-on: ubuntu-latest steps: - name: Checkout Sources @@ -70,10 +70,10 @@ jobs: - name: Analyze Artifact Size Metrics run: ./gradlew analyzeArtifactSizeMetrics working-directory: smithy-kotlin + - name: Show Results uses: awslabs/aws-kotlin-repo-tools/.github/actions/artifact-size-metrics/show-results@main - with: - working-directory: smithy-kotlin + - name: Evaluate if: ${{ !contains(github.event.pull_request.labels.*.name, 'acknowledge-artifact-size-increase') }} working-directory: smithy-kotlin diff --git a/.github/workflows/changelog-verification.yml b/.github/workflows/changelog-verification.yml index 457764177f..995c03843c 100644 --- a/.github/workflows/changelog-verification.yml +++ b/.github/workflows/changelog-verification.yml @@ -4,7 +4,7 @@ permissions: id-token: write on: - pull_request: + pull_request_target: types: [ opened, synchronize, reopened, labeled, unlabeled ] branches: - main @@ -13,6 +13,7 @@ on: jobs: changelog-verification: runs-on: ubuntu-latest + if: github.event.pull_request.user.login != 'dependabot[bot]' # no changelogs for Dependabot version bumps steps: - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index fc8d7af115..5343a8bf01 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -45,6 +45,10 @@ jobs: distribution: 'corretto' java-version: 17 cache: 'gradle' + - name: Configure Gradle + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main + with: + working-directory: 'smithy-kotlin' - name: Build and Test on JVM working-directory: ./smithy-kotlin diff --git a/.github/workflows/kat-transform.yml b/.github/workflows/kat-transform.yml index b4efd9e5a9..9ed7e3ecca 100644 --- a/.github/workflows/kat-transform.yml +++ b/.github/workflows/kat-transform.yml @@ -1,7 +1,7 @@ name: Kat Transform on: - pull_request: + pull_request_target: types: [ opened, synchronize, reopened, labeled, unlabeled ] branches: - main diff --git a/.github/workflows/merge-main.yml b/.github/workflows/merge-main.yml index db1df3a142..ae4af7a055 100644 --- a/.github/workflows/merge-main.yml +++ b/.github/workflows/merge-main.yml @@ -12,4 +12,4 @@ jobs: uses: awslabs/aws-kotlin-repo-tools/.github/actions/merge-main@main with: ci-user-pat: ${{ secrets.CI_USER_PAT }} - exempt-branches: # Add any if required \ No newline at end of file + exempt-branches: # Add any if required diff --git a/CHANGELOG.md b/CHANGELOG.md index ad34f18d2e..06450764f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## [1.5.3] - 07/28/2025 + +### Features +* [#1320](https://github.com/smithy-lang/smithy-kotlin/issues/1320) Enable configuration of timeouts for calls and attempt + +## [1.5.2] - 07/24/2025 + +### Miscellaneous +* [#1339](https://github.com/smithy-lang/smithy-kotlin/issues/1339) Add documentation for OkHttp4Engine when using Android with R8 / ProGuard + +## [1.5.1] - 07/17/2025 + +## [1.5.0] - 07/17/2025 + +### Features +* Upgrade to Kotlin 2.2.0 +* [#1413](https://github.com/awslabs/aws-sdk-kotlin/issues/1413) ⚠️ **IMPORTANT**: Refactor endpoint discoverer classes into interfaces so custom implementations may be provided + +### Fixes +* [#1311](https://github.com/smithy-lang/smithy-kotlin/issues/1311) Reimplement idle connection monitoring using `okhttp3.EventListener` instead of now-internal `okhttp3.ConnectionListener` +* [#1608](https://github.com/awslabs/aws-sdk-kotlin/issues/1608) Switch to always serialize defaults in requests. Previously fields were not serialized in requests if they weren't `@required` and hadn't been changed from the default value. +* [#1413](https://github.com/awslabs/aws-sdk-kotlin/issues/1413) Favor `endpointUrl` instead of endpoint discovery if both are provided + +### Miscellaneous +* Add telemetry provider configuration to `DefaultAwsSigner` + ## [1.4.23] - 07/15/2025 ## [1.4.22] - 07/02/2025 diff --git a/build.gradle.kts b/build.gradle.kts index bdec55641a..7e5c097678 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,8 +2,8 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ +import aws.sdk.kotlin.gradle.dsl.configureJReleaser import aws.sdk.kotlin.gradle.dsl.configureLinting -import aws.sdk.kotlin.gradle.dsl.configureNexus import aws.sdk.kotlin.gradle.util.typedProp buildscript { @@ -14,6 +14,12 @@ buildscript { classpath(libs.kotlinx.atomicfu.plugin) // Add our custom gradle build logic to buildscript classpath classpath(libs.aws.kotlin.repo.tools.build.support) + /* + Enforce jackson to a version supported both by dokka and jreleaser: + https://github.com/Kotlin/dokka/issues/3472#issuecomment-1929712374 + https://github.com/Kotlin/dokka/issues/3194#issuecomment-1929382630 + */ + classpath(enforcedPlatform("com.fasterxml.jackson:jackson-bom:2.19.2")) } } @@ -79,7 +85,7 @@ dependencies { } // Publishing -configureNexus() +configureJReleaser() // Code Style val lintPaths = listOf( diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/customization/RegionSupport.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/customization/RegionSupport.kt index f144c54f9f..9b25eaaa29 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/customization/RegionSupport.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/customization/RegionSupport.kt @@ -40,7 +40,27 @@ class RegionSupport : KotlinIntegration { name = "region" symbol = KotlinTypes.String.toBuilder().nullable().build() documentation = """ - The region to sign with and make requests to. + The AWS region to sign with and make requests to. When specified, this static region configuration + takes precedence over other region resolution methods. + + The region resolution order is: + 1. Static region (if specified) + 2. Custom region provider (if configured) + 3. Default region provider chain + """.trimIndent() + } + + val RegionProviderProp: ConfigProperty = ConfigProperty { + name = "regionProvider" + symbol = RuntimeTypes.SmithyClient.Region.RegionProvider + documentation = """ + An optional region provider that determines the AWS region for client operations. When specified, this provider + takes precedence over the default region provider chain, unless a static region is explicitly configured. + + The region resolution order is: + 1. Static region (if specified) + 2. Custom region provider (if configured) + 3. Default region provider chain """.trimIndent() } } @@ -57,7 +77,7 @@ class RegionSupport : KotlinIntegration { return supportsSigv4 || hasRegionBuiltin || isAwsSdk } - override fun additionalServiceConfigProps(ctx: CodegenContext): List = listOf(RegionProp) + override fun additionalServiceConfigProps(ctx: CodegenContext): List = listOf(RegionProp, RegionProviderProp) override fun customizeEndpointResolution(ctx: ProtocolGenerator.GenerationContext): EndpointCustomization = object : EndpointCustomization { diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/AwsQuery.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/AwsQuery.kt index 48d47a7c18..853ed38a55 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/AwsQuery.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/AwsQuery.kt @@ -11,13 +11,23 @@ import software.amazon.smithy.kotlin.codegen.aws.protocols.core.AbstractQueryFor import software.amazon.smithy.kotlin.codegen.aws.protocols.core.AwsHttpBindingProtocolGenerator import software.amazon.smithy.kotlin.codegen.aws.protocols.core.QueryHttpBindingProtocolGenerator import software.amazon.smithy.kotlin.codegen.aws.protocols.formurl.QuerySerdeFormUrlDescriptorGenerator -import software.amazon.smithy.kotlin.codegen.core.* +import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import software.amazon.smithy.kotlin.codegen.core.RenderingContext +import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes +import software.amazon.smithy.kotlin.codegen.core.withBlock import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes -import software.amazon.smithy.kotlin.codegen.model.* -import software.amazon.smithy.kotlin.codegen.rendering.protocol.* -import software.amazon.smithy.kotlin.codegen.rendering.serde.* +import software.amazon.smithy.kotlin.codegen.model.buildSymbol +import software.amazon.smithy.kotlin.codegen.model.getTrait +import software.amazon.smithy.kotlin.codegen.model.hasTrait +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator +import software.amazon.smithy.kotlin.codegen.rendering.protocol.toRenderingContext +import software.amazon.smithy.kotlin.codegen.rendering.serde.FormUrlSerdeDescriptorGenerator +import software.amazon.smithy.kotlin.codegen.rendering.serde.StructuredDataParserGenerator +import software.amazon.smithy.kotlin.codegen.rendering.serde.StructuredDataSerializerGenerator +import software.amazon.smithy.kotlin.codegen.rendering.serde.XmlParserGenerator import software.amazon.smithy.model.shapes.* -import software.amazon.smithy.model.traits.* +import software.amazon.smithy.model.traits.XmlFlattenedTrait +import software.amazon.smithy.model.traits.XmlNameTrait /** * Handles generating the aws.protocols#awsQuery protocol for services. @@ -45,7 +55,7 @@ class AwsQuery : QueryHttpBindingProtocolGenerator() { writer: KotlinWriter, ) { writer.write("""checkNotNull(payload){ "unable to parse error from empty response" }""") - writer.write("#T(payload)", RuntimeTypes.AwsXmlProtocols.parseRestXmlErrorResponseNoSuspend) + writer.write("#T(payload)", RuntimeTypes.AwsXmlProtocols.parseRestXmlErrorResponse) } } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/Ec2Query.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/Ec2Query.kt index 10be8bfc6e..b84f22f442 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/Ec2Query.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/Ec2Query.kt @@ -10,7 +10,10 @@ import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.kotlin.codegen.aws.protocols.core.AbstractQueryFormUrlSerializerGenerator import software.amazon.smithy.kotlin.codegen.aws.protocols.core.QueryHttpBindingProtocolGenerator import software.amazon.smithy.kotlin.codegen.aws.protocols.formurl.QuerySerdeFormUrlDescriptorGenerator -import software.amazon.smithy.kotlin.codegen.core.* +import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import software.amazon.smithy.kotlin.codegen.core.RenderingContext +import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes +import software.amazon.smithy.kotlin.codegen.core.withBlock import software.amazon.smithy.kotlin.codegen.model.buildSymbol import software.amazon.smithy.kotlin.codegen.model.getTrait import software.amazon.smithy.kotlin.codegen.model.isNullable @@ -39,7 +42,7 @@ class Ec2Query : QueryHttpBindingProtocolGenerator() { writer: KotlinWriter, ) { writer.write("""checkNotNull(payload){ "unable to parse error from empty response" }""") - writer.write("#T(payload)", RuntimeTypes.AwsXmlProtocols.parseEc2QueryErrorResponseNoSuspend) + writer.write("#T(payload)", RuntimeTypes.AwsXmlProtocols.parseEc2QueryErrorResponse) } } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RestXml.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RestXml.kt index 7c1f45b92c..c7c834c317 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RestXml.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RestXml.kt @@ -63,7 +63,7 @@ open class RestXml : AwsHttpBindingProtocolGenerator() { writer: KotlinWriter, ) { writer.write("""checkNotNull(payload){ "unable to parse error from empty response" }""") - writer.write("#T(payload)", RuntimeTypes.AwsXmlProtocols.parseRestXmlErrorResponseNoSuspend) + writer.write("#T(payload)", RuntimeTypes.AwsXmlProtocols.parseRestXmlErrorResponse) } } diff --git a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/customization/RegionSupportTest.kt b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/customization/RegionSupportTest.kt new file mode 100644 index 0000000000..2df3e519f0 --- /dev/null +++ b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/customization/RegionSupportTest.kt @@ -0,0 +1,73 @@ +package software.amazon.smithy.kotlin.codegen.aws.customization + +import org.junit.jupiter.api.Test +import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import software.amazon.smithy.kotlin.codegen.model.expectShape +import software.amazon.smithy.kotlin.codegen.rendering.ServiceClientConfigGenerator +import software.amazon.smithy.kotlin.codegen.test.* +import software.amazon.smithy.model.shapes.ServiceShape + +class RegionSupportTest { + @Test + fun testRegionSupportProperties() { + val model = """ + namespace com.test + + use aws.protocols#awsJson1_1 + use aws.api#service + use aws.auth#sigv4 + + @service(sdkId: "service with overrides", endpointPrefix: "service-with-overrides") + @sigv4(name: "example") + @awsJson1_1 + service Example { + version: "1.0.0", + operations: [GetFoo] + } + + operation GetFoo {} + """.toSmithyModel() + + val serviceShape = model.expectShape("com.test#Example") + + val testCtx = model.newTestContext(serviceName = "Example") + val writer = KotlinWriter("com.test") + + val renderingCtx = testCtx.toRenderingContext(writer, serviceShape) + .copy(integrations = listOf(RegionSupport())) + + ServiceClientConfigGenerator(serviceShape, detectDefaultProps = false).render(renderingCtx, renderingCtx.writer) + val contents = writer.toString() + + val expectedProps = """ + public val region: String? = builder.region + public val regionProvider: RegionProvider? = builder.regionProvider + """.formatForTest() + contents.shouldContainOnlyOnceWithDiff(expectedProps) + + val expectedImpl = """ + /** + * The AWS region to sign with and make requests to. When specified, this static region configuration + * takes precedence over other region resolution methods. + * + * The region resolution order is: + * 1. Static region (if specified) + * 2. Custom region provider (if configured) + * 3. Default region provider chain + */ + public var region: String? = null + + /** + * An optional region provider that determines the AWS region for client operations. When specified, this provider + * takes precedence over the default region provider chain, unless a static region is explicitly configured. + * + * The region resolution order is: + * 1. Static region (if specified) + * 2. Custom region provider (if configured) + * 3. Default region provider chain + */ + public var regionProvider: RegionProvider? = null + """.formatForTest(indent = " ") + contents.shouldContainOnlyOnceWithDiff(expectedImpl) + } +} diff --git a/codegen/smithy-kotlin-codegen-testutils/src/main/kotlin/software/amazon/smithy/kotlin/codegen/test/ModelTestUtils.kt b/codegen/smithy-kotlin-codegen-testutils/src/main/kotlin/software/amazon/smithy/kotlin/codegen/test/ModelTestUtils.kt index e857f91fee..4c421f7fa7 100644 --- a/codegen/smithy-kotlin-codegen-testutils/src/main/kotlin/software/amazon/smithy/kotlin/codegen/test/ModelTestUtils.kt +++ b/codegen/smithy-kotlin-codegen-testutils/src/main/kotlin/software/amazon/smithy/kotlin/codegen/test/ModelTestUtils.kt @@ -8,6 +8,7 @@ import software.amazon.smithy.build.MockManifest import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.kotlin.codegen.* import software.amazon.smithy.kotlin.codegen.core.CodegenContext +import software.amazon.smithy.kotlin.codegen.core.GenerationContext import software.amazon.smithy.kotlin.codegen.core.KotlinDelegator import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration import software.amazon.smithy.kotlin.codegen.model.OperationNormalizer @@ -122,9 +123,11 @@ fun Model.newTestContext( val manifest = MockManifest() val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model = this, rootNamespace = packageName, serviceName = serviceName, settings = settings) val service = this.getShape(ShapeId.from("$packageName#$serviceName")).get().asServiceShape().get() - val delegator = KotlinDelegator(settings, this, manifest, provider, integrations) - val ctx = ProtocolGenerator.GenerationContext( + val codegenCtx = GenerationContext(this, provider, settings, generator, integrations) + val delegator = KotlinDelegator(codegenCtx, manifest, integrations) + + val generationCtx = ProtocolGenerator.GenerationContext( settings, this, service, @@ -133,7 +136,8 @@ fun Model.newTestContext( generator.protocol, delegator, ) - return TestContext(ctx, manifest, generator) + + return TestContext(generationCtx, manifest, generator) } fun TestContext.toCodegenContext() = object : CodegenContext { @@ -173,7 +177,7 @@ fun Model.defaultSettings( sdkId: String = TestModelDefault.SDK_ID, generateDefaultBuildFiles: Boolean = false, nullabilityCheckMode: CheckMode = CheckMode.CLIENT_CAREFUL, - defaultValueSerializationMode: DefaultValueSerializationMode = DefaultValueSerializationMode.WHEN_DIFFERENT, + defaultValueSerializationMode: DefaultValueSerializationMode = DefaultValueSerializationMode.DEFAULT, ): KotlinSettings { val serviceId = if (serviceName == null) { this.inferService() diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/CodegenVisitor.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/CodegenVisitor.kt index 4adf42b91f..012bd09c36 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/CodegenVisitor.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/CodegenVisitor.kt @@ -87,12 +87,12 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default() { integration.decorateSymbolProvider(settings, model, provider) } - writers = KotlinDelegator(settings, model, fileManifest, symbolProvider, integrations) - protocolGenerator = resolveProtocolGenerator(integrations, model, service, settings) - applicationProtocol = protocolGenerator?.applicationProtocol ?: ApplicationProtocol.createDefaultHttpApplicationProtocol() - baseGenerationContext = GenerationContext(model, symbolProvider, settings, protocolGenerator, integrations) + + writers = KotlinDelegator(baseGenerationContext, fileManifest, integrations) + + applicationProtocol = protocolGenerator?.applicationProtocol ?: ApplicationProtocol.createDefaultHttpApplicationProtocol() } private fun resolveProtocolGenerator( diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/KotlinSettings.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/KotlinSettings.kt index 3ee6e0633b..334285ffb3 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/KotlinSettings.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/KotlinSettings.kt @@ -5,12 +5,7 @@ package software.amazon.smithy.kotlin.codegen -import software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait -import software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait -import software.amazon.smithy.aws.traits.protocols.AwsQueryTrait -import software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait -import software.amazon.smithy.aws.traits.protocols.RestJson1Trait -import software.amazon.smithy.aws.traits.protocols.RestXmlTrait +import software.amazon.smithy.aws.traits.protocols.* import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.kotlin.codegen.lang.isValidPackageName import software.amazon.smithy.kotlin.codegen.utils.getOrNull @@ -24,10 +19,10 @@ import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.protocol.traits.Rpcv2CborTrait -import java.util.Optional +import java.util.* import java.util.logging.Logger +import java.util.stream.Collectors import kotlin.IllegalArgumentException -import kotlin.streams.toList // shapeId of service from which to generate an SDK private const val SERVICE = "service" @@ -164,7 +159,7 @@ fun Model.inferService(): ShapeId { val services = shapes(ServiceShape::class.java) .map(Shape::getId) .sorted() - .toList() + .collect(Collectors.toList()) return when { services.isEmpty() -> { @@ -273,10 +268,15 @@ enum class DefaultValueSerializationMode(val value: String) { override fun toString(): String = value companion object { + /** + * The default value serialization mode, which is [ALWAYS] + */ + val DEFAULT = ALWAYS + fun fromValue(value: String): DefaultValueSerializationMode = - values().find { - it.value == value - } ?: throw IllegalArgumentException("$value is not a valid DefaultValueSerializationMode, expected one of ${values().map { it.value }}") + requireNotNull(entries.find { it.value.equals(value, ignoreCase = true) }) { + "$value is not a valid DefaultValueSerializationMode, expected one of ${values().map { it.value }}" + } } } @@ -291,7 +291,7 @@ enum class DefaultValueSerializationMode(val value: String) { data class ApiSettings( val visibility: Visibility = Visibility.PUBLIC, val nullabilityCheckMode: CheckMode = CheckMode.CLIENT_CAREFUL, - val defaultValueSerializationMode: DefaultValueSerializationMode = DefaultValueSerializationMode.WHEN_DIFFERENT, + val defaultValueSerializationMode: DefaultValueSerializationMode = DefaultValueSerializationMode.DEFAULT, val enableEndpointAuthProvider: Boolean = false, val protocolResolutionPriority: Set = DEFAULT_PROTOCOL_RESOLUTION_PRIORITY, ) { @@ -315,7 +315,7 @@ data class ApiSettings( node.get() .getStringMemberOrDefault( DEFAULT_VALUE_SERIALIZATION_MODE, - DefaultValueSerializationMode.WHEN_DIFFERENT.value, + DefaultValueSerializationMode.DEFAULT.value, ), ) val enableEndpointAuthProvider = node.get().getBooleanMemberOrDefault(ENABLE_ENDPOINT_AUTH_PROVIDER, false) diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/AbstractCodeWriterExt.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/AbstractCodeWriterExt.kt index 2b943da042..186dcec017 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/AbstractCodeWriterExt.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/AbstractCodeWriterExt.kt @@ -150,6 +150,15 @@ inline fun , reified V> AbstractCodeWriter.getConte */ inline fun , reified V> AbstractCodeWriter.getContextValue(key: SectionKey): V = getContextValue(key.name) +/** + * Convenience function to set a typed value in the context + * @param key + */ +inline fun , reified V> AbstractCodeWriter.putContextValue( + key: SectionKey, + value: V, +): W = putContext(key.name, value) + /** * Convenience function to set context only if there is no value already associated with the given [key] */ diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/CodegenContext.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/CodegenContext.kt index 06d8b7497d..a3f714d741 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/CodegenContext.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/CodegenContext.kt @@ -8,6 +8,7 @@ package software.amazon.smithy.kotlin.codegen.core import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.kotlin.codegen.KotlinSettings import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +import software.amazon.smithy.kotlin.codegen.integration.SectionKey import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.Shape @@ -16,6 +17,10 @@ import software.amazon.smithy.model.shapes.Shape * Common codegen properties required across different codegen contexts */ interface CodegenContext { + companion object { + val Key = SectionKey("CodegenContext") + } + val model: Model val symbolProvider: SymbolProvider val settings: KotlinSettings diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegator.kt index 9ae10da763..ea0b6459f1 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegator.kt @@ -6,11 +6,9 @@ package software.amazon.smithy.kotlin.codegen.core import software.amazon.smithy.build.FileManifest import software.amazon.smithy.codegen.core.* -import software.amazon.smithy.kotlin.codegen.KotlinSettings import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration import software.amazon.smithy.kotlin.codegen.model.SymbolProperty import software.amazon.smithy.kotlin.codegen.utils.namespaceToPath -import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.Shape import java.nio.file.Paths @@ -21,13 +19,10 @@ const val DEFAULT_TEST_SOURCE_SET_ROOT = "./src/test/kotlin/" * Manages writers for Kotlin files. */ class KotlinDelegator( - private val settings: KotlinSettings, - private val model: Model, + private val ctx: CodegenContext, val fileManifest: FileManifest, - private val symbolProvider: SymbolProvider, private val integrations: List = listOf(), ) { - private val writers: MutableMap = mutableMapOf() // Tracks dependencies for source not provided by codegen that may reside in the service source tree. @@ -91,7 +86,7 @@ class KotlinDelegator( shape: Shape, block: (KotlinWriter) -> Unit, ) { - val symbol = symbolProvider.toSymbol(shape) + val symbol = ctx.symbolProvider.toSymbol(shape) useSymbolWriter(symbol, block) } @@ -151,7 +146,9 @@ class KotlinDelegator( val needsNewline = writers.containsKey(formattedFilename) val writer = writers.getOrPut(formattedFilename) { val kotlinWriter = KotlinWriter(namespace) - if (settings.debug) kotlinWriter.enableStackTraceComments(true) + kotlinWriter.putContextValue(CodegenContext.Key, ctx) + + if (ctx.settings.debug) kotlinWriter.enableStackTraceComments(true) // Register all integrations [SectionWriterBindings] on the writer. integrations.forEach { integration -> diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt index b575efae60..f967a689e3 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt @@ -37,7 +37,7 @@ private fun getDefaultRuntimeVersion(): String { // publishing info const val RUNTIME_GROUP: String = "aws.smithy.kotlin" val RUNTIME_VERSION: String = System.getProperty("smithy.kotlin.codegen.clientRuntimeVersion", getDefaultRuntimeVersion()) -val KOTLIN_COMPILER_VERSION: String = System.getProperty("smithy.kotlin.codegen.kotlinCompilerVersion", "2.1.0") +val KOTLIN_COMPILER_VERSION: String = System.getProperty("smithy.kotlin.codegen.kotlinCompilerVersion", "2.2.0") enum class SourceSet { CommonMain, @@ -134,6 +134,7 @@ data class KotlinDependency( // External third-party dependencies val KOTLIN_STDLIB = KotlinDependency(GradleConfiguration.Implementation, "kotlin", "org.jetbrains.kotlin", "kotlin-stdlib", KOTLIN_COMPILER_VERSION) val KOTLIN_TEST = KotlinDependency(GradleConfiguration.TestImplementation, "kotlin.test", "org.jetbrains.kotlin", "kotlin-test", KOTLIN_COMPILER_VERSION) + val KOTLIN_TEST_IMPL = KOTLIN_TEST.copy(config = GradleConfiguration.Implementation) } override fun getDependencies(): List { diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt index 1cbf3b407b..d5122a2f48 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt @@ -45,11 +45,11 @@ object RuntimeTypes { object HttpClient : RuntimeTypePackage(KotlinDependency.HTTP_CLIENT) { val SdkHttpClient = symbol("SdkHttpClient") - object Middleware : RuntimeTypePackage(KotlinDependency.HTTP, "middleware") { + object Middleware : RuntimeTypePackage(KotlinDependency.HTTP_CLIENT, "middleware") { val MutateHeadersMiddleware = symbol("MutateHeaders") } - object Operation : RuntimeTypePackage(KotlinDependency.HTTP, "operation") { + object Operation : RuntimeTypePackage(KotlinDependency.HTTP_CLIENT, "operation") { val AuthSchemeResolver = symbol("AuthSchemeResolver") val context = symbol("context") val EndpointResolver = symbol("EndpointResolver") @@ -68,18 +68,20 @@ object RuntimeTypes { val setResolvedEndpoint = symbol("setResolvedEndpoint") } - object Config : RuntimeTypePackage(KotlinDependency.HTTP, "config") { + object Config : RuntimeTypePackage(KotlinDependency.HTTP_CLIENT, "config") { val HttpClientConfig = symbol("HttpClientConfig") val HttpEngineConfig = symbol("HttpEngineConfig") + val TimeoutConfig = symbol("TimeoutConfig") } - object Engine : RuntimeTypePackage(KotlinDependency.HTTP, "engine") { + object Engine : RuntimeTypePackage(KotlinDependency.HTTP_CLIENT, "engine") { val HttpClientEngine = symbol("HttpClientEngine") val manage = symbol("manage", "engine.internal", isExtension = true) } - object Interceptors : RuntimeTypePackage(KotlinDependency.HTTP, "interceptors") { + object Interceptors : RuntimeTypePackage(KotlinDependency.HTTP_CLIENT, "interceptors") { val ContinueInterceptor = symbol("ContinueInterceptor") + val DiscoveredEndpointErrorInterceptor = symbol("DiscoveredEndpointErrorInterceptor") val HttpInterceptor = symbol("HttpInterceptor") val HttpChecksumRequiredInterceptor = symbol("HttpChecksumRequiredInterceptor") val FlexibleChecksumsRequestInterceptor = symbol("FlexibleChecksumsRequestInterceptor") @@ -97,7 +99,6 @@ object RuntimeTypes { } object Core : RuntimeTypePackage(KotlinDependency.CORE) { - val Clock = symbol("Clock", "time") val ExecutionContext = symbol("ExecutionContext", "operation") val ErrorMetadata = symbol("ErrorMetadata") val ServiceErrorMetadata = symbol("ServiceErrorMetadata") @@ -126,11 +127,12 @@ object RuntimeTypes { val attributesOf = symbol("attributesOf") val AttributeKey = symbol("AttributeKey") val createOrAppend = symbol("createOrAppend") + val ExpiringKeyedCache = symbol("ExpiringKeyedCache") val get = symbol("get") val mutableMultiMapOf = symbol("mutableMultiMapOf") + val PeriodicSweepCache = symbol("PeriodicSweepCache") val putIfAbsent = symbol("putIfAbsent") val putIfAbsentNotNull = symbol("putIfAbsentNotNull") - val ReadThroughCache = symbol("ReadThroughCache") val toMutableAttributes = symbol("toMutableAttributes") val emptyAttributes = symbol("emptyAttributes") } @@ -251,6 +253,10 @@ object RuntimeTypes { val Url = symbol("Url") } } + + object Region : RuntimeTypePackage(KotlinDependency.SMITHY_CLIENT, "region") { + val RegionProvider = symbol("RegionProvider") + } } object Serde : RuntimeTypePackage(KotlinDependency.SERDE) { @@ -374,6 +380,7 @@ object RuntimeTypes { val BearerTokenAuthScheme = symbol("BearerTokenAuthScheme") val BearerTokenProviderConfig = symbol("BearerTokenProviderConfig") val BearerTokenProvider = symbol("BearerTokenProvider") + val BearerToken = symbol("BearerToken") val EnvironmentBearerTokenProvider = symbol("EnvironmentBearerTokenProvider") @@ -453,8 +460,8 @@ object RuntimeTypes { val RestJsonErrorDeserializer = symbol("RestJsonErrorDeserializer") } object AwsXmlProtocols : RuntimeTypePackage(KotlinDependency.AWS_XML_PROTOCOLS) { - val parseRestXmlErrorResponseNoSuspend = symbol("parseRestXmlErrorResponseNoSuspend") - val parseEc2QueryErrorResponseNoSuspend = symbol("parseEc2QueryErrorResponseNoSuspend") + val parseRestXmlErrorResponse = symbol("parseRestXmlErrorResponse") + val parseEc2QueryErrorResponse = symbol("parseEc2QueryErrorResponse") } object SmithyRpcV2Protocols : RuntimeTypePackage(KotlinDependency.SMITHY_RPCV2_PROTOCOLS) { diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/lang/DocumentationPreprocessor.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/lang/DocumentationPreprocessor.kt index 9baf5790b9..04518e7032 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/lang/DocumentationPreprocessor.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/lang/DocumentationPreprocessor.kt @@ -57,13 +57,18 @@ class DocumentationPreprocessor : KotlinIntegration { .applyWithin("
", "
", String::escapeHtml) val parsed = Jsoup.parse(sanitized) + fun Node.emptyOrBlank(): Boolean = when { + this is TextNode -> isBlank + else -> childNodes().all { it.emptyOrBlank() } + } + parsed.body().filterDescendants( // Jsoup will preserve newlines between elements as blank text nodes. These have zero bearing on the content // of the document to begin with and only serve to complicate traversal. { it is TextNode && it.isBlank }, // Some docs contain empty definition terms, which we render as section headers. An empty section header // (literal "## \n" is invalid markdown according to dokka. - { it.nodeName() == "dt" && it.childNodes().isEmpty() }, + { it.nodeName() == "dt" && it.emptyOrBlank() }, ) return parsed diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ExceptionBaseClassGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ExceptionBaseClassGenerator.kt index aa308c5343..73c789e823 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ExceptionBaseClassGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ExceptionBaseClassGenerator.kt @@ -10,7 +10,6 @@ import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.kotlin.codegen.KotlinSettings import software.amazon.smithy.kotlin.codegen.core.* import software.amazon.smithy.kotlin.codegen.integration.SectionId -import software.amazon.smithy.kotlin.codegen.integration.SectionKey import software.amazon.smithy.kotlin.codegen.model.buildSymbol import software.amazon.smithy.kotlin.codegen.model.namespace import software.amazon.smithy.model.knowledge.TopDownIndex @@ -31,12 +30,10 @@ object ExceptionBaseClassGenerator { /** * Defines a section in which code can be added to the body of the base exception type. */ - object ExceptionBaseClassSection : SectionId { - val CodegenContext: SectionKey = SectionKey("CodegenContext") - } + object ExceptionBaseClassSection : SectionId fun render(ctx: CodegenContext, writer: KotlinWriter) { - writer.declareSection(ExceptionBaseClassSection, mapOf(ExceptionBaseClassSection.CodegenContext to ctx)) { + writer.declareSection(ExceptionBaseClassSection) { ServiceExceptionBaseClassGenerator().render(ctx, writer) } } diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/GradleGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/GradleGenerator.kt index 07f7541a06..d4123f0f03 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/GradleGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/GradleGenerator.kt @@ -6,7 +6,10 @@ package software.amazon.smithy.kotlin.codegen.rendering import software.amazon.smithy.build.FileManifest import software.amazon.smithy.kotlin.codegen.KotlinSettings -import software.amazon.smithy.kotlin.codegen.core.* +import software.amazon.smithy.kotlin.codegen.core.InlineCodeWriter +import software.amazon.smithy.kotlin.codegen.core.InlineCodeWriterFormatter +import software.amazon.smithy.kotlin.codegen.core.KOTLIN_COMPILER_VERSION +import software.amazon.smithy.kotlin.codegen.core.KotlinDependency import software.amazon.smithy.utils.AbstractCodeWriter // Determines the jvmTarget version emitted to the build file @@ -134,7 +137,7 @@ fun renderRootJvmPluginConfig(writer: GradleWriter) { """ jvm { compilations.all { - kotlinOptions.jvmTarget = #S + compilerOptions.jvmTarget = #S } testRuns["test"].executionTask.configure { useJUnitPlatform() diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ServiceClientConfigGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ServiceClientConfigGenerator.kt index 421f841f13..592b413f9a 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ServiceClientConfigGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ServiceClientConfigGenerator.kt @@ -57,6 +57,9 @@ class ServiceClientConfigGenerator( add(RuntimeConfigProperty.RetryStrategy) add(RuntimeConfigProperty.TelemetryProvider) + add(RuntimeConfigProperty.AttemptTimeout) + add(RuntimeConfigProperty.CallTimeout) + if (shape.hasTrait()) { addAll(clientContextConfigProps(shape.expectTrait())) } diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt index 5d90f376d2..476050b017 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt @@ -6,7 +6,10 @@ package software.amazon.smithy.kotlin.codegen.rendering import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.kotlin.codegen.core.* +import software.amazon.smithy.kotlin.codegen.core.RenderingContext +import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes +import software.amazon.smithy.kotlin.codegen.core.defaultName +import software.amazon.smithy.kotlin.codegen.core.withBlock import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes import software.amazon.smithy.kotlin.codegen.model.* import software.amazon.smithy.kotlin.codegen.rendering.serde.ClientErrorCorrection @@ -192,20 +195,31 @@ class StructureGenerator( for (memberShape in sortedMembers) { val target = model.expectShape(memberShape.target) - val memberName = memberNameSymbolIndex[memberShape]!!.first + val (memberName, memberSymbol) = memberNameSymbolIndex.getValue(memberShape) if (target is BlobShape && !target.hasTrait()) { - openBlock("if (#1L != null) {", memberName) - .write("if (other.#1L == null) return false", memberName) - .write("if (!#1L.contentEquals(other.#1L)) return false", memberName) - .closeBlock("} else if (other.#1L != null) return false", memberName) + if (memberSymbol.isNullable) { + openBlock("if (#1L != null) {", memberName) + .write("if (other.#1L == null) return false", memberName) + .write("if (!#1L.contentEquals(other.#1L)) return false", memberName) + .closeBlock("} else if (other.#1L != null) return false", memberName) + } else { + write("if (!#1L.contentEquals(other.#1L)) return false", memberName) + } } else if (target is ListShape && target.member.targetOrSelf(model).isBlobShape) { - openBlock("if (#L != null) {", memberName) - .write("if (other.#L == null) return false", memberName) - .write("if (#1L.size != other.#1L.size) return false", memberName) - .openBlock("for (i in #L.indices) {", memberName) - .write("if (!#1L[i].contentEquals(other.#1L[i])) return false", memberName) - .closeBlock("}") - .closeBlock("} else if (other.#1L != null) return false", memberName) + if (memberSymbol.isNullable) { + openBlock("if (#L != null) {", memberName) + .write("if (other.#L == null) return false", memberName) + .write("if (#1L.size != other.#1L.size) return false", memberName) + .openBlock("for (i in #L.indices) {", memberName) + .write("if (!#1L[i].contentEquals(other.#1L[i])) return false", memberName) + .closeBlock("}") + .closeBlock("} else if (other.#1L != null) return false", memberName) + } else { + write("if (#1L.size != other.#1L.size) return false", memberName) + .openBlock("for (i in #L.indices) {", memberName) + .write("if (!#1L[i].contentEquals(other.#1L[i])) return false", memberName) + .closeBlock("}") + } } else if (target is DoubleShape || target is FloatShape) { // NaNs must be compared using .equals() write("if (!(#1L?.equals(other.#1L) ?: (other.#1L == null))) return false", memberName) diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/DefaultEndpointDiscovererGenerator.kt similarity index 55% rename from codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGenerator.kt rename to codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/DefaultEndpointDiscovererGenerator.kt index 73b6e434da..2d21de0577 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/DefaultEndpointDiscovererGenerator.kt @@ -12,12 +12,11 @@ import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes import software.amazon.smithy.kotlin.codegen.model.buildSymbol import software.amazon.smithy.kotlin.codegen.model.expectShape import software.amazon.smithy.kotlin.codegen.model.expectTrait -import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointResolverAdapterGenerator import software.amazon.smithy.kotlin.codegen.rendering.endpoints.SdkEndpointBuiltinIntegration import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ServiceShape -class EndpointDiscovererGenerator(private val ctx: CodegenContext, private val delegator: KotlinDelegator) { +class DefaultEndpointDiscovererGenerator(private val ctx: CodegenContext, private val delegator: KotlinDelegator) { private val symbol = symbolFor(ctx.settings) private val service = ctx.model.expectShape(ctx.settings.service) private val clientSymbol = ctx.symbolProvider.toSymbol(service) @@ -30,58 +29,47 @@ class EndpointDiscovererGenerator(private val ctx: CodegenContext, private val d companion object { fun symbolFor(settings: KotlinSettings): Symbol = buildSymbol { val clientName = clientName(settings.sdkId) - name = "${clientName}EndpointDiscoverer" + name = "Default${clientName}EndpointDiscoverer" namespace = "${settings.pkg.name}.endpoints" } } fun render() { delegator.applyFileWriter(symbol) { + val service = clientName(ctx.settings.sdkId) dokka( """ - A class which looks up specific endpoints for ${ctx.settings.sdkId} calls via the `$operationName` - API. These unique endpoints are cached as appropriate to avoid unnecessary latency in subsequent - calls. + A class which looks up specific endpoints for $service calls via the `$operationName` API. These + unique endpoints are cached as appropriate to avoid unnecessary latency in subsequent calls. + @param cache An [ExpiringKeyedCache] implementation used to cache discovered hosts """.trimIndent(), ) + withBlock( - "#L class #T {", + "#1L class #2T(#1L val cache: #3T = #5T(10.#6T)) : #7T {", "}", ctx.settings.api.visibility, symbol, + RuntimeTypes.Core.Collections.ExpiringKeyedCache, + RuntimeTypes.Core.Net.Host, + RuntimeTypes.Core.Collections.PeriodicSweepCache, + KotlinTypes.Time.minutes, + EndpointDiscovererInterfaceGenerator.symbolFor(ctx.settings), ) { - write( - "private val cache = #T(10.#T, #T.System)", - RuntimeTypes.Core.Collections.ReadThroughCache, - RuntimeTypes.Core.Net.Host, - KotlinTypes.Time.minutes, - RuntimeTypes.Core.Clock, - ) - write("") renderAsEndpointResolver() write("") - renderDiscoverHost() - write("") renderInvalidate() } - write("") - write( - """private val discoveryParamsKey = #T("DiscoveryParams")""", - RuntimeTypes.Core.Collections.AttributeKey, - ) - write("private data class DiscoveryParams(private val region: String?, private val identity: String)") } } private fun KotlinWriter.renderAsEndpointResolver() { withBlock( - "internal fun asEndpointResolver(client: #T, delegate: #T) = #T { request ->", + "override fun asEndpointResolver(client: #1T, delegate: #2T): #2T = #2T { request ->", "}", clientSymbol, - EndpointResolverAdapterGenerator.getSymbol(ctx.settings), RuntimeTypes.HttpClient.Operation.EndpointResolver, ) { - // Backported from https://github.com/smithy-lang/smithy-kotlin/pull/1221; replace when merging v1.5 to main withBlock("if (client.config.#L == null) {", "}", SdkEndpointBuiltinIntegration.EndpointUrlProp.propertyName) { write("val identity = request.identity") write( @@ -90,7 +78,7 @@ class EndpointDiscovererGenerator(private val ctx: CodegenContext, private val d ) write("") write("val cacheKey = DiscoveryParams(client.config.region, identity.accessKeyId)") - write("request.context[discoveryParamsKey] = cacheKey") + write("request.context[DiscoveryParamsKey] = cacheKey") write("val discoveredHost = cache.get(cacheKey) { discoverHost(client) }") write("") write("val originalEndpoint = delegate.resolve(request)") @@ -99,6 +87,7 @@ class EndpointDiscovererGenerator(private val ctx: CodegenContext, private val d write("originalEndpoint.headers,") write("originalEndpoint.attributes,") } + // If user manually specifies endpointUrl, skip endpoint discovery closeAndOpenBlock("} else {") write("delegate.resolve(request)") @@ -106,34 +95,9 @@ class EndpointDiscovererGenerator(private val ctx: CodegenContext, private val d } } - private fun KotlinWriter.renderDiscoverHost() { - openBlock( - "private suspend fun discoverHost(client: #T): #T<#T> =", - clientSymbol, - RuntimeTypes.Core.Utils.ExpiringValue, - RuntimeTypes.Core.Net.Host, - ) - // ASSUMPTION No services which use discovery include parameters to the EP operation (despite being - // possible according to the Smithy spec). - write("client.#L()", operationName) - indent() - write(".endpoints") - withBlock("?.map { ep -> #T(", ")}", RuntimeTypes.Core.Utils.ExpiringValue) { - write("#T.parse(ep.address!!),", RuntimeTypes.Core.Net.Host) - write("#T.now() + ep.cachePeriodInMinutes.#T,", RuntimeTypes.Core.Instant, KotlinTypes.Time.minutes) - } - write("?.firstOrNull()") - write( - """?: throw #T("Unable to discover any endpoints when invoking #L!")""", - RuntimeTypes.SmithyClient.Endpoints.EndpointProviderException, - operationName, - ) - dedent(2) - } - private fun KotlinWriter.renderInvalidate() { - withBlock("internal suspend fun invalidate(context: #T) {", "}", RuntimeTypes.Core.ExecutionContext) { - write("context.getOrNull(discoveryParamsKey)?.let { cache.invalidate(it) }") + withBlock("override public suspend fun invalidate(context: #T) {", "}", RuntimeTypes.Core.ExecutionContext) { + write("context.getOrNull(DiscoveryParamsKey)?.let { cache.invalidate(it) }") } } } diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererInterfaceGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererInterfaceGenerator.kt new file mode 100644 index 0000000000..ddc70752db --- /dev/null +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererInterfaceGenerator.kt @@ -0,0 +1,94 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.kotlin.codegen.rendering.endpoints.discovery + +import software.amazon.smithy.aws.traits.clientendpointdiscovery.ClientEndpointDiscoveryTrait +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.kotlin.codegen.KotlinSettings +import software.amazon.smithy.kotlin.codegen.core.* +import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes +import software.amazon.smithy.kotlin.codegen.model.buildSymbol +import software.amazon.smithy.kotlin.codegen.model.expectShape +import software.amazon.smithy.kotlin.codegen.model.expectTrait +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ServiceShape + +class EndpointDiscovererInterfaceGenerator(private val ctx: CodegenContext, private val delegator: KotlinDelegator) { + private val symbol = symbolFor(ctx.settings) + private val service = ctx.model.expectShape(ctx.settings.service) + private val clientSymbol = ctx.symbolProvider.toSymbol(service) + private val operationName = run { + val epDiscoveryTrait = service.expectTrait() + val operation = ctx.model.expectShape(epDiscoveryTrait.operation) + operation.defaultName() + } + + companion object { + fun symbolFor(settings: KotlinSettings): Symbol = buildSymbol { + val clientName = clientName(settings.sdkId) + name = "${clientName}EndpointDiscoverer" + namespace = "${settings.pkg.name}.endpoints" + } + } + + fun render() { + delegator.applyFileWriter(symbol) { + dokka("Represents the logic for automatically discovering endpoints for ${ctx.settings.sdkId} calls") + withBlock( + "#L interface #T {", + "}", + ctx.settings.api.visibility, + symbol, + ) { + write( + "#1L fun asEndpointResolver(client: #2T, delegate: #3T): #3T", + ctx.settings.api.visibility, + clientSymbol, + RuntimeTypes.HttpClient.Operation.EndpointResolver, + ) + write("") + renderDiscoverHost() + write("") + write("public suspend fun invalidate(context: #T)", RuntimeTypes.Core.ExecutionContext) + } + write("") + write( + "#L data class DiscoveryParams(private val region: String?, private val identity: String)", + ctx.settings.api.visibility, + ) + write( + """#1L val DiscoveryParamsKey: #2T = #2T("DiscoveryParams")""", + ctx.settings.api.visibility, + RuntimeTypes.Core.Collections.AttributeKey, + ) + } + } + + private fun KotlinWriter.renderDiscoverHost() { + openBlock( + "#L suspend fun discoverHost(client: #T): #T<#T> =", + ctx.settings.api.visibility, + clientSymbol, + RuntimeTypes.Core.Utils.ExpiringValue, + RuntimeTypes.Core.Net.Host, + ) + // ASSUMPTION No services which use discovery include parameters to the EP operation (despite being + // possible according to the Smithy spec). + write("client.#L()", operationName) + indent() + write(".endpoints") + withBlock("?.map { ep -> #T(", ")}", RuntimeTypes.Core.Utils.ExpiringValue) { + write("#T.parse(ep.address!!),", RuntimeTypes.Core.Net.Host) + write("#T.now() + ep.cachePeriodInMinutes.#T,", RuntimeTypes.Core.Instant, KotlinTypes.Time.minutes) + } + write("?.firstOrNull()") + write( + """?: throw #T("Unable to discover any endpoints when invoking #L!")""", + RuntimeTypes.SmithyClient.Endpoints.EndpointProviderException, + operationName, + ) + dedent(2) + } +} diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscoveryIntegration.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscoveryIntegration.kt index 62906af4d7..4e8a7446a1 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscoveryIntegration.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscoveryIntegration.kt @@ -22,26 +22,38 @@ import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ServiceShape class EndpointDiscoveryIntegration : KotlinIntegration { - override fun additionalServiceConfigProps(ctx: CodegenContext): List { - val endpointDiscoveryOptional = ctx + companion object { + const val CLIENT_CONFIG_NAME = "endpointDiscoverer" + const val ORDER: Byte = 0 // doesn't depend on any other integrations + + fun isEnabledFor(model: Model, settings: KotlinSettings) = model + .expectShape(settings.service) + .hasTrait() + + fun isOptionalFor(ctx: CodegenContext) = ctx .model .operationShapes .none { it.getTrait()?.isRequired == true } - val discovererSymbol = EndpointDiscovererGenerator.symbolFor(ctx.settings) + } + + override fun additionalServiceConfigProps(ctx: CodegenContext): List { + val endpointDiscoveryOptional = isOptionalFor(ctx) + val interfaceSymbol = EndpointDiscovererInterfaceGenerator.symbolFor(ctx.settings) return super.additionalServiceConfigProps(ctx) + listOf( ConfigProperty { - name = "endpointDiscoverer" + name = CLIENT_CONFIG_NAME if (endpointDiscoveryOptional) { documentation = """ - The endpoint discoverer for this client, if applicable. By default, no endpoint - discovery is provided. To use endpoint discovery, set this to a valid - [${discovererSymbol.name}] instance. + The endpoint discoverer for this client, if applicable. By default, no endpoint discovery is + provided. To use endpoint discovery, set this to a valid [${interfaceSymbol.name}] instance. """.trimIndent() - symbol = discovererSymbol.asNullable() + symbol = interfaceSymbol.asNullable() } else { + val defaultImplSymbol = DefaultEndpointDiscovererGenerator.symbolFor(ctx.settings) documentation = "The endpoint discoverer for this client" - useSymbolWithNullableBuilder(discovererSymbol, "${discovererSymbol.name}()") + additionalImports = listOf(defaultImplSymbol) + useSymbolWithNullableBuilder(interfaceSymbol, "${defaultImplSymbol.name}()") } }, ) @@ -50,10 +62,11 @@ class EndpointDiscoveryIntegration : KotlinIntegration { override fun customizeMiddleware( ctx: ProtocolGenerator.GenerationContext, resolved: List, - ): List = super.customizeMiddleware(ctx, resolved) + listOf(DiscoveredEndpointMiddleware) + ): List = resolved + DiscoveredEndpointErrorMiddleware - override fun enabledForService(model: Model, settings: KotlinSettings): Boolean = - model.expectShape(settings.service).hasTrait() + override fun enabledForService(model: Model, settings: KotlinSettings): Boolean = isEnabledFor(model, settings) + + override val order = ORDER override val sectionWriters: List = listOf( SectionWriterBinding(HttpProtocolClientGenerator.EndpointResolverAdapterBinding, ::renderEndpointResolver), @@ -69,13 +82,15 @@ class EndpointDiscoveryIntegration : KotlinIntegration { null -> writer.write("#L", previousValue) true -> writer.write( - "execution.endpointResolver = config.endpointDiscoverer.asEndpointResolver(this@#L, #T(config))", + "execution.endpointResolver = config.#L.asEndpointResolver(this@#L, #T(config))", + CLIENT_CONFIG_NAME, defaultClientName, EndpointResolverAdapterGenerator.getSymbol(ctx.settings), ) false -> writer.write( - "execution.endpointResolver = config.endpointDiscoverer?.asEndpointResolver(this@#1L, #2T(config)) ?: #2T(config)", + "execution.endpointResolver = config.#1L?.asEndpointResolver(this@#2L, #3T(config)) ?: #3T(config)", + CLIENT_CONFIG_NAME, defaultClientName, EndpointResolverAdapterGenerator.getSymbol(ctx.settings), ) @@ -83,30 +98,29 @@ class EndpointDiscoveryIntegration : KotlinIntegration { } override fun writeAdditionalFiles(ctx: CodegenContext, delegator: KotlinDelegator) { - EndpointDiscovererGenerator(ctx, delegator).render() - super.writeAdditionalFiles(ctx, delegator) + EndpointDiscovererInterfaceGenerator(ctx, delegator).render() + + if (!isOptionalFor(ctx)) { + DefaultEndpointDiscovererGenerator(ctx, delegator).render() + } } } -private object DiscoveredEndpointMiddleware : ProtocolMiddleware { - override val name: String = "DiscoveredEndpointMiddleware" +private object DiscoveredEndpointErrorMiddleware : ProtocolMiddleware { + override val name: String = "DiscoveredEndpointErrorMiddleware" override fun isEnabledFor(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean = - op.getTrait()?.optionalError?.getOrNull() != null && + ctx.service.getTrait()?.optionalError?.getOrNull() != null && op.hasTrait() override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) { - val interceptor = buildSymbol { - name = "DiscoveredEndpointErrorInterceptor" - namespace(KotlinDependency.HTTP_CLIENT, "aws.smithy.kotlin.runtime.http.interceptors") - } - val errorShapeId = ctx.service.expectTrait().optionalError.get() val errorShape = ctx.model.expectShape(errorShapeId) val errorSymbol = ctx.symbolProvider.toSymbol(errorShape) writer.write( - "config.endpointDiscoverer?.let { op.interceptors.add(#T(#T, it::invalidate)) }", - interceptor, + "config.#L?.let { op.interceptors.add(#T(#T::class, it::invalidate)) }", + EndpointDiscoveryIntegration.CLIENT_CONFIG_NAME, + RuntimeTypes.HttpClient.Interceptors.DiscoveredEndpointErrorInterceptor, errorSymbol, ) } diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt index f12ab07a26..0b7c3f7e0a 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt @@ -9,8 +9,11 @@ import software.amazon.smithy.kotlin.codegen.core.* import software.amazon.smithy.kotlin.codegen.integration.SectionId import software.amazon.smithy.kotlin.codegen.integration.SectionKey import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes -import software.amazon.smithy.kotlin.codegen.model.* +import software.amazon.smithy.kotlin.codegen.model.getTrait +import software.amazon.smithy.kotlin.codegen.model.hasIdempotentTokenMember +import software.amazon.smithy.kotlin.codegen.model.hasStreamingMember import software.amazon.smithy.kotlin.codegen.model.knowledge.AuthIndex +import software.amazon.smithy.kotlin.codegen.model.operationSignature import software.amazon.smithy.kotlin.codegen.rendering.auth.AuthSchemeProviderAdapterGenerator import software.amazon.smithy.kotlin.codegen.rendering.auth.IdentityProviderConfigGenerator import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointResolverAdapterGenerator @@ -342,6 +345,8 @@ open class HttpProtocolClientGenerator( "}", RuntimeTypes.Core.ExecutionContext, ) { + putIfAbsent(RuntimeTypes.HttpClient.Operation.HttpOperationContext, "AttemptTimeout", nullable = true) + putIfAbsent(RuntimeTypes.HttpClient.Operation.HttpOperationContext, "CallTimeout", nullable = true) putIfAbsent(RuntimeTypes.SmithyClient.SdkClientOption, "ClientName") putIfAbsent(RuntimeTypes.SmithyClient.SdkClientOption, "LogMode") if (ctx.service.hasIdempotentTokenMember(ctx.model)) { diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/RuntimeConfigProperty.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/RuntimeConfigProperty.kt index 0108838d9c..36dbb1c671 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/RuntimeConfigProperty.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/RuntimeConfigProperty.kt @@ -10,6 +10,7 @@ import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.codegen.core.SymbolReference import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes +import software.amazon.smithy.kotlin.codegen.model.asNullable import software.amazon.smithy.kotlin.codegen.model.buildSymbol /** @@ -185,6 +186,31 @@ object RuntimeConfigProperty { The ordered preference of [AuthScheme] that this client will use. """.trimIndent() } + + val AttemptTimeout = ConfigProperty { + name = "attemptTimeout" + symbol = KotlinTypes.Time.Duration.asNullable() + baseClass = RuntimeTypes.HttpClient.Config.TimeoutConfig + useNestedBuilderBaseClass() + + documentation = """ + The maximum amount of time to wait for any single attempt of a request within the retry loop. By default, + the value is `null` indicating no timeout is enforced. Attempt timeouts may be retried if allowed by the + current retry policy and retry capacity. + """.trimIndent() + } + + val CallTimeout = ConfigProperty { + name = "callTimeout" + symbol = KotlinTypes.Time.Duration.asNullable() + baseClass = RuntimeTypes.HttpClient.Config.TimeoutConfig + useNestedBuilderBaseClass() + + documentation = """ + The maximum amount of time to wait for completion of a call, including any retries after the first attempt. + By default, the value is `null` indicating no timeout is enforced. Call timeouts are not retried. + """.trimIndent() + } } internal val Symbol.nestedBuilder: Symbol diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/KotlinSettingsTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/KotlinSettingsTest.kt index 2de132c9fa..7910b25cc1 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/KotlinSettingsTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/KotlinSettingsTest.kt @@ -11,6 +11,7 @@ import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.ArgumentsProvider import org.junit.jupiter.params.provider.ArgumentsSource import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.support.ParameterDeclarations import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.kotlin.codegen.test.TestModelDefault import software.amazon.smithy.kotlin.codegen.test.toSmithyModel @@ -392,7 +393,10 @@ class TestProtocolSelectionArgumentProvider : ArgumentsProvider { private const val NO_CBOR = "awsJson1_0, awsJson1_1, restJson1, restXml, awsQuery, ec2Query" } - override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( + override fun provideArguments( + parameters: ParameterDeclarations?, + context: ExtensionContext?, + ): Stream = Stream.of( Arguments.of( ALL_PROTOCOLS, "rpcv2Cbor, awsJson1_0", diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegatorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegatorTest.kt index 162f32ac92..a5fb28e902 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegatorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegatorTest.kt @@ -97,7 +97,9 @@ class KotlinDelegatorTest { Node.parse(configContents).expectObjectNode(), ) val manifest = MockManifest() - val delegator = KotlinDelegator(settings, model, manifest, KotlinSymbolProvider(model, settings)) + val symbolProvider = KotlinSymbolProvider(model, settings) + val ctx = GenerationContext(model, symbolProvider, settings, protocolGenerator = null) + val delegator = KotlinDelegator(ctx, manifest) val generatedSymbol = buildSymbol { name = "Foo" diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ExceptionGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ExceptionGeneratorTest.kt index a2272852fe..6356d3fbf6 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ExceptionGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ExceptionGeneratorTest.kt @@ -241,7 +241,7 @@ class ExceptionGeneratorTest { get() = listOf(SectionWriterBinding(ExceptionBaseClassGenerator.ExceptionBaseClassSection, exceptionSectionWriter)) private val exceptionSectionWriter = SectionWriter { writer, _ -> - val ctx = writer.getContextValue(ExceptionBaseClassGenerator.ExceptionBaseClassSection.CodegenContext) + val ctx = writer.getContextValue(CodegenContext.Key) ServiceExceptionBaseClassGenerator(exceptionBaseClassSymbol).render(ctx, writer) } } diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ServiceClientConfigGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ServiceClientConfigGeneratorTest.kt index 7f1521ee02..cb73290a86 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ServiceClientConfigGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ServiceClientConfigGeneratorTest.kt @@ -8,7 +8,10 @@ package software.amazon.smithy.kotlin.codegen.rendering import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import software.amazon.smithy.codegen.core.SymbolReference -import software.amazon.smithy.kotlin.codegen.core.* +import software.amazon.smithy.kotlin.codegen.core.CodegenContext +import software.amazon.smithy.kotlin.codegen.core.KotlinDependency +import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes import software.amazon.smithy.kotlin.codegen.loadModelFromResource @@ -42,14 +45,16 @@ class ServiceClientConfigGeneratorTest { contents.assertBalancedBracesAndParens() val expectedCtor = """ -public class Config private constructor(builder: Builder) : HttpAuthConfig, HttpClientConfig, HttpEngineConfig by builder.buildHttpEngineConfig(), IdempotencyTokenConfig, RetryClientConfig, RetryStrategyClientConfig by builder.buildRetryStrategyClientConfig(), SdkClientConfig, TelemetryConfig { +public class Config private constructor(builder: Builder) : HttpAuthConfig, HttpClientConfig, HttpEngineConfig by builder.buildHttpEngineConfig(), IdempotencyTokenConfig, RetryClientConfig, RetryStrategyClientConfig by builder.buildRetryStrategyClientConfig(), SdkClientConfig, TelemetryConfig, TimeoutConfig { """ contents.shouldContainWithDiff(expectedCtor) val expectedProps = """ override val clientName: String = builder.clientName + override val attemptTimeout: Duration? = builder.attemptTimeout override val authSchemePreference: kotlin.collections.List? = builder.authSchemePreference override val authSchemes: kotlin.collections.List = builder.authSchemes + override val callTimeout: Duration? = builder.callTimeout public val endpointProvider: TestEndpointProvider = requireNotNull(builder.endpointProvider) { "endpointProvider is a required configuration property" } override val idempotencyTokenProvider: IdempotencyTokenProvider = builder.idempotencyTokenProvider ?: IdempotencyTokenProvider.Default override val interceptors: kotlin.collections.List = builder.interceptors @@ -60,12 +65,19 @@ public class Config private constructor(builder: Builder) : HttpAuthConfig, Http contents.shouldContainWithDiff(expectedProps) val expectedBuilder = """ - public class Builder : HttpAuthConfig.Builder, HttpClientConfig.Builder, HttpEngineConfig.Builder by HttpEngineConfigImpl.BuilderImpl(), IdempotencyTokenConfig.Builder, RetryClientConfig.Builder, RetryStrategyClientConfig.Builder by RetryStrategyClientConfigImpl.BuilderImpl(), SdkClientConfig.Builder, TelemetryConfig.Builder { + public class Builder : HttpAuthConfig.Builder, HttpClientConfig.Builder, HttpEngineConfig.Builder by HttpEngineConfigImpl.BuilderImpl(), IdempotencyTokenConfig.Builder, RetryClientConfig.Builder, RetryStrategyClientConfig.Builder by RetryStrategyClientConfigImpl.BuilderImpl(), SdkClientConfig.Builder, TelemetryConfig.Builder, TimeoutConfig.Builder { /** * A reader-friendly name for the client. */ override var clientName: String = "Test" + /** + * The maximum amount of time to wait for any single attempt of a request within the retry loop. By default, + * the value is `null` indicating no timeout is enforced. Attempt timeouts may be retried if allowed by the + * current retry policy and retry capacity. + */ + override var attemptTimeout: Duration? = null + /** * The ordered preference of [AuthScheme] that this client will use. */ @@ -79,6 +91,12 @@ public class Config private constructor(builder: Builder) : HttpAuthConfig, Http */ override var authSchemes: kotlin.collections.List = emptyList() + /** + * The maximum amount of time to wait for completion of a call, including any retries after the first attempt. + * By default, the value is `null` indicating no timeout is enforced. Call timeouts are not retried. + */ + override var callTimeout: Duration? = null + /** * The endpoint provider used to determine where to make service requests. **This is an advanced config * option.** @@ -245,8 +263,10 @@ public class Config private constructor(builder: Builder) { // Expect logMode config value to override default to LogMode.Request val expectedConfigValues = """ override val clientName: String = builder.clientName + override val attemptTimeout: Duration? = builder.attemptTimeout override val authSchemePreference: kotlin.collections.List? = builder.authSchemePreference override val authSchemes: kotlin.collections.List = builder.authSchemes + override val callTimeout: Duration? = builder.callTimeout public val customProp: Int? = builder.customProp public val endpointProvider: TestEndpointProvider = requireNotNull(builder.endpointProvider) { "endpointProvider is a required configuration property" } override val idempotencyTokenProvider: IdempotencyTokenProvider = builder.idempotencyTokenProvider ?: IdempotencyTokenProvider.Default diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/DefaultEndpointDiscovererGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/DefaultEndpointDiscovererGeneratorTest.kt new file mode 100644 index 0000000000..d5e0a6e152 --- /dev/null +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/DefaultEndpointDiscovererGeneratorTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.kotlin.codegen.rendering.endpoints.discovery + +import org.junit.jupiter.api.Test +import software.amazon.smithy.build.MockManifest +import software.amazon.smithy.kotlin.codegen.test.formatForTest +import software.amazon.smithy.kotlin.codegen.test.newTestContext +import software.amazon.smithy.kotlin.codegen.test.shouldContainOnlyOnceWithDiff +import software.amazon.smithy.kotlin.codegen.test.toCodegenContext + +class DefaultEndpointDiscovererGeneratorTest { + private val renderedCodegen: String = run { + val model = model() + val testCtx = model.newTestContext() + val delegator = testCtx.generationCtx.delegator + val generator = DefaultEndpointDiscovererGenerator(testCtx.toCodegenContext(), delegator) + generator.render() + + delegator.flushWriters() + val testManifest = delegator.fileManifest as MockManifest + testManifest.expectFileString("/src/main/kotlin/com/test/endpoints/DefaultTestEndpointDiscoverer.kt") + } + + @Test + fun testClass() { + renderedCodegen.shouldContainOnlyOnceWithDiff( + """ + /** + * A class which looks up specific endpoints for Test calls via the `getEndpoints` API. These + * unique endpoints are cached as appropriate to avoid unnecessary latency in subsequent calls. + * @param cache An [ExpiringKeyedCache] implementation used to cache discovered hosts + */ + public class DefaultTestEndpointDiscoverer(public val cache: ExpiringKeyedCache = PeriodicSweepCache(10.minutes)) : TestEndpointDiscoverer { + """.trimIndent(), + ) + } + + @Test + fun testAsEndpointResolver() { + renderedCodegen.shouldContainOnlyOnceWithDiff( + """ + override fun asEndpointResolver(client: TestClient, delegate: EndpointResolver): EndpointResolver = EndpointResolver { request -> + if (client.config.endpointUrl == null) { + val identity = request.identity + require(identity is Credentials) { "Endpoint discovery requires AWS credentials" } + + val cacheKey = DiscoveryParams(client.config.region, identity.accessKeyId) + request.context[DiscoveryParamsKey] = cacheKey + val discoveredHost = cache.get(cacheKey) { discoverHost(client) } + + val originalEndpoint = delegate.resolve(request) + Endpoint( + originalEndpoint.uri.copy { host = discoveredHost }, + originalEndpoint.headers, + originalEndpoint.attributes, + ) + } else { + delegate.resolve(request) + } + } + """.formatForTest(), + ) + } + + @Test + fun testInvalidate() { + renderedCodegen.shouldContainOnlyOnceWithDiff( + """ + override public suspend fun invalidate(context: ExecutionContext) { + context.getOrNull(DiscoveryParamsKey)?.let { cache.invalidate(it) } + } + """.formatForTest(), + ) + } +} diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGeneratorTest.kt deleted file mode 100644 index 37b9e6a900..0000000000 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGeneratorTest.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package software.amazon.smithy.kotlin.codegen.rendering.endpoints.discovery - -import org.junit.jupiter.api.Test -import software.amazon.smithy.build.MockManifest -import software.amazon.smithy.kotlin.codegen.test.* - -class EndpointDiscovererGeneratorTest { - @Test - fun testClass() { - val actual = render() - - actual.shouldContainOnlyOnceWithDiff( - """ - public class TestEndpointDiscoverer { - private val cache = ReadThroughCache(10.minutes, Clock.System) - """.trimIndent(), - ) - - actual.shouldContainOnlyOnceWithDiff( - """ - } - - private val discoveryParamsKey = AttributeKey("DiscoveryParams") - private data class DiscoveryParams(private val region: String?, private val identity: String) - """.trimIndent(), - ) - } - - @Test - fun testAsEndpointResolver() { - val actual = render() - - actual.shouldContainOnlyOnceWithDiff( - """ - internal fun asEndpointResolver(client: TestClient, delegate: EndpointResolverAdapter) = EndpointResolver { request -> - if (client.config.endpointUrl == null) { - val identity = request.identity - require(identity is Credentials) { "Endpoint discovery requires AWS credentials" } - - val cacheKey = DiscoveryParams(client.config.region, identity.accessKeyId) - request.context[discoveryParamsKey] = cacheKey - val discoveredHost = cache.get(cacheKey) { discoverHost(client) } - - val originalEndpoint = delegate.resolve(request) - Endpoint( - originalEndpoint.uri.copy { host = discoveredHost }, - originalEndpoint.headers, - originalEndpoint.attributes, - ) - } else { - delegate.resolve(request) - } - } - """.formatForTest(), - ) - } - - @Test - fun testDiscoverHost() { - val actual = render() - - actual.shouldContainOnlyOnceWithDiff( - """ - private suspend fun discoverHost(client: TestClient): ExpiringValue = - client.getEndpoints() - .endpoints - ?.map { ep -> ExpiringValue( - Host.parse(ep.address!!), - Instant.now() + ep.cachePeriodInMinutes.minutes, - )} - ?.firstOrNull() - ?: throw EndpointProviderException("Unable to discover any endpoints when invoking getEndpoints!") - """.formatForTest(), - ) - } - - @Test - fun testInvalidate() { - val actual = render() - - actual.shouldContainOnlyOnceWithDiff( - """ - internal suspend fun invalidate(context: ExecutionContext) { - context.getOrNull(discoveryParamsKey)?.let { cache.invalidate(it) } - } - """.formatForTest(), - ) - } - - private fun render(): String { - val model = model() - val testCtx = model.newTestContext() - val delegator = testCtx.generationCtx.delegator - val generator = EndpointDiscovererGenerator(testCtx.toCodegenContext(), delegator) - generator.render() - - delegator.flushWriters() - val testManifest = delegator.fileManifest as MockManifest - return testManifest.expectFileString("/src/main/kotlin/com/test/endpoints/TestEndpointDiscoverer.kt") - } - - private fun model() = - """ - namespace com.test - - use aws.protocols#awsJson1_1 - use aws.api#service - use aws.auth#sigv4 - - @service(sdkId: "test") - @sigv4(name: "test") - @awsJson1_1 - @aws.api#clientEndpointDiscovery( - operation: GetEndpoints, - error: BadEndpointError - ) - service Test { - version: "1.0.0", - operations: [GetEndpoints] - } - - @error("client") - @httpError(421) - structure BadEndpointError { } - - @http(method: "GET", uri: "/endpoints") - operation GetEndpoints { - input: GetEndpointsInput - output: GetEndpointsOutput - } - - @input - structure GetEndpointsInput { } - - @output - structure GetEndpointsOutput { - Endpoints: Endpoints - } - - list Endpoints { - member: Endpoint - } - - structure Endpoint { - Address: String - CachePeriodInMinutes: Long - } - """.toSmithyModel() -} diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererInterfaceGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererInterfaceGeneratorTest.kt new file mode 100644 index 0000000000..998fc8e577 --- /dev/null +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererInterfaceGeneratorTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.kotlin.codegen.rendering.endpoints.discovery + +import org.junit.jupiter.api.Test +import software.amazon.smithy.build.MockManifest +import software.amazon.smithy.kotlin.codegen.test.formatForTest +import software.amazon.smithy.kotlin.codegen.test.newTestContext +import software.amazon.smithy.kotlin.codegen.test.shouldContainOnlyOnceWithDiff +import software.amazon.smithy.kotlin.codegen.test.toCodegenContext + +class EndpointDiscovererInterfaceGeneratorTest { + @Test + fun testInterface() { + val actual = render() + + actual.shouldContainOnlyOnceWithDiff( + """ + /** + * Represents the logic for automatically discovering endpoints for Test calls + */ + public interface TestEndpointDiscoverer { + public fun asEndpointResolver(client: TestClient, delegate: EndpointResolver): EndpointResolver + """.trimIndent(), + ) + + actual.shouldContainOnlyOnceWithDiff( + """ + public suspend fun invalidate(context: ExecutionContext) + """.trimIndent(), + ) + + actual.shouldContainOnlyOnceWithDiff( + """ + } + + public data class DiscoveryParams(private val region: String?, private val identity: String) + public val DiscoveryParamsKey: AttributeKey = AttributeKey("DiscoveryParams") + """.trimIndent(), + ) + } + + @Test + fun testDiscoverHost() { + val actual = render() + + actual.shouldContainOnlyOnceWithDiff( + """ + public suspend fun discoverHost(client: TestClient): ExpiringValue = + client.getEndpoints() + .endpoints + ?.map { ep -> ExpiringValue( + Host.parse(ep.address!!), + Instant.now() + ep.cachePeriodInMinutes.minutes, + )} + ?.firstOrNull() + ?: throw EndpointProviderException("Unable to discover any endpoints when invoking getEndpoints!") + """.formatForTest(), + ) + } + + private fun render(): String { + val model = model() + val testCtx = model.newTestContext() + val delegator = testCtx.generationCtx.delegator + val generator = EndpointDiscovererInterfaceGenerator(testCtx.toCodegenContext(), delegator) + generator.render() + + delegator.flushWriters() + val testManifest = delegator.fileManifest as MockManifest + return testManifest.expectFileString("/src/main/kotlin/com/test/endpoints/TestEndpointDiscoverer.kt") + } +} diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscoveryIntegrationTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscoveryIntegrationTest.kt index 48fec68de5..1fdb3f3973 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscoveryIntegrationTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscoveryIntegrationTest.kt @@ -36,7 +36,7 @@ class EndpointDiscoveryIntegrationTest { val contents = writer.toString() if (discoveryRequired) { - val configStr = "public val endpointDiscoverer: TestEndpointDiscoverer = builder.endpointDiscoverer ?: TestEndpointDiscoverer()" + val configStr = "public val endpointDiscoverer: TestEndpointDiscoverer = builder.endpointDiscoverer ?: DefaultTestEndpointDiscoverer()" contents.shouldContainOnlyOnceWithDiff(configStr) val builderStr = """ @@ -52,9 +52,8 @@ class EndpointDiscoveryIntegrationTest { val builderStr = """ /** - * The endpoint discoverer for this client, if applicable. By default, no endpoint - * discovery is provided. To use endpoint discovery, set this to a valid - * [TestEndpointDiscoverer] instance. + * The endpoint discoverer for this client, if applicable. By default, no endpoint discovery is + * provided. To use endpoint discovery, set this to a valid [TestEndpointDiscoverer] instance. */ public var endpointDiscoverer: TestEndpointDiscoverer? = null """.formatForTest(" ") @@ -62,6 +61,23 @@ class EndpointDiscoveryIntegrationTest { } } + @Test + fun testDiscoveredEndpointErrorMiddleware() { + val model = model() + val ctx = model.newTestContext(integrations = listOf(EndpointDiscoveryIntegration())) + val generator = MockHttpProtocolGenerator(model) + generator.generateProtocolClient(ctx.generationCtx) + + ctx.generationCtx.delegator.finalize() + ctx.generationCtx.delegator.flushWriters() + + val actual = ctx.manifest.expectFileString("/src/main/kotlin/com/test/DefaultTestClient.kt") + + val getFooMethod = actual.lines(" override suspend fun getFoo(input: GetFooRequest): GetFooResponse {", " }") + val expectedInterceptor = "config.endpointDiscoverer?.let { op.interceptors.add(DiscoveredEndpointErrorInterceptor(BadEndpointError::class, it::invalidate)) }" + getFooMethod.shouldContainOnlyOnceWithDiff(expectedInterceptor) + } + private fun model(discoveryRequired: Boolean = true) = """ namespace com.test diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscoveryTestUtils.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscoveryTestUtils.kt new file mode 100644 index 0000000000..55d58d6cc4 --- /dev/null +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscoveryTestUtils.kt @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.kotlin.codegen.rendering.endpoints.discovery + +import software.amazon.smithy.kotlin.codegen.test.toSmithyModel + +fun model() = + // language=smithy + """ + namespace com.test + + use aws.protocols#awsJson1_1 + use aws.api#service + use aws.auth#sigv4 + + @service(sdkId: "test") + @sigv4(name: "test") + @awsJson1_1 + @aws.api#clientEndpointDiscovery( + operation: GetEndpoints, + error: BadEndpointError + ) + service Test { + version: "1.0.0", + operations: [GetEndpoints] + } + + @error("client") + @httpError(421) + structure BadEndpointError { } + + @http(method: "GET", uri: "/endpoints") + operation GetEndpoints { + input: GetEndpointsInput + output: GetEndpointsOutput + } + + @input + structure GetEndpointsInput { } + + @output + structure GetEndpointsOutput { + Endpoints: Endpoints + } + + list Endpoints { + member: Endpoint + } + + structure Endpoint { + Address: String + CachePeriodInMinutes: Long + } + """.toSmithyModel() diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializerTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializerTest.kt index 95138ad529..097db4b4c8 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializerTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializerTest.kt @@ -53,7 +53,8 @@ class HttpStringValuesMapSerializerTest { @Test fun `it handles primitive header shapes when different mode`() { - val contents = getTestContents(defaultModel, "com.test#PrimitiveShapesOperation", HttpBinding.Location.HEADER) + val settings = defaultModel.defaultSettings(defaultValueSerializationMode = DefaultValueSerializationMode.WHEN_DIFFERENT) + val contents = getTestContents(defaultModel, "com.test#PrimitiveShapesOperation", HttpBinding.Location.HEADER, settings) contents.assertBalancedBracesAndParens() val expectedContents = """ @@ -68,7 +69,8 @@ class HttpStringValuesMapSerializerTest { @Test fun `it handles primitive query shapes when different mode`() { - val contents = getTestContents(defaultModel, "com.test#PrimitiveShapesOperation", HttpBinding.Location.QUERY) + val settings = defaultModel.defaultSettings(defaultValueSerializationMode = DefaultValueSerializationMode.WHEN_DIFFERENT) + val contents = getTestContents(defaultModel, "com.test#PrimitiveShapesOperation", HttpBinding.Location.QUERY, settings) contents.assertBalancedBracesAndParens() val expectedContents = """ @@ -129,7 +131,8 @@ class HttpStringValuesMapSerializerTest { } """.prependNamespaceAndService(operations = listOf("Foo")).toSmithyModel() - val contents = getTestContents(model, "com.test#Foo", HttpBinding.Location.HEADER) + val settings = defaultModel.defaultSettings(defaultValueSerializationMode = DefaultValueSerializationMode.WHEN_DIFFERENT) + val contents = getTestContents(model, "com.test#Foo", HttpBinding.Location.HEADER, settings) contents.assertBalancedBracesAndParens() val expectedContents = """ diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt index 347da007cb..c0eae1f05b 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt @@ -72,7 +72,8 @@ class SerializeStructGeneratorTest { } """.trimIndent() - val actual = codegenSerializerForShape(model, "com.test#Foo") + val settings = model.defaultSettings(defaultValueSerializationMode = DefaultValueSerializationMode.WHEN_DIFFERENT) + val actual = codegenSerializerForShape(model, "com.test#Foo", settings = settings) actual.shouldContainOnlyOnceWithDiff(expected) } diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt index 49ee90f484..038a2ff7da 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt @@ -11,13 +11,10 @@ import software.amazon.smithy.build.MockManifest import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.kotlin.codegen.KotlinCodegenPlugin import software.amazon.smithy.kotlin.codegen.KotlinSettings -import software.amazon.smithy.kotlin.codegen.core.CodegenContext +import software.amazon.smithy.kotlin.codegen.core.GenerationContext import software.amazon.smithy.kotlin.codegen.core.KotlinDelegator -import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration import software.amazon.smithy.kotlin.codegen.loadModelFromResource -import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator import software.amazon.smithy.kotlin.codegen.test.* -import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.ShapeId import kotlin.test.Test import kotlin.test.assertEquals @@ -130,16 +127,9 @@ class ServiceWaitersGeneratorTest { val service = model.getShape(ShapeId.from(TestModelDefault.SERVICE_SHAPE_ID)).get().asServiceShape().get() val settings = KotlinSettings(service.id, KotlinSettings.PackageSettings(TestModelDefault.NAMESPACE, TestModelDefault.MODEL_VERSION), sdkId = service.id.name) - val ctx = object : CodegenContext { - override val model: Model = model - override val symbolProvider: SymbolProvider = provider - override val settings: KotlinSettings = settings - override val protocolGenerator: ProtocolGenerator? = null - override val integrations: List = listOf() - } - val manifest = MockManifest() - val delegator = KotlinDelegator(settings, model, manifest, provider) + val ctx = GenerationContext(model, provider, settings, protocolGenerator = null) + val delegator = KotlinDelegator(ctx, manifest) val generator = ServiceWaitersGenerator() generator.writeAdditionalFiles(ctx, delegator) diff --git a/gradle.properties b/gradle.properties index 8f39c9c434..5760061e6b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,10 +14,10 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.24-SNAPSHOT +sdkVersion=1.5.4-SNAPSHOT # codegen -codegenVersion=0.34.24-SNAPSHOT +codegenVersion=0.35.4-SNAPSHOT # FIXME Remove after Dokka 2.0 Gradle plugin is stable org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c801f80d83..bdd68f6896 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,37 +1,37 @@ [versions] -kotlin-version = "2.1.0" +kotlin-version = "2.2.0" dokka-version = "2.0.0" -aws-kotlin-repo-tools-version = "0.4.28-kn" +aws-kotlin-repo-tools-version = "0.4.30-kn" # libs -coroutines-version = "1.9.0" -atomicfu-version = "0.25.0" -okhttp-version = "5.0.0-alpha.14" +coroutines-version = "1.10.2" +atomicfu-version = "0.29.0" +okhttp-version = "5.1.0" okhttp4-version = "4.12.0" -okio-version = "3.9.1" +okio-version = "3.16.0" otel-version = "1.45.0" slf4j-version = "2.0.16" slf4j-v1x-version = "1.7.36" -crt-kotlin-version = "0.9.3-SNAPSHOT" -micrometer-version = "1.14.2" -binary-compatibility-validator-version = "0.16.3" +crt-kotlin-version = "0.10.0" +micrometer-version = "1.15.2" +binary-compatibility-validator-version = "0.18.1" kotlin-multiplatform-bignum-version = "0.3.10" kotlinx-datetime-version = "0.6.1" # codegen -smithy-version = "1.60.2" +smithy-version = "1.61.0" # testing -junit-version = "5.10.5" +junit-version = "5.13.4" kotest-version = "5.9.1" -kotlin-compile-testing-version = "0.7.0" -kotlinx-benchmark-version = "0.4.12" +kotlin-compile-testing-version = "0.8.0" +kotlinx-benchmark-version = "0.4.14" kotlinx-serialization-version = "1.7.3" docker-java-version = "3.4.0" -ktor-version = "3.1.1" +ktor-version = "3.2.3" kaml-version = "0.55.0" -jsoup-version = "1.19.1" +jsoup-version = "1.21.1" [libraries] aws-kotlin-repo-tools-build-support = { module="aws.sdk.kotlin.gradle:build-support", version.ref = "aws-kotlin-repo-tools-version" } diff --git a/runtime/auth/aws-credentials/api/aws-credentials.api b/runtime/auth/aws-credentials/api/aws-credentials.api index 15df96aff6..429f5eec3b 100644 --- a/runtime/auth/aws-credentials/api/aws-credentials.api +++ b/runtime/auth/aws-credentials/api/aws-credentials.api @@ -18,9 +18,9 @@ public abstract interface class aws/smithy/kotlin/runtime/auth/awscredentials/Cl public abstract interface class aws/smithy/kotlin/runtime/auth/awscredentials/Credentials : aws/smithy/kotlin/runtime/identity/Identity { public static final field Companion Laws/smithy/kotlin/runtime/auth/awscredentials/Credentials$Companion; public abstract fun getAccessKeyId ()Ljava/lang/String; - public abstract fun getProviderName ()Ljava/lang/String; + public fun getProviderName ()Ljava/lang/String; public abstract fun getSecretAccessKey ()Ljava/lang/String; - public abstract fun getSessionToken ()Ljava/lang/String; + public fun getSessionToken ()Ljava/lang/String; } public final class aws/smithy/kotlin/runtime/auth/awscredentials/Credentials$Companion { diff --git a/runtime/auth/aws-signing-common/api/aws-signing-common.api b/runtime/auth/aws-signing-common/api/aws-signing-common.api index c9353509be..046afa8c53 100644 --- a/runtime/auth/aws-signing-common/api/aws-signing-common.api +++ b/runtime/auth/aws-signing-common/api/aws-signing-common.api @@ -199,13 +199,6 @@ public final class aws/smithy/kotlin/runtime/auth/awssigning/PresignerKt { public static final fun presignRequest (Laws/smithy/kotlin/runtime/http/request/HttpRequestBuilder;Laws/smithy/kotlin/runtime/operation/ExecutionContext;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;Laws/smithy/kotlin/runtime/http/operation/EndpointResolver;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class aws/smithy/kotlin/runtime/auth/awssigning/UnsupportedSigningAlgorithmException : aws/smithy/kotlin/runtime/ClientException { - public fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm;)V - public fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm;Ljava/lang/Throwable;)V - public synthetic fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getSigningAlgorithm ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm; -} - public final class aws/smithy/kotlin/runtime/auth/awssigning/internal/AwsChunkedUtilKt { public static final field AWS_CHUNKED_THRESHOLD I public static final field CHUNK_SIZE_BYTES I diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningExceptions.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningExceptions.kt deleted file mode 100644 index 8ff8c1e35f..0000000000 --- a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningExceptions.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.smithy.kotlin.runtime.auth.awssigning - -import aws.smithy.kotlin.runtime.ClientException -import aws.smithy.kotlin.runtime.InternalApi - -/** - * Is thrown when a signing algorithm is not supported by a signer - * - * See: [AwsSigningAlgorithm], [AwsSigner] - * - * @param message The message displayed by the exception - * @param signingAlgorithm The unsupported signing algorithm - * @param cause The cause of the exception - */ -@InternalApi -@Deprecated("This exception is no longer thrown. It will be removed in the next minor version, v1.5.x.") -public class UnsupportedSigningAlgorithmException( - message: String, - public val signingAlgorithm: AwsSigningAlgorithm, - cause: Throwable? = null, -) : ClientException( - message, - cause, -) { - public constructor( - message: String, - signingAlgorithm: AwsSigningAlgorithm, - ) : this ( - message, - signingAlgorithm, - null, - ) -} diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculator.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculator.kt index 4f4239a2ce..c4fe4f5ca4 100644 --- a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculator.kt +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculator.kt @@ -5,7 +5,7 @@ package aws.smithy.kotlin.runtime.auth.awssigning import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials -import aws.smithy.kotlin.runtime.collections.ReadThroughCache +import aws.smithy.kotlin.runtime.collections.PeriodicSweepCache import aws.smithy.kotlin.runtime.content.BigInteger import aws.smithy.kotlin.runtime.hashing.HashSupplier import aws.smithy.kotlin.runtime.hashing.Sha256 @@ -32,7 +32,7 @@ internal val N_MINUS_TWO = "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9 * @param sha256Provider the [HashSupplier] to use for computing SHA-256 hashes */ internal class SigV4aSignatureCalculator(override val sha256Provider: HashSupplier = ::Sha256) : BaseSigV4SignatureCalculator(AwsSigningAlgorithm.SIGV4_ASYMMETRIC, sha256Provider) { - private val privateKeyCache = ReadThroughCache( + private val privateKeyCache = PeriodicSweepCache( minimumSweepPeriod = 1.hours, // note: Sweeps are effectively a no-op because expiration is [Instant.MAX_VALUE] ) diff --git a/runtime/auth/aws-signing-tests/build.gradle.kts b/runtime/auth/aws-signing-tests/build.gradle.kts index 5810181041..667021303e 100644 --- a/runtime/auth/aws-signing-tests/build.gradle.kts +++ b/runtime/auth/aws-signing-tests/build.gradle.kts @@ -17,7 +17,6 @@ kotlin { api(project(":runtime:auth:http-auth-aws")) implementation(libs.kotlin.test) implementation(libs.kotlinx.coroutines.test) - implementation(libs.junit.jupiter.params) } } @@ -28,6 +27,7 @@ kotlin { implementation(libs.ktor.http.cio) implementation(libs.ktor.utils) implementation(libs.kotlin.test.junit5) + implementation(libs.junit.jupiter.params) implementation(libs.kotlinx.serialization.json) } } diff --git a/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt b/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt index b3c4202d26..70d48bfe45 100644 --- a/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt +++ b/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt @@ -11,7 +11,9 @@ import aws.smithy.kotlin.runtime.auth.awssigning.* import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.collections.ValuesMap import aws.smithy.kotlin.runtime.collections.get -import aws.smithy.kotlin.runtime.http.* +import aws.smithy.kotlin.runtime.http.HttpBody +import aws.smithy.kotlin.runtime.http.HttpMethod +import aws.smithy.kotlin.runtime.http.SdkHttpClient import aws.smithy.kotlin.runtime.http.auth.AwsHttpSigner import aws.smithy.kotlin.runtime.http.auth.SigV4AuthScheme import aws.smithy.kotlin.runtime.http.operation.* @@ -24,9 +26,7 @@ import aws.smithy.kotlin.runtime.net.url.Url import aws.smithy.kotlin.runtime.operation.ExecutionContext import aws.smithy.kotlin.runtime.time.Instant import io.ktor.http.cio.* -import io.ktor.util.* import io.ktor.utils.io.* -import io.ktor.utils.io.core.* import kotlinx.coroutines.runBlocking import kotlinx.io.readByteArray import kotlinx.serialization.json.* @@ -420,8 +420,7 @@ private fun buildOperation( serializeWith = object : HttpSerializer.NonStreaming { override fun serialize(context: ExecutionContext, input: Unit): HttpRequestBuilder = serialized } - @Suppress("DEPRECATION") - deserializer = IdentityDeserializer + deserializeWith = HttpDeserializer.Identity context { operationName = "testSigningOperation" diff --git a/runtime/auth/http-auth-api/api/http-auth-api.api b/runtime/auth/http-auth-api/api/http-auth-api.api index 491aa48555..2f541fb6ef 100644 --- a/runtime/auth/http-auth-api/api/http-auth-api.api +++ b/runtime/auth/http-auth-api/api/http-auth-api.api @@ -1,7 +1,7 @@ public abstract interface class aws/smithy/kotlin/runtime/http/auth/AuthScheme { public abstract fun getSchemeId-DepwgT4 ()Ljava/lang/String; public abstract fun getSigner ()Laws/smithy/kotlin/runtime/http/auth/HttpSigner; - public abstract fun identityProvider (Laws/smithy/kotlin/runtime/identity/IdentityProviderConfig;)Laws/smithy/kotlin/runtime/identity/IdentityProvider; + public fun identityProvider (Laws/smithy/kotlin/runtime/identity/IdentityProviderConfig;)Laws/smithy/kotlin/runtime/identity/IdentityProvider; } public final class aws/smithy/kotlin/runtime/http/auth/AuthScheme$DefaultImpls { diff --git a/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt b/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt index 6797ed74fb..0daa2b31fe 100644 --- a/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt +++ b/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt @@ -38,13 +38,11 @@ class DefaultAwsHttpSignerTest : AwsHttpSignerTestBase(DefaultAwsSigner) * Basic sanity tests. Signing (including `AwsHttpSigner`) is covered by the more exhaustive * test suite in the `aws-signing-tests` module. */ -@Suppress("HttpUrlsUsage") public abstract class AwsHttpSignerTestBase( private val signer: AwsSigner, ) { private val testCredentials = Credentials("AKID", "SECRET", "SESSION") - @Suppress("DEPRECATION") private fun buildOperation( requestBody: String = "{\"TableName\": \"foo\"}", streaming: Boolean = false, @@ -52,29 +50,11 @@ public abstract class AwsHttpSignerTestBase( unsigned: Boolean = false, ): SdkHttpOperation { val operation: SdkHttpOperation = SdkHttpOperation.build { - serializer = object : HttpSerialize { - override suspend fun serialize(context: ExecutionContext, input: Unit): HttpRequestBuilder = - HttpRequestBuilder().apply { - method = HttpMethod.POST - url.scheme = Scheme.HTTP - url.host = Host.Domain("demo.us-east-1.amazonaws.com") - url.path.encoded = "/" - headers.append("Host", "demo.us-east-1.amazonaws.com") - headers.appendAll("x-amz-archive-description", listOf("test", "test")) - body = when (streaming) { - true -> { - object : HttpBody.ChannelContent() { - override val contentLength: Long = requestBody.length.toLong() - override fun readFrom(): SdkByteReadChannel = SdkByteReadChannel(requestBody.encodeToByteArray()) - override val isOneShot: Boolean = !replayable - } - } - false -> HttpBody.fromBytes(requestBody.encodeToByteArray()) - } - headers.append("Content-Length", body.contentLength?.toString() ?: "0") - } + serializeWith = when (streaming) { + true -> StreamingSerializer(requestBody, replayable) + false -> NonStreamingSerializer(requestBody) } - deserializer = IdentityDeserializer + deserializeWith = HttpDeserializer.Identity operationName = "testSigningOperation" serviceName = "testService" context { @@ -190,3 +170,36 @@ public abstract class AwsHttpSignerTestBase( assertEquals(expectedSig, signed.headers["Authorization"]) } } + +private class NonStreamingSerializer(private val requestBody: String) : HttpSerializer.NonStreaming { + override fun serialize(context: ExecutionContext, input: Unit) = HttpRequestBuilder().apply { + method = HttpMethod.POST + url.scheme = Scheme.HTTP + url.host = Host.Domain("demo.us-east-1.amazonaws.com") + url.path.encoded = "/" + body = HttpBody.fromBytes(requestBody.encodeToByteArray()) + headers.append("Host", "demo.us-east-1.amazonaws.com") + headers.appendAll("x-amz-archive-description", listOf("test", "test")) + headers.append("Content-Length", body.contentLength?.toString() ?: "0") + } +} + +private class StreamingSerializer( + private val requestBody: String, + private val replayable: Boolean, +) : HttpSerializer.Streaming { + override suspend fun serialize(context: ExecutionContext, input: Unit) = HttpRequestBuilder().apply { + method = HttpMethod.POST + url.scheme = Scheme.HTTP + url.host = Host.Domain("demo.us-east-1.amazonaws.com") + url.path.encoded = "/" + body = object : HttpBody.ChannelContent() { + override val contentLength: Long = requestBody.length.toLong() + override fun readFrom(): SdkByteReadChannel = SdkByteReadChannel(requestBody.encodeToByteArray()) + override val isOneShot: Boolean = !replayable + } + headers.append("Host", "demo.us-east-1.amazonaws.com") + headers.appendAll("x-amz-archive-description", listOf("test", "test")) + headers.append("Content-Length", body.contentLength?.toString() ?: "0") + } +} diff --git a/runtime/auth/identity-api/api/identity-api.api b/runtime/auth/identity-api/api/identity-api.api index 9b07562934..824dc99385 100644 --- a/runtime/auth/identity-api/api/identity-api.api +++ b/runtime/auth/identity-api/api/identity-api.api @@ -56,6 +56,7 @@ public final class aws/smithy/kotlin/runtime/identity/IdentityAttributesKt { public abstract interface class aws/smithy/kotlin/runtime/identity/IdentityProvider { public abstract fun resolve (Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun resolve$default (Laws/smithy/kotlin/runtime/identity/IdentityProvider;Laws/smithy/kotlin/runtime/collections/Attributes;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } public final class aws/smithy/kotlin/runtime/identity/IdentityProvider$DefaultImpls { diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index e5674f748f..949c95973e 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -74,6 +74,7 @@ subprojects { freeCompilerArgs.add("-Xexpect-actual-classes") } } + tasks.withType { compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") diff --git a/runtime/observability/telemetry-api/api/telemetry-api.api b/runtime/observability/telemetry-api/api/telemetry-api.api index 470061210b..5e1f6b8684 100644 --- a/runtime/observability/telemetry-api/api/telemetry-api.api +++ b/runtime/observability/telemetry-api/api/telemetry-api.api @@ -176,11 +176,16 @@ public abstract interface class aws/smithy/kotlin/runtime/telemetry/logging/Logg public static final field Companion Laws/smithy/kotlin/runtime/telemetry/logging/Logger$Companion; public abstract fun atLevel (Laws/smithy/kotlin/runtime/telemetry/logging/LogLevel;)Laws/smithy/kotlin/runtime/telemetry/logging/LogRecordBuilder; public abstract fun debug (Ljava/lang/Throwable;Lkotlin/jvm/functions/Function0;)V + public static synthetic fun debug$default (Laws/smithy/kotlin/runtime/telemetry/logging/Logger;Ljava/lang/Throwable;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public abstract fun error (Ljava/lang/Throwable;Lkotlin/jvm/functions/Function0;)V + public static synthetic fun error$default (Laws/smithy/kotlin/runtime/telemetry/logging/Logger;Ljava/lang/Throwable;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public abstract fun info (Ljava/lang/Throwable;Lkotlin/jvm/functions/Function0;)V + public static synthetic fun info$default (Laws/smithy/kotlin/runtime/telemetry/logging/Logger;Ljava/lang/Throwable;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public abstract fun isEnabledFor (Laws/smithy/kotlin/runtime/telemetry/logging/LogLevel;)Z public abstract fun trace (Ljava/lang/Throwable;Lkotlin/jvm/functions/Function0;)V + public static synthetic fun trace$default (Laws/smithy/kotlin/runtime/telemetry/logging/Logger;Ljava/lang/Throwable;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public abstract fun warn (Ljava/lang/Throwable;Lkotlin/jvm/functions/Function0;)V + public static synthetic fun warn$default (Laws/smithy/kotlin/runtime/telemetry/logging/Logger;Ljava/lang/Throwable;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V } public final class aws/smithy/kotlin/runtime/telemetry/logging/Logger$Companion { @@ -266,6 +271,7 @@ public abstract class aws/smithy/kotlin/runtime/telemetry/metrics/AbstractUpDown public abstract interface class aws/smithy/kotlin/runtime/telemetry/metrics/AsyncMeasurement { public abstract fun record (Ljava/lang/Number;Laws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/telemetry/context/Context;)V + public static synthetic fun record$default (Laws/smithy/kotlin/runtime/telemetry/metrics/AsyncMeasurement;Ljava/lang/Number;Laws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/telemetry/context/Context;ILjava/lang/Object;)V } public final class aws/smithy/kotlin/runtime/telemetry/metrics/AsyncMeasurement$DefaultImpls { @@ -284,6 +290,7 @@ public final class aws/smithy/kotlin/runtime/telemetry/metrics/AsyncMeasurementH public abstract interface class aws/smithy/kotlin/runtime/telemetry/metrics/Histogram { public static final field Companion Laws/smithy/kotlin/runtime/telemetry/metrics/Histogram$Companion; public abstract fun record (Ljava/lang/Number;Laws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/telemetry/context/Context;)V + public static synthetic fun record$default (Laws/smithy/kotlin/runtime/telemetry/metrics/Histogram;Ljava/lang/Number;Laws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/telemetry/context/Context;ILjava/lang/Object;)V } public final class aws/smithy/kotlin/runtime/telemetry/metrics/Histogram$Companion { @@ -307,12 +314,19 @@ public final class aws/smithy/kotlin/runtime/telemetry/metrics/HistogramKt { public abstract interface class aws/smithy/kotlin/runtime/telemetry/metrics/Meter { public static final field Companion Laws/smithy/kotlin/runtime/telemetry/metrics/Meter$Companion; public abstract fun createAsyncUpDownCounter (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;)Laws/smithy/kotlin/runtime/telemetry/metrics/AsyncMeasurementHandle; + public static synthetic fun createAsyncUpDownCounter$default (Laws/smithy/kotlin/runtime/telemetry/metrics/Meter;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/telemetry/metrics/AsyncMeasurementHandle; public abstract fun createDoubleGauge (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;)Laws/smithy/kotlin/runtime/telemetry/metrics/AsyncMeasurementHandle; + public static synthetic fun createDoubleGauge$default (Laws/smithy/kotlin/runtime/telemetry/metrics/Meter;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/telemetry/metrics/AsyncMeasurementHandle; public abstract fun createDoubleHistogram (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Laws/smithy/kotlin/runtime/telemetry/metrics/Histogram; + public static synthetic fun createDoubleHistogram$default (Laws/smithy/kotlin/runtime/telemetry/metrics/Meter;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/telemetry/metrics/Histogram; public abstract fun createLongGauge (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;)Laws/smithy/kotlin/runtime/telemetry/metrics/AsyncMeasurementHandle; + public static synthetic fun createLongGauge$default (Laws/smithy/kotlin/runtime/telemetry/metrics/Meter;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/telemetry/metrics/AsyncMeasurementHandle; public abstract fun createLongHistogram (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Laws/smithy/kotlin/runtime/telemetry/metrics/Histogram; + public static synthetic fun createLongHistogram$default (Laws/smithy/kotlin/runtime/telemetry/metrics/Meter;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/telemetry/metrics/Histogram; public abstract fun createMonotonicCounter (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Laws/smithy/kotlin/runtime/telemetry/metrics/MonotonicCounter; + public static synthetic fun createMonotonicCounter$default (Laws/smithy/kotlin/runtime/telemetry/metrics/Meter;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/telemetry/metrics/MonotonicCounter; public abstract fun createUpDownCounter (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Laws/smithy/kotlin/runtime/telemetry/metrics/UpDownCounter; + public static synthetic fun createUpDownCounter$default (Laws/smithy/kotlin/runtime/telemetry/metrics/Meter;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/telemetry/metrics/UpDownCounter; } public final class aws/smithy/kotlin/runtime/telemetry/metrics/Meter$Companion { @@ -341,6 +355,7 @@ public final class aws/smithy/kotlin/runtime/telemetry/metrics/MeterProvider$Com public abstract interface class aws/smithy/kotlin/runtime/telemetry/metrics/MonotonicCounter { public static final field Companion Laws/smithy/kotlin/runtime/telemetry/metrics/MonotonicCounter$Companion; public abstract fun add (JLaws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/telemetry/context/Context;)V + public static synthetic fun add$default (Laws/smithy/kotlin/runtime/telemetry/metrics/MonotonicCounter;JLaws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/telemetry/context/Context;ILjava/lang/Object;)V } public final class aws/smithy/kotlin/runtime/telemetry/metrics/MonotonicCounter$Companion { @@ -354,6 +369,7 @@ public final class aws/smithy/kotlin/runtime/telemetry/metrics/MonotonicCounter$ public abstract interface class aws/smithy/kotlin/runtime/telemetry/metrics/UpDownCounter { public static final field Companion Laws/smithy/kotlin/runtime/telemetry/metrics/UpDownCounter$Companion; public abstract fun add (JLaws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/telemetry/context/Context;)V + public static synthetic fun add$default (Laws/smithy/kotlin/runtime/telemetry/metrics/UpDownCounter;JLaws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/telemetry/context/Context;ILjava/lang/Object;)V } public final class aws/smithy/kotlin/runtime/telemetry/metrics/UpDownCounter$Companion { @@ -427,9 +443,10 @@ public final class aws/smithy/kotlin/runtime/telemetry/trace/SpanStatus : java/l public abstract interface class aws/smithy/kotlin/runtime/telemetry/trace/TraceSpan : aws/smithy/kotlin/runtime/telemetry/context/Scope { public static final field Companion Laws/smithy/kotlin/runtime/telemetry/trace/TraceSpan$Companion; - public abstract fun asContextElement ()Lkotlin/coroutines/CoroutineContext; + public fun asContextElement ()Lkotlin/coroutines/CoroutineContext; public abstract fun close ()V public abstract fun emitEvent (Ljava/lang/String;Laws/smithy/kotlin/runtime/collections/Attributes;)V + public static synthetic fun emitEvent$default (Laws/smithy/kotlin/runtime/telemetry/trace/TraceSpan;Ljava/lang/String;Laws/smithy/kotlin/runtime/collections/Attributes;ILjava/lang/Object;)V public abstract fun getSpanContext ()Laws/smithy/kotlin/runtime/telemetry/trace/SpanContext; public abstract fun mergeAttributes (Laws/smithy/kotlin/runtime/collections/Attributes;)V public abstract fun set (Laws/smithy/kotlin/runtime/collections/AttributeKey;Ljava/lang/Object;)V @@ -468,6 +485,7 @@ public final class aws/smithy/kotlin/runtime/telemetry/trace/TraceSpanExtKt { public abstract interface class aws/smithy/kotlin/runtime/telemetry/trace/Tracer { public static final field Companion Laws/smithy/kotlin/runtime/telemetry/trace/Tracer$Companion; public abstract fun createSpan (Ljava/lang/String;Laws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/telemetry/trace/SpanKind;Laws/smithy/kotlin/runtime/telemetry/context/Context;)Laws/smithy/kotlin/runtime/telemetry/trace/TraceSpan; + public static synthetic fun createSpan$default (Laws/smithy/kotlin/runtime/telemetry/trace/Tracer;Ljava/lang/String;Laws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/telemetry/trace/SpanKind;Laws/smithy/kotlin/runtime/telemetry/context/Context;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/telemetry/trace/TraceSpan; } public final class aws/smithy/kotlin/runtime/telemetry/trace/Tracer$Companion { diff --git a/runtime/protocol/aws-json-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/json/AwsJsonProtocolTest.kt b/runtime/protocol/aws-json-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/json/AwsJsonProtocolTest.kt index b26ed992af..344ce79e0d 100644 --- a/runtime/protocol/aws-json-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/json/AwsJsonProtocolTest.kt +++ b/runtime/protocol/aws-json-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/json/AwsJsonProtocolTest.kt @@ -6,8 +6,10 @@ package aws.smithy.kotlin.runtime.awsprotocol.json import aws.smithy.kotlin.runtime.collections.get -import aws.smithy.kotlin.runtime.http.* +import aws.smithy.kotlin.runtime.http.HttpBody +import aws.smithy.kotlin.runtime.http.SdkHttpClient import aws.smithy.kotlin.runtime.http.operation.* +import aws.smithy.kotlin.runtime.http.readAll import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder import aws.smithy.kotlin.runtime.http.response.HttpResponse import aws.smithy.kotlin.runtime.httptest.TestEngine @@ -19,10 +21,9 @@ import kotlin.test.assertEquals class AwsJsonProtocolTest { @Test fun testSetJsonProtocolHeaders() = runTest { - @Suppress("DEPRECATION") val op = SdkHttpOperation.build { - serializer = UnitSerializer - deserializer = IdentityDeserializer + serializeWith = HttpSerializer.Unit + deserializeWith = HttpDeserializer.Identity operationName = "Bar" serviceName = "Foo" } @@ -41,10 +42,9 @@ class AwsJsonProtocolTest { @Test fun testEmptyBody() = runTest { - @Suppress("DEPRECATION") val op = SdkHttpOperation.build { - serializer = UnitSerializer - deserializer = IdentityDeserializer + serializeWith = HttpSerializer.Unit + deserializeWith = HttpDeserializer.Identity operationName = "Bar" serviceName = "Foo" } @@ -62,14 +62,14 @@ class AwsJsonProtocolTest { fun testDoesNotOverride() = runTest { @Suppress("DEPRECATION") val op = SdkHttpOperation.build { - serializer = object : HttpSerialize { - override suspend fun serialize(context: ExecutionContext, input: Unit): HttpRequestBuilder = + serializeWith = object : HttpSerializer.NonStreaming { + override fun serialize(context: ExecutionContext, input: Unit): HttpRequestBuilder = HttpRequestBuilder().apply { headers["Content-Type"] = "application/xml" body = HttpBody.fromBytes("foo".encodeToByteArray()) } } - deserializer = IdentityDeserializer + deserializeWith = HttpDeserializer.Identity operationName = "Bar" serviceName = "Foo" } diff --git a/runtime/protocol/aws-xml-protocols/api/aws-xml-protocols.api b/runtime/protocol/aws-xml-protocols/api/aws-xml-protocols.api index 7325432724..ab7db70f5a 100644 --- a/runtime/protocol/aws-xml-protocols/api/aws-xml-protocols.api +++ b/runtime/protocol/aws-xml-protocols/api/aws-xml-protocols.api @@ -1,10 +1,8 @@ public final class aws/smithy/kotlin/runtime/awsprotocol/xml/Ec2QueryErrorDeserializerKt { - public static final fun parseEc2QueryErrorResponse ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun parseEc2QueryErrorResponseNoSuspend ([B)Laws/smithy/kotlin/runtime/awsprotocol/ErrorDetails; + public static final fun parseEc2QueryErrorResponse ([B)Laws/smithy/kotlin/runtime/awsprotocol/ErrorDetails; } public final class aws/smithy/kotlin/runtime/awsprotocol/xml/RestXmlErrorDeserializerKt { - public static final fun parseRestXmlErrorResponse ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun parseRestXmlErrorResponseNoSuspend ([B)Laws/smithy/kotlin/runtime/awsprotocol/ErrorDetails; + public static final fun parseRestXmlErrorResponse ([B)Laws/smithy/kotlin/runtime/awsprotocol/ErrorDetails; } diff --git a/runtime/protocol/aws-xml-protocols/common/src/aws/smithy/kotlin/runtime/awsprotocol/xml/Ec2QueryErrorDeserializer.kt b/runtime/protocol/aws-xml-protocols/common/src/aws/smithy/kotlin/runtime/awsprotocol/xml/Ec2QueryErrorDeserializer.kt index 63391e1b12..2bd349177c 100644 --- a/runtime/protocol/aws-xml-protocols/common/src/aws/smithy/kotlin/runtime/awsprotocol/xml/Ec2QueryErrorDeserializer.kt +++ b/runtime/protocol/aws-xml-protocols/common/src/aws/smithy/kotlin/runtime/awsprotocol/xml/Ec2QueryErrorDeserializer.kt @@ -7,18 +7,16 @@ package aws.smithy.kotlin.runtime.awsprotocol.xml import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.awsprotocol.ErrorDetails import aws.smithy.kotlin.runtime.serde.getOrDeserializeErr -import aws.smithy.kotlin.runtime.serde.xml.* +import aws.smithy.kotlin.runtime.serde.xml.XmlTagReader +import aws.smithy.kotlin.runtime.serde.xml.data +import aws.smithy.kotlin.runtime.serde.xml.xmlTagReader internal data class Ec2QueryErrorResponse(val errors: List, val requestId: String?) internal data class Ec2QueryError(val code: String?, val message: String?) -@Deprecated("use parseEc2QueryErrorResponseNoSuspend") @InternalApi -public suspend fun parseEc2QueryErrorResponse(payload: ByteArray): ErrorDetails = - parseEc2QueryErrorResponseNoSuspend(payload) - -public fun parseEc2QueryErrorResponseNoSuspend(payload: ByteArray): ErrorDetails { +public fun parseEc2QueryErrorResponse(payload: ByteArray): ErrorDetails { val response = Ec2QueryErrorResponseDeserializer.deserialize(xmlTagReader(payload)) val firstError = response.errors.firstOrNull() return ErrorDetails(firstError?.code, firstError?.message, response.requestId) diff --git a/runtime/protocol/aws-xml-protocols/common/src/aws/smithy/kotlin/runtime/awsprotocol/xml/RestXmlErrorDeserializer.kt b/runtime/protocol/aws-xml-protocols/common/src/aws/smithy/kotlin/runtime/awsprotocol/xml/RestXmlErrorDeserializer.kt index 99925c0dc1..74a9448146 100644 --- a/runtime/protocol/aws-xml-protocols/common/src/aws/smithy/kotlin/runtime/awsprotocol/xml/RestXmlErrorDeserializer.kt +++ b/runtime/protocol/aws-xml-protocols/common/src/aws/smithy/kotlin/runtime/awsprotocol/xml/RestXmlErrorDeserializer.kt @@ -6,7 +6,7 @@ package aws.smithy.kotlin.runtime.awsprotocol.xml import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.awsprotocol.ErrorDetails -import aws.smithy.kotlin.runtime.serde.* +import aws.smithy.kotlin.runtime.serde.getOrDeserializeErr import aws.smithy.kotlin.runtime.serde.xml.XmlTagReader import aws.smithy.kotlin.runtime.serde.xml.data import aws.smithy.kotlin.runtime.serde.xml.xmlTagReader @@ -26,18 +26,8 @@ internal data class XmlError( override val message: String?, ) : RestXmlErrorDetails -/** - * Deserializes rest XML protocol errors as specified by: - * https://awslabs.github.io/smithy/1.0/spec/aws/aws-restxml-protocol.html#error-response-serialization - * - * Returns parsed data in normalized form or throws [DeserializationException] if response cannot be parsed. - */ -@Deprecated("use parseRestXmlErrorResponseNoSuspend") @InternalApi -public suspend fun parseRestXmlErrorResponse(payload: ByteArray): ErrorDetails = - parseRestXmlErrorResponseNoSuspend(payload) - -public fun parseRestXmlErrorResponseNoSuspend(payload: ByteArray): ErrorDetails { +public fun parseRestXmlErrorResponse(payload: ByteArray): ErrorDetails { val details = XmlErrorDeserializer.deserialize(xmlTagReader(payload)) return ErrorDetails(details.code, details.message, details.requestId) } diff --git a/runtime/protocol/aws-xml-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/xml/Ec2QueryErrorDeserializerTest.kt b/runtime/protocol/aws-xml-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/xml/Ec2QueryErrorDeserializerTest.kt index 6dfb531a5a..d2b4a6972e 100644 --- a/runtime/protocol/aws-xml-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/xml/Ec2QueryErrorDeserializerTest.kt +++ b/runtime/protocol/aws-xml-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/xml/Ec2QueryErrorDeserializerTest.kt @@ -26,7 +26,7 @@ class Ec2QueryErrorDeserializerTest { foo-request """.trimIndent().encodeToByteArray() - val actual = parseEc2QueryErrorResponseNoSuspend(payload) + val actual = parseEc2QueryErrorResponse(payload) assertEquals("InvalidGreeting", actual.code) assertEquals("Hi", actual.message) assertEquals("foo-request", actual.requestId) @@ -61,7 +61,7 @@ class Ec2QueryErrorDeserializerTest { for (payload in tests) { assertFailsWith { - parseEc2QueryErrorResponseNoSuspend(payload) + parseEc2QueryErrorResponse(payload) } } } @@ -90,7 +90,7 @@ class Ec2QueryErrorDeserializerTest { ).map { it.trimIndent().encodeToByteArray() } for (payload in tests) { - val actual = parseEc2QueryErrorResponseNoSuspend(payload) + val actual = parseEc2QueryErrorResponse(payload) assertNull(actual.code) assertNull(actual.message) assertEquals("foo-request", actual.requestId) diff --git a/runtime/protocol/aws-xml-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/xml/RestXmlErrorDeserializerTest.kt b/runtime/protocol/aws-xml-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/xml/RestXmlErrorDeserializerTest.kt index 9b94c7c31c..1f08e7bd3c 100644 --- a/runtime/protocol/aws-xml-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/xml/RestXmlErrorDeserializerTest.kt +++ b/runtime/protocol/aws-xml-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/xml/RestXmlErrorDeserializerTest.kt @@ -6,7 +6,10 @@ package aws.smithy.kotlin.runtime.awsprotocol.xml import aws.smithy.kotlin.runtime.serde.DeserializationException import kotlinx.coroutines.test.runTest -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull class RestXmlErrorDeserializerTest { @@ -36,7 +39,7 @@ class RestXmlErrorDeserializerTest { ) for (payload in tests) { - val actual = parseRestXmlErrorResponseNoSuspend(payload) + val actual = parseRestXmlErrorResponse(payload) assertEquals("InvalidGreeting", actual.code) assertEquals("Hi", actual.message) assertEquals("foo-id", actual.requestId) @@ -70,7 +73,7 @@ class RestXmlErrorDeserializerTest { for (payload in tests) { assertFailsWith { - parseRestXmlErrorResponseNoSuspend(payload) + parseRestXmlErrorResponse(payload) } } } @@ -92,7 +95,7 @@ class RestXmlErrorDeserializerTest { ) for (payload in tests) { - val error = parseRestXmlErrorResponseNoSuspend(payload) + val error = parseRestXmlErrorResponse(payload) assertEquals("foo-id", error.requestId) assertNull(error.code) assertNull(error.message) diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/api/http-client-engine-okhttp.api b/runtime/protocol/http-client-engines/http-client-engine-okhttp/api/http-client-engine-okhttp.api index 25c2339550..77e6120478 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/api/http-client-engine-okhttp.api +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/api/http-client-engine-okhttp.api @@ -87,7 +87,7 @@ public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngineConf } public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngineKt { - public static final fun buildClient (Laws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngineConfig;Laws/smithy/kotlin/runtime/http/engine/internal/HttpClientMetrics;)Lokhttp3/OkHttpClient; + public static final fun buildClient (Laws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngineConfig;Laws/smithy/kotlin/runtime/http/engine/internal/HttpClientMetrics;[Lokhttp3/EventListener;)Lokhttp3/OkHttpClient; } public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpHeadersAdapter : aws/smithy/kotlin/runtime/http/Headers { diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ConnectionIdleMonitor.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ConnectionMonitoringEventListener.kt similarity index 78% rename from runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ConnectionIdleMonitor.kt rename to runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ConnectionMonitoringEventListener.kt index 3f4c366f70..131466e66e 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ConnectionIdleMonitor.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ConnectionMonitoringEventListener.kt @@ -4,12 +4,10 @@ */ package aws.smithy.kotlin.runtime.http.engine.okhttp +import aws.smithy.kotlin.runtime.io.Closeable import aws.smithy.kotlin.runtime.telemetry.logging.logger import kotlinx.coroutines.* -import okhttp3.Call -import okhttp3.Connection -import okhttp3.ConnectionListener -import okhttp3.ExperimentalOkHttpApi +import okhttp3.* import okhttp3.internal.closeQuietly import okio.IOException import okio.buffer @@ -22,12 +20,20 @@ import kotlin.coroutines.coroutineContext import kotlin.time.Duration import kotlin.time.measureTime -@OptIn(ExperimentalOkHttpApi::class) -internal class ConnectionIdleMonitor(val pollInterval: Duration) : ConnectionListener() { +/** + * An [okhttp3.EventListener] implementation that monitors connections for remote closure. + * This replaces the functionality previously provided by the now-internal [okhttp3.ConnectionListener]. + */ +internal class ConnectionMonitoringEventListener(private val pollInterval: Duration) : + EventListener(), + Closeable { private val monitorScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val monitors = ConcurrentHashMap() - fun close(): Unit = runBlocking { + /** + * Close all active connection monitors. + */ + override fun close(): Unit = runBlocking { val monitorJob = requireNotNull(monitorScope.coroutineContext[Job]) { "Connection idle monitor scope cannot be cancelled because it does not have a job: $this" } @@ -40,13 +46,16 @@ internal class ConnectionIdleMonitor(val pollInterval: Duration) : ConnectionLis ?.callContext ?: Dispatchers.IO - override fun connectionAcquired(connection: Connection, call: Call) { + // Cancel monitoring when a connection is acquired + override fun connectionAcquired(call: Call, connection: Connection) { + super.connectionAcquired(call, connection) + // Non-locking map access is okay here because this code will only execute synchronously as part of a // `connectionAcquired` event and will be complete before any future `connectionReleased` event could fire for // the same connection. monitors.remove(connection)?.let { monitor -> val context = call.callContext() - val logger = context.logger() + val logger = context.logger() logger.trace { "Cancel monitoring for $connection" } // Use `runBlocking` because this _must_ finish before OkHttp goes to use the connection @@ -58,13 +67,18 @@ internal class ConnectionIdleMonitor(val pollInterval: Duration) : ConnectionLis } } - override fun connectionReleased(connection: Connection, call: Call) { + // Start monitoring when a connection is released + override fun connectionReleased(call: Call, connection: Connection) { + super.connectionReleased(call, connection) + val connId = System.identityHashCode(connection) val callContext = call.callContext() + + // Start monitoring val monitor = monitorScope.launch(CoroutineName("okhttp-conn-monitor-for-$connId")) { doMonitor(connection, callContext) } - callContext.logger().trace { "Launched coroutine $monitor to monitor $connection" } + callContext.logger().trace { "Launched coroutine $monitor to monitor $connection" } // Non-locking map access is okay here because this code will only execute synchronously as part of a // `connectionReleased` event and will be complete before any future `connectionAcquired` event could fire for @@ -73,7 +87,7 @@ internal class ConnectionIdleMonitor(val pollInterval: Duration) : ConnectionLis } private suspend fun doMonitor(conn: Connection, callContext: CoroutineContext) { - val logger = callContext.logger() + val logger = callContext.logger() val socket = conn.socket() val source = try { diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/EventListenerChain.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/EventListenerChain.kt new file mode 100644 index 0000000000..c18bd331f3 --- /dev/null +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/EventListenerChain.kt @@ -0,0 +1,115 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.http.engine.okhttp + +import aws.smithy.kotlin.runtime.io.closeIfCloseable +import okhttp3.* +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Proxy + +/** + * An [okhttp3.EventListener] that delegates to a chain of EventListeners. + * Start events are sent in forward order, terminal events are sent in reverse order + */ +internal class EventListenerChain( + private val listeners: List, +) : EventListener() { + private val reverseListeners = listeners.reversed() + + fun close() { + listeners.forEach { + it.closeIfCloseable() + } + } + + override fun callStart(call: Call): Unit = + listeners.forEach { it.callStart(call) } + + override fun dnsStart(call: Call, domainName: String): Unit = + listeners.forEach { it.dnsStart(call, domainName) } + + override fun dnsEnd(call: Call, domainName: String, inetAddressList: List): Unit = + reverseListeners.forEach { it.dnsEnd(call, domainName, inetAddressList) } + + override fun proxySelectStart(call: Call, url: HttpUrl): Unit = + listeners.forEach { it.proxySelectStart(call, url) } + + override fun proxySelectEnd(call: Call, url: HttpUrl, proxies: List): Unit = + reverseListeners.forEach { it.proxySelectEnd(call, url, proxies) } + + override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy): Unit = + listeners.forEach { it.connectStart(call, inetSocketAddress, proxy) } + + override fun secureConnectStart(call: Call): Unit = + listeners.forEach { it.secureConnectStart(call) } + + override fun secureConnectEnd(call: Call, handshake: Handshake?): Unit = + reverseListeners.forEach { it.secureConnectEnd(call, handshake) } + + override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?): Unit = + reverseListeners.forEach { it.connectEnd(call, inetSocketAddress, proxy, protocol) } + + override fun connectFailed(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ioe: IOException): Unit = + reverseListeners.forEach { it.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) } + + override fun connectionAcquired(call: Call, connection: Connection): Unit = + listeners.forEach { it.connectionAcquired(call, connection) } + + override fun connectionReleased(call: Call, connection: Connection): Unit = + reverseListeners.forEach { it.connectionReleased(call, connection) } + + override fun requestHeadersStart(call: Call): Unit = + listeners.forEach { it.requestHeadersStart(call) } + + override fun requestHeadersEnd(call: Call, request: Request): Unit = + reverseListeners.forEach { it.requestHeadersEnd(call, request) } + + override fun requestBodyStart(call: Call): Unit = + listeners.forEach { it.requestBodyStart(call) } + + override fun requestBodyEnd(call: Call, byteCount: Long): Unit = + reverseListeners.forEach { it.requestBodyEnd(call, byteCount) } + + override fun requestFailed(call: Call, ioe: IOException): Unit = + reverseListeners.forEach { it.requestFailed(call, ioe) } + + override fun responseHeadersStart(call: Call): Unit = + listeners.forEach { it.responseHeadersStart(call) } + + override fun responseHeadersEnd(call: Call, response: Response): Unit = + reverseListeners.forEach { it.responseHeadersEnd(call, response) } + + override fun responseBodyStart(call: Call): Unit = + listeners.forEach { it.responseBodyStart(call) } + + override fun responseBodyEnd(call: Call, byteCount: Long): Unit = + reverseListeners.forEach { it.responseBodyEnd(call, byteCount) } + + override fun responseFailed(call: Call, ioe: IOException): Unit = + reverseListeners.forEach { it.responseFailed(call, ioe) } + + override fun callEnd(call: Call): Unit = + reverseListeners.forEach { it.callEnd(call) } + + override fun callFailed(call: Call, ioe: IOException): Unit = + reverseListeners.forEach { it.callFailed(call, ioe) } + + override fun canceled(call: Call): Unit = + reverseListeners.forEach { it.canceled(call) } + + override fun satisfactionFailure(call: Call, response: Response): Unit = + reverseListeners.forEach { it.satisfactionFailure(call, response) } + + override fun cacheConditionalHit(call: Call, cachedResponse: Response): Unit = + listeners.forEach { it.cacheConditionalHit(call, cachedResponse) } + + override fun cacheHit(call: Call, response: Response): Unit = + listeners.forEach { it.cacheHit(call, response) } + + override fun cacheMiss(call: Call): Unit = + listeners.forEach { it.cacheMiss(call) } +} diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt index a20387d0a0..297e015eed 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt @@ -8,9 +8,13 @@ package aws.smithy.kotlin.runtime.http.engine.okhttp import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.http.HttpCall import aws.smithy.kotlin.runtime.http.config.EngineFactory -import aws.smithy.kotlin.runtime.http.engine.* +import aws.smithy.kotlin.runtime.http.engine.AlpnId +import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase +import aws.smithy.kotlin.runtime.http.engine.TlsContext +import aws.smithy.kotlin.runtime.http.engine.callContext import aws.smithy.kotlin.runtime.http.engine.internal.HttpClientMetrics import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.io.closeIfCloseable import aws.smithy.kotlin.runtime.net.TlsVersion import aws.smithy.kotlin.runtime.operation.ExecutionContext import aws.smithy.kotlin.runtime.time.Instant @@ -18,7 +22,6 @@ import aws.smithy.kotlin.runtime.time.fromEpochMilliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.job import okhttp3.* -import okhttp3.ConnectionPool import okhttp3.coroutines.executeAsync import java.util.concurrent.TimeUnit import kotlin.time.toJavaDuration @@ -44,9 +47,14 @@ public class OkHttpEngine( override val engineConstructor: (OkHttpEngineConfig.Builder.() -> Unit) -> OkHttpEngine = ::invoke } + // Create a single shared connection monitoring listener if idle polling is enabled + private val connectionMonitoringListener: EventListener? = + config.connectionIdlePollingInterval?.let { + ConnectionMonitoringEventListener(it) + } + private val metrics = HttpClientMetrics(TELEMETRY_SCOPE, config.telemetryProvider) - private val connectionIdleMonitor = config.connectionIdlePollingInterval?.let { ConnectionIdleMonitor(it) } - private val client = config.buildClientWithConnectionListener(metrics, connectionIdleMonitor) + private val client = config.buildClient(metrics, connectionMonitoringListener) @OptIn(ExperimentalCoroutinesApi::class) override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { @@ -73,16 +81,20 @@ public class OkHttpEngine( } override fun shutdown() { - connectionIdleMonitor?.close() + connectionMonitoringListener?.closeIfCloseable() client.connectionPool.evictAll() client.dispatcher.executorService.shutdown() metrics.close() } } -private fun OkHttpEngineConfig.buildClientFromConfig( +/** + * Convert SDK version of HTTP configuration to OkHttp specific configuration and return the configured client + */ +@InternalApi +public fun OkHttpEngineConfig.buildClient( metrics: HttpClientMetrics, - poolOverride: ConnectionPool? = null, + vararg clientScopedEventListeners: EventListener?, ): OkHttpClient { val config = this @@ -102,7 +114,7 @@ private fun OkHttpEngineConfig.buildClientFromConfig( writeTimeout(config.socketWriteTimeout.toJavaDuration()) // use our own pool configured with the timeout settings taken from config - val pool = poolOverride ?: ConnectionPool( + val pool = ConnectionPool( maxIdleConnections = 5, // The default from the no-arg ConnectionPool() constructor keepAliveDuration = config.connectionIdleTimeout.inWholeMilliseconds, TimeUnit.MILLISECONDS, @@ -116,7 +128,14 @@ private fun OkHttpEngineConfig.buildClientFromConfig( dispatcher(dispatcher) // Log events coming from okhttp. Allocate a new listener per-call to facilitate dedicated trace spans. - eventListenerFactory { call -> HttpEngineEventListener(pool, config.hostResolver, dispatcher, metrics, call) } + eventListenerFactory { call -> + EventListenerChain( + listOfNotNull( + HttpEngineEventListener(pool, config.hostResolver, dispatcher, metrics, call), + *clientScopedEventListeners, + ), + ) + } // map protocols if (config.tlsContext.alpn.isNotEmpty()) { @@ -140,34 +159,6 @@ private fun OkHttpEngineConfig.buildClientFromConfig( }.build() } -/** - * Convert SDK version of HTTP configuration to OkHttp specific configuration and return the configured client - */ -// Used by OkHttp4Engine - OkHttp4 does NOT have `connectionListener` -// TODO - Refactor in next minor version - Move this to OkHttp4Engine and make it private -@InternalApi -public fun OkHttpEngineConfig.buildClient( - metrics: HttpClientMetrics, -): OkHttpClient = this.buildClientFromConfig(metrics) - -/** - * Convert SDK version of HTTP configuration to OkHttp specific configuration and return the configured client - */ -// Used by OkHttpEngine - OkHttp5 does have `connectionListener` -@OptIn(ExperimentalOkHttpApi::class) -private fun OkHttpEngineConfig.buildClientWithConnectionListener( - metrics: HttpClientMetrics, - connectionListener: ConnectionIdleMonitor?, -): OkHttpClient = this.buildClientFromConfig( - metrics, - ConnectionPool( - maxIdleConnections = 5, // The default from the no-arg ConnectionPool() constructor - keepAliveDuration = this.connectionIdleTimeout.inWholeMilliseconds, - timeUnit = TimeUnit.MILLISECONDS, - connectionListener = connectionListener ?: ConnectionListener.NONE, - ), -) - private fun minTlsConnectionSpec(tlsContext: TlsContext): ConnectionSpec { val minVersion = tlsContext.minVersion ?: TlsVersion.TLS_1_2 val okHttpTlsVersions = SdkTlsVersion diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/test/aws/smithy/kotlin/runtime/http/engine/okhttp/EventListenerChainTest.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/test/aws/smithy/kotlin/runtime/http/engine/okhttp/EventListenerChainTest.kt new file mode 100644 index 0000000000..c9acb07cef --- /dev/null +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/test/aws/smithy/kotlin/runtime/http/engine/okhttp/EventListenerChainTest.kt @@ -0,0 +1,281 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.http.engine.okhttp + +import okhttp3.* +import java.io.Closeable +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Proxy +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class EventListenerChainTest { + @Test + fun testForwardEvents() { + val eventOrder = mutableListOf() + + val listener1 = TestEventListener("listener1", eventOrder) + val listener2 = TestEventListener("listener2", eventOrder) + + val chain = EventListenerChain(listOf(listener1, listener2)) + + val call = createMockCall() + + // Test forward events + chain.callStart(call) + chain.dnsStart(call, "example.com") + chain.proxySelectStart(call, createHttpUrl()) + + // Verify forward events were called in order (listener1 first, then listener2) + assertEquals("listener1:callStart", eventOrder[0]) + assertEquals("listener2:callStart", eventOrder[1]) + assertEquals("listener1:dnsStart", eventOrder[2]) + assertEquals("listener2:dnsStart", eventOrder[3]) + assertEquals("listener1:proxySelectStart", eventOrder[4]) + assertEquals("listener2:proxySelectStart", eventOrder[5]) + } + + @Test + fun testReverseEvents() { + val eventOrder = mutableListOf() + + val listener1 = TestEventListener("listener1", eventOrder) + val listener2 = TestEventListener("listener2", eventOrder) + + val chain = EventListenerChain(listOf(listener1, listener2)) + + val call = createMockCall() + + // Test reverse events + chain.dnsEnd(call, "example.com", listOf()) + chain.proxySelectEnd(call, createHttpUrl(), listOf()) + chain.callEnd(call) + + // Verify reverse events were called in reverse order (listener2 first, then listener1) + assertEquals("listener2:dnsEnd", eventOrder[0]) + assertEquals("listener1:dnsEnd", eventOrder[1]) + assertEquals("listener2:proxySelectEnd", eventOrder[2]) + assertEquals("listener1:proxySelectEnd", eventOrder[3]) + assertEquals("listener2:callEnd", eventOrder[4]) + assertEquals("listener1:callEnd", eventOrder[5]) + } + + @Test + fun testClose() { + val eventOrder = mutableListOf() + + val listener1 = TestEventListener("listener1", eventOrder) + val listener2 = TestEventListener("listener2", eventOrder) + + val chain = EventListenerChain(listOf(listener1, listener2)) + + // Close the chain + chain.close() + + // Verify all listeners were closed + assertTrue(listener1.closed) + assertTrue(listener2.closed) + } + + @Test + fun testMixedEvents() { + val eventOrder = mutableListOf() + + val listener1 = TestEventListener("listener1", eventOrder) + val listener2 = TestEventListener("listener2", eventOrder) + + val chain = EventListenerChain(listOf(listener1, listener2)) + + val call = createMockCall() + + // Test mixed forward and reverse events + chain.callStart(call) + chain.dnsStart(call, "example.com") + chain.dnsEnd(call, "example.com", listOf()) + + // Verify the order of events + assertEquals("listener1:callStart", eventOrder[0]) // listener1 first (forward) + assertEquals("listener2:callStart", eventOrder[1]) // listener2 second (forward) + assertEquals("listener1:dnsStart", eventOrder[2]) // listener1 first (forward) + assertEquals("listener2:dnsStart", eventOrder[3]) // listener2 second (forward) + assertEquals("listener2:dnsEnd", eventOrder[4]) // listener2 first (reverse) + assertEquals("listener1:dnsEnd", eventOrder[5]) // listener1 second (reverse) + + // Clear event order + eventOrder.clear() + + // Test more events to verify the sequence + chain.requestHeadersStart(call) // forward event + chain.requestHeadersEnd(call, Request.Builder().url("https://example.com").build()) // reverse event + chain.responseHeadersStart(call) // forward event + chain.responseHeadersEnd( + call, + Response.Builder() + .request(Request.Builder().url("https://example.com").build()) + .protocol(Protocol.HTTP_2) + .code(200) + .message("OK") + .build(), + ) // reverse event + + // Verify the sequence of events + assertEquals("listener1:requestHeadersStart", eventOrder[0]) // listener1 first (forward) + assertEquals("listener2:requestHeadersStart", eventOrder[1]) // listener2 second (forward) + assertEquals("listener2:requestHeadersEnd", eventOrder[2]) // listener2 first (reverse) + assertEquals("listener1:requestHeadersEnd", eventOrder[3]) // listener1 second (reverse) + assertEquals("listener1:responseHeadersStart", eventOrder[4]) // listener1 first (forward) + assertEquals("listener2:responseHeadersStart", eventOrder[5]) // listener2 second (forward) + assertEquals("listener2:responseHeadersEnd", eventOrder[6]) // listener2 first (reverse) + assertEquals("listener1:responseHeadersEnd", eventOrder[7]) // listener1 second (reverse) + } + + // A test EventListener that records the order of calls + private class TestEventListener(val name: String, val eventOrder: MutableList) : + EventListener(), + Closeable { + var closed = false + + override fun callStart(call: Call) { + eventOrder.add("$name:callStart") + } + + override fun dnsStart(call: Call, domainName: String) { + eventOrder.add("$name:dnsStart") + } + + override fun dnsEnd(call: Call, domainName: String, inetAddressList: List) { + eventOrder.add("$name:dnsEnd") + } + + override fun proxySelectStart(call: Call, url: HttpUrl) { + eventOrder.add("$name:proxySelectStart") + } + + override fun proxySelectEnd(call: Call, url: HttpUrl, proxies: List) { + eventOrder.add("$name:proxySelectEnd") + } + + override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) { + eventOrder.add("$name:connectStart") + } + + override fun secureConnectStart(call: Call) { + eventOrder.add("$name:secureConnectStart") + } + + override fun secureConnectEnd(call: Call, handshake: Handshake?) { + eventOrder.add("$name:secureConnectEnd") + } + + override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?) { + eventOrder.add("$name:connectEnd") + } + + override fun connectFailed(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ioe: IOException) { + eventOrder.add("$name:connectFailed") + } + + override fun connectionAcquired(call: Call, connection: Connection) { + eventOrder.add("$name:connectionAcquired") + } + + override fun connectionReleased(call: Call, connection: Connection) { + eventOrder.add("$name:connectionReleased") + } + + override fun requestHeadersStart(call: Call) { + eventOrder.add("$name:requestHeadersStart") + } + + override fun requestHeadersEnd(call: Call, request: Request) { + eventOrder.add("$name:requestHeadersEnd") + } + + override fun requestBodyStart(call: Call) { + eventOrder.add("$name:requestBodyStart") + } + + override fun requestBodyEnd(call: Call, byteCount: Long) { + eventOrder.add("$name:requestBodyEnd") + } + + override fun requestFailed(call: Call, ioe: IOException) { + eventOrder.add("$name:requestFailed") + } + + override fun responseHeadersStart(call: Call) { + eventOrder.add("$name:responseHeadersStart") + } + + override fun responseHeadersEnd(call: Call, response: Response) { + eventOrder.add("$name:responseHeadersEnd") + } + + override fun responseBodyStart(call: Call) { + eventOrder.add("$name:responseBodyStart") + } + + override fun responseBodyEnd(call: Call, byteCount: Long) { + eventOrder.add("$name:responseBodyEnd") + } + + override fun responseFailed(call: Call, ioe: IOException) { + eventOrder.add("$name:responseFailed") + } + + override fun callEnd(call: Call) { + eventOrder.add("$name:callEnd") + } + + override fun callFailed(call: Call, ioe: IOException) { + eventOrder.add("$name:callFailed") + } + + override fun canceled(call: Call) { + eventOrder.add("$name:canceled") + } + + override fun satisfactionFailure(call: Call, response: Response) { + eventOrder.add("$name:satisfactionFailure") + } + + override fun cacheConditionalHit(call: Call, cachedResponse: Response) { + eventOrder.add("$name:cacheConditionalHit") + } + + override fun cacheHit(call: Call, response: Response) { + eventOrder.add("$name:cacheHit") + } + + override fun cacheMiss(call: Call) { + eventOrder.add("$name:cacheMiss") + } + + override fun close() { + closed = true + } + } + + // Helper methods to create mock objects + private fun createMockCall(): Call = object : Call { + override fun cancel() {} + override fun clone(): Call = this + override fun enqueue(responseCallback: Callback) {} + override fun execute(): Response = throw UnsupportedOperationException() + override fun isCanceled(): Boolean = false + override fun isExecuted(): Boolean = false + override fun request(): Request = Request.Builder().url("https://example.com").build() + override fun timeout(): okio.Timeout = okio.Timeout() + } + + private fun createHttpUrl(): HttpUrl = HttpUrl.Builder() + .scheme("https") + .host("example.com") + .build() +} diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/README.md b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/README.md index 0f7077a217..7d3e12bba3 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/README.md +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/README.md @@ -1,19 +1,13 @@ # OkHttp4 Engine -The AWS SDK for Kotlin depends on OkHttp **5.0.0-alpha.x**, which despite being in alpha, is claimed to be production stable and safe for consumption: +The AWS SDK for Kotlin depends on a stable version of OkHttp **5.x**. -> Although this release is labeled alpha, the only unstable thing in it is our new APIs. -> This release has many critical bug fixes and is safe to run in production. -> We’re eager to stabilize our new APIs so we can get out of alpha. -> -> https://square.github.io/okhttp/changelogs/changelog/#version-500-alpha12 - -This `OkHttp4Engine` is intended to be used for applications which still depend on okhttp3 **4.x** and can't upgrade to the newest alpha version. +This `OkHttp4Engine` is intended to be used by applications which still depend on okhttp3 **4.x** and can't upgrade to the next major version. ## Configuration ### Gradle -Because the SDK's default HTTP engine depends on okhttp3 **5.0.0-alpha.X**, consumers will need to force Gradle to resolve to **4.x** to prevent the alpha dependency from being introduced transitively. Here is a sample configuration: +Because the SDK's default HTTP engine depends on okhttp3 **5.x**, consumers will need to force Gradle to resolve to **4.x** to prevent the newer dependency from being introduced transitively. Here is a sample configuration: ```kts dependencies { implementation("aws.sdk.kotlin:s3:$SDK_VERSION") // and any other AWS SDK clients... @@ -25,7 +19,7 @@ configurations.all { // Force resolve to OkHttp 4.x force("com.squareup.okhttp3:okhttp:4.12.0") // or whichever version you are using... } - exclude(group = "com.squareup.okhttp3", module = "okhttp-coroutines") // Exclude dependency on okhttp-coroutines, which is introduced in 5.0.0-alpha.X + exclude(group = "com.squareup.okhttp3", module = "okhttp-coroutines") // Exclude dependency on okhttp-coroutines, which was introduced in 5.0.0-alpha.X } ``` @@ -86,6 +80,17 @@ Caused by: java.lang.ClassNotFoundException: okhttp3.coroutines.ExecuteAsyncKt ... 9 more ``` -It likely means you failed to configure the SDK client to use the `OkHttpEngine4`. -Please double-check all of your SDK client configurations to ensure `httpClient = OkHttpEngine4()` is configured, -and if the problem persists, [open an issue](https://github.com/smithy-lang/smithy-kotlin/issues/new/choose). \ No newline at end of file +It likely means you failed to configure the SDK client to use the `OkHttp4Engine`. +Please double-check all of your SDK client configurations to ensure `httpClient = OkHttp4Engine()` is configured, +and if the problem persists, [open an issue](https://github.com/smithy-lang/smithy-kotlin/issues/new/choose). + +### Android R8 / ProGuard Configuration +If you're using the OkHttp4Engine in an Android application with R8 or Proguard for code minification, and you see an error similar to the following: +``` +ERROR: R8: Missing class okhttp3.coroutines.ExecuteAsyncKt (referenced from: java.lang.Object aws.smithy.kotlin.runtime.http.engine.okhttp.OkHttpEngine.roundTrip(aws.smithy.kotlin.runtime.operation.ExecutionContext, aws.smithy.kotlin.runtime.http.request.HttpRequest, kotlin.coroutines.Continuation)) +``` + +You'll need to add the following rule to either `proguard-rules.pro` or `consumer-rules.pro`, depending on your project structure: +``` +-dontwarn okhttp3.coroutines.ExecuteAsyncKt +``` diff --git a/runtime/protocol/http-client-engines/test-suite/build.gradle.kts b/runtime/protocol/http-client-engines/test-suite/build.gradle.kts index 065872fe7a..3f0d75176b 100644 --- a/runtime/protocol/http-client-engines/test-suite/build.gradle.kts +++ b/runtime/protocol/http-client-engines/test-suite/build.gradle.kts @@ -46,12 +46,6 @@ kotlin { jvmTest { dependencies { implementation(libs.docker.core) - // FIXME docker-java has a ton of dependencies with vulnerabilities, and they don't seem motivated to fix them. - // So we must override their dependencies with the latest patched versions. https://github.com/docker-java/docker-java/issues/1974 - implementation("com.fasterxml.jackson.core:jackson-databind:2.12.7.1") // https://github.com/docker-java/docker-java/issues/2177 - implementation("org.apache.commons:commons-compress:1.26.0") // https://github.com/docker-java/docker-java/pull/2256 - implementation("org.bouncycastle:bcpkix-jdk18on:1.78") // https://github.com/docker-java/docker-java/pull/2326 - implementation(libs.docker.transport.zerodep) implementation(project(":runtime:observability:telemetry-defaults")) diff --git a/runtime/protocol/http-client/api/http-client.api b/runtime/protocol/http-client/api/http-client.api index 9a752b6788..ecfd153135 100644 --- a/runtime/protocol/http-client/api/http-client.api +++ b/runtime/protocol/http-client/api/http-client.api @@ -29,6 +29,8 @@ public abstract interface class aws/smithy/kotlin/runtime/http/config/HttpEngine public abstract fun getHttpClient ()Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine; public abstract fun httpClient (Laws/smithy/kotlin/runtime/http/config/EngineFactory;Lkotlin/jvm/functions/Function1;)V public abstract fun httpClient (Lkotlin/jvm/functions/Function1;)V + public static synthetic fun httpClient$default (Laws/smithy/kotlin/runtime/http/config/HttpEngineConfig$Builder;Laws/smithy/kotlin/runtime/http/config/EngineFactory;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun httpClient$default (Laws/smithy/kotlin/runtime/http/config/HttpEngineConfig$Builder;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public abstract fun setHttpClient (Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine;)V } @@ -40,6 +42,18 @@ public final class aws/smithy/kotlin/runtime/http/config/HttpEngineConfig$Builde public abstract interface annotation class aws/smithy/kotlin/runtime/http/config/HttpEngineConfigDsl : java/lang/annotation/Annotation { } +public abstract interface class aws/smithy/kotlin/runtime/http/config/TimeoutConfig { + public abstract fun getAttemptTimeout-FghU774 ()Lkotlin/time/Duration; + public abstract fun getCallTimeout-FghU774 ()Lkotlin/time/Duration; +} + +public abstract interface class aws/smithy/kotlin/runtime/http/config/TimeoutConfig$Builder { + public abstract fun getAttemptTimeout-FghU774 ()Lkotlin/time/Duration; + public abstract fun getCallTimeout-FghU774 ()Lkotlin/time/Duration; + public abstract fun setAttemptTimeout-BwNAW2A (Lkotlin/time/Duration;)V + public abstract fun setCallTimeout-BwNAW2A (Lkotlin/time/Duration;)V +} + public final class aws/smithy/kotlin/runtime/http/engine/AlpnId : java/lang/Enum { public static final field H2_PRIOR_KNOWLEDGE Laws/smithy/kotlin/runtime/http/engine/AlpnId; public static final field HTTP1_1 Laws/smithy/kotlin/runtime/http/engine/AlpnId; @@ -309,7 +323,7 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/ContinueIntercept } public final class aws/smithy/kotlin/runtime/http/interceptors/DiscoveredEndpointErrorInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { - public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V + public fun (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;)V public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -487,19 +501,33 @@ public final class aws/smithy/kotlin/runtime/http/middleware/MutateHeaders : aws public final fun setIfMissing (Ljava/lang/String;Ljava/lang/String;)V } +public final class aws/smithy/kotlin/runtime/http/operation/AttemptTimeoutException : aws/smithy/kotlin/runtime/http/operation/ClientTimeoutException { + public fun (Ljava/lang/String;Ljava/lang/Throwable;)V +} + public abstract interface class aws/smithy/kotlin/runtime/http/operation/AuthSchemeResolver { public abstract fun resolve (Laws/smithy/kotlin/runtime/http/operation/OperationRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class aws/smithy/kotlin/runtime/http/operation/CallTimeoutException : aws/smithy/kotlin/runtime/http/operation/ClientTimeoutException { + public fun (Ljava/lang/String;Ljava/lang/Throwable;)V +} + +public abstract class aws/smithy/kotlin/runtime/http/operation/ClientTimeoutException : aws/smithy/kotlin/runtime/ClientException { + public fun (Ljava/lang/String;Ljava/lang/Throwable;Z)V +} + public abstract interface class aws/smithy/kotlin/runtime/http/operation/EndpointResolver { public abstract fun resolve (Laws/smithy/kotlin/runtime/http/operation/ResolveEndpointRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpDeserialize { - public abstract fun deserialize (Laws/smithy/kotlin/runtime/operation/ExecutionContext;Laws/smithy/kotlin/runtime/http/HttpCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpDeserializer { + public static final field Companion Laws/smithy/kotlin/runtime/http/operation/HttpDeserializer$Companion; } -public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpDeserializer { +public final class aws/smithy/kotlin/runtime/http/operation/HttpDeserializer$Companion { + public final fun getIdentity ()Laws/smithy/kotlin/runtime/http/operation/HttpDeserializer; + public final fun getUnit ()Laws/smithy/kotlin/runtime/http/operation/HttpDeserializer; } public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpDeserializer$NonStreaming : aws/smithy/kotlin/runtime/http/operation/HttpDeserializer { @@ -512,6 +540,8 @@ public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpDes public final class aws/smithy/kotlin/runtime/http/operation/HttpOperationContext { public static final field INSTANCE Laws/smithy/kotlin/runtime/http/operation/HttpOperationContext; + public final fun getAttemptTimeout ()Laws/smithy/kotlin/runtime/collections/AttributeKey; + public final fun getCallTimeout ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getClockSkew ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getClockSkewApproximateSigningTime ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getDefaultChecksumAlgorithm ()Laws/smithy/kotlin/runtime/collections/AttributeKey; @@ -523,11 +553,12 @@ public final class aws/smithy/kotlin/runtime/http/operation/HttpOperationContext public final fun getSdkInvocationId ()Laws/smithy/kotlin/runtime/collections/AttributeKey; } -public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpSerialize { - public abstract fun serialize (Laws/smithy/kotlin/runtime/operation/ExecutionContext;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpSerializer { + public static final field Companion Laws/smithy/kotlin/runtime/http/operation/HttpSerializer$Companion; } -public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpSerializer { +public final class aws/smithy/kotlin/runtime/http/operation/HttpSerializer$Companion { + public final fun getUnit ()Laws/smithy/kotlin/runtime/http/operation/HttpSerializer; } public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpSerializer$NonStreaming : aws/smithy/kotlin/runtime/http/operation/HttpSerializer { @@ -538,13 +569,8 @@ public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpSer public abstract fun serialize (Laws/smithy/kotlin/runtime/operation/ExecutionContext;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class aws/smithy/kotlin/runtime/http/operation/IdentityDeserializer : aws/smithy/kotlin/runtime/http/operation/HttpDeserialize { - public static final field INSTANCE Laws/smithy/kotlin/runtime/http/operation/IdentityDeserializer; - public fun deserialize (Laws/smithy/kotlin/runtime/operation/ExecutionContext;Laws/smithy/kotlin/runtime/http/HttpCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - public abstract interface class aws/smithy/kotlin/runtime/http/operation/InitializeMiddleware : aws/smithy/kotlin/runtime/io/middleware/Middleware { - public abstract fun install (Laws/smithy/kotlin/runtime/http/operation/SdkHttpOperation;)V + public fun install (Laws/smithy/kotlin/runtime/http/operation/SdkHttpOperation;)V } public final class aws/smithy/kotlin/runtime/http/operation/InitializeMiddleware$DefaultImpls { @@ -556,7 +582,7 @@ public abstract interface class aws/smithy/kotlin/runtime/http/operation/InlineM } public abstract interface class aws/smithy/kotlin/runtime/http/operation/ModifyRequestMiddleware : aws/smithy/kotlin/runtime/io/middleware/ModifyRequest { - public abstract fun install (Laws/smithy/kotlin/runtime/http/operation/SdkHttpOperation;)V + public fun install (Laws/smithy/kotlin/runtime/http/operation/SdkHttpOperation;)V } public final class aws/smithy/kotlin/runtime/http/operation/ModifyRequestMiddleware$DefaultImpls { @@ -564,7 +590,7 @@ public final class aws/smithy/kotlin/runtime/http/operation/ModifyRequestMiddlew } public abstract interface class aws/smithy/kotlin/runtime/http/operation/MutateMiddleware : aws/smithy/kotlin/runtime/io/middleware/Middleware { - public abstract fun install (Laws/smithy/kotlin/runtime/http/operation/SdkHttpOperation;)V + public fun install (Laws/smithy/kotlin/runtime/http/operation/SdkHttpOperation;)V } public final class aws/smithy/kotlin/runtime/http/operation/MutateMiddleware$DefaultImpls { @@ -633,7 +659,7 @@ public final class aws/smithy/kotlin/runtime/http/operation/OperationTelemetryKt } public abstract interface class aws/smithy/kotlin/runtime/http/operation/ReceiveMiddleware : aws/smithy/kotlin/runtime/io/middleware/Middleware { - public abstract fun install (Laws/smithy/kotlin/runtime/http/operation/SdkHttpOperation;)V + public fun install (Laws/smithy/kotlin/runtime/http/operation/SdkHttpOperation;)V } public final class aws/smithy/kotlin/runtime/http/operation/ReceiveMiddleware$DefaultImpls { @@ -675,20 +701,16 @@ public final class aws/smithy/kotlin/runtime/http/operation/SdkHttpOperationBuil public final fun build ()Laws/smithy/kotlin/runtime/http/operation/SdkHttpOperation; public final fun getContext ()Laws/smithy/kotlin/runtime/operation/ExecutionContext; public final fun getDeserializeWith ()Laws/smithy/kotlin/runtime/http/operation/HttpDeserializer; - public final fun getDeserializer ()Laws/smithy/kotlin/runtime/http/operation/HttpDeserialize; public final fun getExecution ()Laws/smithy/kotlin/runtime/http/operation/SdkOperationExecution; public final fun getHostPrefix ()Ljava/lang/String; public final fun getOperationName ()Ljava/lang/String; public final fun getSerializeWith ()Laws/smithy/kotlin/runtime/http/operation/HttpSerializer; - public final fun getSerializer ()Laws/smithy/kotlin/runtime/http/operation/HttpSerialize; public final fun getServiceName ()Ljava/lang/String; public final fun getTelemetry ()Laws/smithy/kotlin/runtime/http/operation/SdkOperationTelemetry; public final fun setDeserializeWith (Laws/smithy/kotlin/runtime/http/operation/HttpDeserializer;)V - public final fun setDeserializer (Laws/smithy/kotlin/runtime/http/operation/HttpDeserialize;)V public final fun setHostPrefix (Ljava/lang/String;)V public final fun setOperationName (Ljava/lang/String;)V public final fun setSerializeWith (Laws/smithy/kotlin/runtime/http/operation/HttpSerializer;)V - public final fun setSerializer (Laws/smithy/kotlin/runtime/http/operation/HttpSerialize;)V public final fun setServiceName (Ljava/lang/String;)V } @@ -731,14 +753,3 @@ public final class aws/smithy/kotlin/runtime/http/operation/SdkOperationTelemetr public final fun setSpanName (Ljava/lang/String;)V } -public final class aws/smithy/kotlin/runtime/http/operation/UnitDeserializer : aws/smithy/kotlin/runtime/http/operation/HttpDeserialize { - public static final field INSTANCE Laws/smithy/kotlin/runtime/http/operation/UnitDeserializer; - public fun deserialize (Laws/smithy/kotlin/runtime/operation/ExecutionContext;Laws/smithy/kotlin/runtime/http/HttpCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class aws/smithy/kotlin/runtime/http/operation/UnitSerializer : aws/smithy/kotlin/runtime/http/operation/HttpSerialize { - public static final field INSTANCE Laws/smithy/kotlin/runtime/http/operation/UnitSerializer; - public synthetic fun serialize (Laws/smithy/kotlin/runtime/operation/ExecutionContext;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun serialize (Laws/smithy/kotlin/runtime/operation/ExecutionContext;Lkotlin/Unit;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/config/TimeoutConfig.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/config/TimeoutConfig.kt new file mode 100644 index 0000000000..25bc7db84d --- /dev/null +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/config/TimeoutConfig.kt @@ -0,0 +1,39 @@ +package aws.smithy.kotlin.runtime.http.config + +import kotlin.time.Duration + +/** + * Defines optional timeout configuration for clients. + */ +public interface TimeoutConfig { + /** + * The maximum amount of time to wait for any single attempt of a request within the retry loop. By default, the + * value is `null` indicating no timeout is enforced. Attempt timeouts may be retried if allowed by the current + * retry policy and retry capacity. + */ + public val attemptTimeout: Duration? + + /** + * The maximum amount of time to wait for completion of a call, including any retries after the first attempt. By + * default, the value is `null` indicating no timeout is enforced. Call timeouts are not retried. + */ + public val callTimeout: Duration? + + /** + * A mutable instance used to set timeout configuration for clients. + */ + public interface Builder { + /** + * The maximum amount of time to wait for any single attempt of a request within the retry loop. By default, the + * value is `null` indicating no timeout is enforced. Attempt timeouts may be retried if allowed by the current + * retry policy and retry capacity. + */ + public var attemptTimeout: Duration? + + /** + * The maximum amount of time to wait for completion of a call, including any retries after the first attempt. + * By default, the value is `null` indicating no timeout is enforced. Call timeouts are not retried. + */ + public var callTimeout: Duration? + } +} diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/engine/HttpClientEngine.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/engine/HttpClientEngine.kt index 854ee44b67..48ee017edd 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/engine/HttpClientEngine.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/engine/HttpClientEngine.kt @@ -13,6 +13,7 @@ import aws.smithy.kotlin.runtime.operation.ExecutionContext import kotlinx.atomicfu.atomic import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext /** * Functionality a real HTTP client must provide. @@ -39,12 +40,24 @@ public interface CloseableHttpClientEngine : Closeable /** - * Base class that SDK [HttpClientEngine]s SHOULD inherit from rather than implementing directly. + * Base class that SDK [HttpClientEngine]s SHOULD inherit from rather than implementing directly. This class's + * [CoroutineContext] will include [SupervisorJob] because the failure of individual requests should not affect other + * requests or the overall engine. */ @InternalApi -public abstract class HttpClientEngineBase(engineName: String) : CloseableHttpClientEngine { - // why SupervisorJob? because failure of individual requests should not affect other requests or the overall engine - override val coroutineContext: CoroutineContext = SupervisorJob() + CoroutineName("http-client-engine-$engineName-context") +public abstract class HttpClientEngineBase private constructor( + override val coroutineContext: CoroutineContext, +) : CloseableHttpClientEngine { + public constructor(engineName: String) : this(engineName, EmptyCoroutineContext) + + /** + * Initializes a new [HttpClientEngineBase]. This internal overload allows setting a base [CoroutineContext] which + * is useful in tests using `runTest` (otherwise the base context will be empty and so will default to using + * [Dispatchers.Default]). + */ + internal constructor(engineName: String, baseContext: CoroutineContext) : + this(baseContext + SupervisorJob() + CoroutineName("http-client-engine-$engineName-context")) + private val closed = atomic(false) final override fun close() { diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/DiscoveredEndpointErrorInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/DiscoveredEndpointErrorInterceptor.kt index ce1ef41857..adb4babd48 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/DiscoveredEndpointErrorInterceptor.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/DiscoveredEndpointErrorInterceptor.kt @@ -23,7 +23,7 @@ import kotlin.reflect.KClass @InternalApi public class DiscoveredEndpointErrorInterceptor( private val errorType: KClass, - private val invalidate: (ExecutionContext) -> Unit, + private val invalidate: suspend (ExecutionContext) -> Unit, ) : HttpInterceptor { override suspend fun modifyBeforeAttemptCompletion( context: ResponseInterceptorContext, diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/middleware/RetryMiddleware.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/middleware/RetryMiddleware.kt index 7b8144c82e..cb865caa10 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/middleware/RetryMiddleware.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/middleware/RetryMiddleware.kt @@ -9,7 +9,6 @@ import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric import aws.smithy.kotlin.runtime.http.interceptors.InterceptorExecutor import aws.smithy.kotlin.runtime.http.operation.* -import aws.smithy.kotlin.runtime.http.operation.deepCopy import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder import aws.smithy.kotlin.runtime.http.request.immutableView import aws.smithy.kotlin.runtime.http.request.toBuilder @@ -84,7 +83,10 @@ internal class RetryMiddleware( ): Result { val result = interceptors.readBeforeAttempt(request.subject.immutableView()) .mapCatching { - next.call(request) + val attemptTimeout = request.context.getOrNull(HttpOperationContext.AttemptTimeout) + scopedTimeout(TimeoutScope.Attempt(attempt), attemptTimeout) { + next.call(request) + } } // get the http call for this attempt (if we made it that far) diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt index 56444dca98..9078b50aaa 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt @@ -19,6 +19,15 @@ import kotlin.time.Duration */ @InternalApi public object HttpOperationContext { + /** + * The amount of time to wait for a single attempt to complete + */ + public val AttemptTimeout: AttributeKey = AttributeKey("aws.smithy.kotlin#AttemptDuration") + + /** + * The amount of time to wait for a call to complete, including any retries + */ + public val CallTimeout: AttributeKey = AttributeKey("aws.smithy.kotlin#CallDuration") /** * A prefix to prepend the resolved hostname with. diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpSerde.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpSerde.kt index 5e50a3cca9..b50a05d037 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpSerde.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpSerde.kt @@ -15,6 +15,13 @@ import aws.smithy.kotlin.runtime.operation.ExecutionContext */ @InternalApi public sealed interface HttpSerializer { + @InternalApi + public companion object { + public val Unit: HttpSerializer = object : NonStreaming { + override fun serialize(context: ExecutionContext, input: Unit): HttpRequestBuilder = + HttpRequestBuilder() + } + } /** * Serializer for streaming operations that need full control over serialization of the body @@ -38,6 +45,17 @@ public sealed interface HttpSerializer { */ @InternalApi public sealed interface HttpDeserializer { + @InternalApi + public companion object { + public val Identity: HttpDeserializer = object : NonStreaming { + override fun deserialize(context: ExecutionContext, call: HttpCall, payload: ByteArray?): HttpResponse = + call.response + } + + public val Unit: HttpDeserializer = object : NonStreaming { + override fun deserialize(context: ExecutionContext, call: HttpCall, payload: ByteArray?) { } + } + } /** * Deserializer for streaming operations that need full control over deserialization of the body @@ -56,66 +74,3 @@ public sealed interface HttpDeserializer { public fun deserialize(context: ExecutionContext, call: HttpCall, payload: ByteArray?): T } } - -/** - * Implemented by types that know how to serialize to the HTTP protocol. - */ -@Deprecated("use HttpSerializer.Streaming") -@InternalApi -public fun interface HttpSerialize { - public suspend fun serialize(context: ExecutionContext, input: T): HttpRequestBuilder -} - -@Suppress("DEPRECATION") -private class LegacyHttpSerializeAdapter(val serializer: HttpSerialize) : HttpSerializer.Streaming { - override suspend fun serialize(context: ExecutionContext, input: T): HttpRequestBuilder = - serializer.serialize(context, input) -} - -@Suppress("DEPRECATION") -internal fun HttpSerialize.intoSerializer(): HttpSerializer = LegacyHttpSerializeAdapter(this) - -/** - * Implemented by types that know how to deserialize from the HTTP protocol. - */ -@Deprecated("use HttpDeserializer.Streaming") -@InternalApi -public fun interface HttpDeserialize { - public suspend fun deserialize(context: ExecutionContext, call: HttpCall): T -} - -@Suppress("DEPRECATION") -private class LegacyHttpDeserializeAdapter(val deserializer: HttpDeserialize) : HttpDeserializer.Streaming { - override suspend fun deserialize(context: ExecutionContext, call: HttpCall): T = - deserializer.deserialize(context, call) -} - -@Suppress("DEPRECATION") -internal fun HttpDeserialize.intoDeserializer(): HttpDeserializer = LegacyHttpDeserializeAdapter(this) - -/** - * Convenience deserialize implementation for a type with no output type - */ -@Suppress("DEPRECATION") -@InternalApi -public object UnitDeserializer : HttpDeserialize { - override suspend fun deserialize(context: ExecutionContext, call: HttpCall) {} -} - -/** - * Convenience serialize implementation for a type with no input type - */ -@Suppress("DEPRECATION") -@InternalApi -public object UnitSerializer : HttpSerialize { - override suspend fun serialize(context: ExecutionContext, input: Unit): HttpRequestBuilder = HttpRequestBuilder() -} - -/** - * Convenience deserialize implementation that returns the response without modification - */ -@Suppress("DEPRECATION") -@InternalApi -public object IdentityDeserializer : HttpDeserialize { - override suspend fun deserialize(context: ExecutionContext, call: HttpCall): HttpResponse = call.response -} diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/SdkHttpOperation.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/SdkHttpOperation.kt index 775da6c891..abb3b8cc46 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/SdkHttpOperation.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/SdkHttpOperation.kt @@ -36,17 +36,6 @@ public class SdkHttpOperation internal constructor( internal val typeInfo: OperationTypeInfo, internal val telemetry: SdkOperationTelemetry, ) { - - @Suppress("DEPRECATION") - internal constructor( - execution: SdkOperationExecution, - context: ExecutionContext, - serializer: HttpSerialize, - deserializer: HttpDeserialize, - typeInfo: OperationTypeInfo, - telemetry: SdkOperationTelemetry, - ) : this(execution, context, serializer.intoSerializer(), deserializer.intoDeserializer(), typeInfo, telemetry) - init { context[HttpOperationContext.SdkInvocationId] = Uuid.random().toString() } @@ -142,27 +131,8 @@ public class SdkHttpOperationBuilder( private val outputType: KClass<*>, ) { public val telemetry: SdkOperationTelemetry = SdkOperationTelemetry() - - @Suppress("DEPRECATION") - @Deprecated("use serializeWith") - public var serializer: HttpSerialize? = null - set(value) { - field = value - serializeWith = value?.intoSerializer() - } - public var serializeWith: HttpSerializer? = null - - @Suppress("DEPRECATION") - @Deprecated("use deserializeWith") - public var deserializer: HttpDeserialize? = null - set(value) { - field = value - deserializeWith = value?.intoDeserializer() - } - public var deserializeWith: HttpDeserializer? = null - public val execution: SdkOperationExecution = SdkOperationExecution() public val context: ExecutionContext = ExecutionContext() diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/SdkOperationExecution.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/SdkOperationExecution.kt index d30c7c546b..2384e508b3 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/SdkOperationExecution.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/SdkOperationExecution.kt @@ -15,10 +15,13 @@ import aws.smithy.kotlin.runtime.client.logMode import aws.smithy.kotlin.runtime.collections.attributesOf import aws.smithy.kotlin.runtime.collections.emptyAttributes import aws.smithy.kotlin.runtime.collections.merge -import aws.smithy.kotlin.runtime.http.* +import aws.smithy.kotlin.runtime.http.HttpCall +import aws.smithy.kotlin.runtime.http.HttpHandler import aws.smithy.kotlin.runtime.http.auth.SignHttpRequest +import aws.smithy.kotlin.runtime.http.complete import aws.smithy.kotlin.runtime.http.interceptors.InterceptorExecutor import aws.smithy.kotlin.runtime.http.middleware.RetryMiddleware +import aws.smithy.kotlin.runtime.http.readAll import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder import aws.smithy.kotlin.runtime.http.request.dumpRequest import aws.smithy.kotlin.runtime.http.request.immutableView @@ -193,8 +196,6 @@ private fun HttpDeserializer.decorate( interceptors: InterceptorExecutor, ): Handler = DeserializeHandler(inner, this, interceptors) -// internal glue used to marry one phase to another - /** * Outermost handler that wraps the entire middleware pipeline and handles interceptor hooks related * to the start/end of an operation @@ -207,7 +208,10 @@ private class OperationHandler( coroutineContext.trace> { "operation started" } val result = interceptors.readBeforeExecution(request.subject) .mapCatching { - inner.call(request) + val callTimeout = request.context.getOrNull(HttpOperationContext.CallTimeout) + scopedTimeout(TimeoutScope.Call, callTimeout) { + inner.call(request) + } } .let { interceptors.modifyBeforeCompletion(it) diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/Timeouts.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/Timeouts.kt new file mode 100644 index 0000000000..a805e45017 --- /dev/null +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/Timeouts.kt @@ -0,0 +1,71 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.http.operation + +import aws.smithy.kotlin.runtime.ClientException +import aws.smithy.kotlin.runtime.ErrorMetadata +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration + +internal sealed interface TimeoutScope { + val description: String + + data object Call : TimeoutScope { + override val description = "Call" + } + + data class Attempt(val attempt: Int) : TimeoutScope { + override val description = "Attempt #$attempt" + } +} + +/** + * Indicates that a client-side configured timeout was exceeded (e.g., call timeout, attempt timeout, etc.) + */ +public abstract class ClientTimeoutException( + message: String, + cause: Throwable, + retryable: Boolean, +) : ClientException(message, cause) { + init { + sdkErrorMetadata.attributes[ErrorMetadata.Retryable] = retryable + } +} + +/** + * Indicates that a single attempt took longer than allowed to complete + */ +public class AttemptTimeoutException(message: String, cause: Throwable) : ClientTimeoutException(message, cause, true) + +/** + * Indicates that a call (including any retry attempts) took longer than allowed to complete + */ +public class CallTimeoutException(message: String, cause: Throwable) : ClientTimeoutException(message, cause, false) + +internal suspend inline fun scopedTimeout( + scope: TimeoutScope, + duration: Duration?, + crossinline block: suspend () -> T, +): T = when (duration) { + null -> block() + else -> + try { + withTimeout(duration) { block() } + } catch (e: TimeoutCancellationException) { + val message = buildString { + append(scope.description) + append(" exceeded configured timeout of ") + append(duration) + } + + val exceptionType: (String, Throwable) -> Throwable = when (scope) { + is TimeoutScope.Attempt -> ::AttemptTimeoutException + TimeoutScope.Call -> ::CallTimeoutException + } + + throw exceptionType(message, e) + } +} diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/operation/SdkHttpOperationTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/operation/SdkHttpOperationTest.kt index 22e36d1cf1..e564bedcc2 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/operation/SdkHttpOperationTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/operation/SdkHttpOperationTest.kt @@ -36,8 +36,8 @@ class SdkHttpOperationTest { val ex = assertFailsWith { @Suppress("DEPRECATION") SdkHttpOperation.build { - serializer = UnitSerializer - deserializer = UnitDeserializer + serializeWith = HttpSerializer.Unit + deserializeWith = HttpDeserializer.Unit } } diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/operation/SdkOperationExecutionTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/operation/SdkOperationExecutionTest.kt index 11657db007..e0fc1b0289 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/operation/SdkOperationExecutionTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/operation/SdkOperationExecutionTest.kt @@ -6,12 +6,11 @@ package aws.smithy.kotlin.runtime.http.operation import aws.smithy.kotlin.runtime.auth.AuthSchemeId -import aws.smithy.kotlin.runtime.http.Headers -import aws.smithy.kotlin.runtime.http.HttpBody -import aws.smithy.kotlin.runtime.http.HttpCall -import aws.smithy.kotlin.runtime.http.HttpStatusCode -import aws.smithy.kotlin.runtime.http.SdkHttpClient -import aws.smithy.kotlin.runtime.http.auth.* +import aws.smithy.kotlin.runtime.http.* +import aws.smithy.kotlin.runtime.http.auth.AnonymousIdentityProvider +import aws.smithy.kotlin.runtime.http.auth.AuthScheme +import aws.smithy.kotlin.runtime.http.auth.HttpSigner +import aws.smithy.kotlin.runtime.http.auth.SignHttpRequest import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineConfig import aws.smithy.kotlin.runtime.http.request.HttpRequest @@ -20,13 +19,74 @@ import aws.smithy.kotlin.runtime.http.response.HttpResponse import aws.smithy.kotlin.runtime.identity.asIdentityProviderConfig import aws.smithy.kotlin.runtime.operation.ExecutionContext import aws.smithy.kotlin.runtime.time.Instant +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue +import kotlin.coroutines.CoroutineContext +import kotlin.test.* +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds class SdkOperationExecutionTest { + @Test + fun testAttemptTimeoutWithLongCall(): Unit = runTest { + val serialized = HttpRequestBuilder() + val op = newTestOperation(serialized, Unit).apply { + context[HttpOperationContext.AttemptTimeout] = 200.milliseconds + } + + val engine = DelayingHttpEngine(listOf(300.milliseconds, 300.milliseconds, 100.milliseconds)) + val client = SdkHttpClient(engine) + + val result = op.roundTrip(client, Unit) + assertEquals(Unit, result) + assertEquals(3, engine.callCount) + } + + @Test + fun testAttemptTimeoutWithShortCall(): Unit = runTest { + val serialized = HttpRequestBuilder() + val op = newTestOperation(serialized, Unit).apply { + context[HttpOperationContext.AttemptTimeout] = 300.milliseconds + } + + val engine = DelayingHttpEngine(200.milliseconds) + val client = SdkHttpClient(engine) + + val result = op.roundTrip(client, Unit) + assertEquals(Unit, result) + assertEquals(1, engine.callCount) + } + + @Test + fun testCallTimeoutWithLongCall(): Unit = runTest { + val serialized = HttpRequestBuilder() + val op = newTestOperation(serialized, Unit).apply { + context[HttpOperationContext.CallTimeout] = 200.milliseconds + } + + val engine = DelayingHttpEngine(300.milliseconds) + val client = SdkHttpClient(engine) + + assertFailsWith { op.roundTrip(client, Unit) } + assertEquals(1, engine.callCount) + } + + @Test + fun testCallTimeoutWithShortCall(): Unit = runTest { + val serialized = HttpRequestBuilder() + val op = newTestOperation(serialized, Unit).apply { + context[HttpOperationContext.CallTimeout] = 300.milliseconds + } + + val engine = DelayingHttpEngine(200.milliseconds) + val client = SdkHttpClient(engine) + + val result = op.roundTrip(client, Unit) + assertEquals(Unit, result) + assertEquals(1, engine.callCount) + } + @Test fun testOperationMiddlewareOrder() = runTest { // sanity test middleware flows the way we expect @@ -105,3 +165,32 @@ class SdkOperationExecutionTest { assertEquals(expectedOrder, actualOrder) } } + +private fun CoroutineScope.DelayingHttpEngine(duration: Duration) = + DelayingHttpEngine(coroutineContext, listOf(duration)) + +private fun CoroutineScope.DelayingHttpEngine(durations: List) = + DelayingHttpEngine(coroutineContext, durations) + +private class DelayingHttpEngine( + testContext: CoroutineContext, + durations: List, +) : HttpClientEngineBase("test engine", testContext) { + private val durations = run { + val terminator = durations.last() + val initial = durations.dropLast(1) + initial.asSequence() + generateSequence { terminator } + }.iterator() + + var callCount = 0 + private set + + override val config: HttpClientEngineConfig = HttpClientEngineConfig.Default + + override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { + callCount++ + delay(durations.next()) + val resp = HttpResponse(HttpStatusCode.OK, Headers.Empty, HttpBody.Empty) + return HttpCall(request, resp, Instant.now(), Instant.now()) + } +} diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 0c7ba6d1cd..1ba5b08c40 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -180,6 +180,12 @@ public final class aws/smithy/kotlin/runtime/collections/CollectionExtKt { public static final fun createOrAppend (Ljava/util/List;Ljava/lang/Object;)Ljava/util/List; } +public abstract interface class aws/smithy/kotlin/runtime/collections/ExpiringKeyedCache { + public abstract fun get (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getSize ()I + public abstract fun invalidate (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class aws/smithy/kotlin/runtime/collections/LruCache { public fun (I)V public final fun get (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -191,9 +197,9 @@ public final class aws/smithy/kotlin/runtime/collections/LruCache { } public abstract interface class aws/smithy/kotlin/runtime/collections/MultiMap : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker { - public abstract fun contains (Ljava/lang/Object;Ljava/lang/Object;)Z + public fun contains (Ljava/lang/Object;Ljava/lang/Object;)Z public abstract fun getEntryValues ()Lkotlin/sequences/Sequence; - public abstract fun toMutableMultiMap ()Laws/smithy/kotlin/runtime/collections/MutableMultiMap; + public fun toMutableMultiMap ()Laws/smithy/kotlin/runtime/collections/MutableMultiMap; } public final class aws/smithy/kotlin/runtime/collections/MultiMap$DefaultImpls { @@ -216,15 +222,15 @@ public abstract interface class aws/smithy/kotlin/runtime/collections/MutableMul public abstract fun add (Ljava/lang/Object;Ljava/lang/Object;)Z public abstract fun addAll (Ljava/lang/Object;ILjava/util/Collection;)Z public abstract fun addAll (Ljava/lang/Object;Ljava/util/Collection;)Z - public abstract fun addAll (Ljava/util/Map;)V - public abstract fun contains (Ljava/lang/Object;Ljava/lang/Object;)Z + public fun addAll (Ljava/util/Map;)V + public fun contains (Ljava/lang/Object;Ljava/lang/Object;)Z public abstract fun getEntryValues ()Lkotlin/sequences/Sequence; - public abstract fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/util/List; + public fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/util/List; public abstract fun removeAll (Ljava/lang/Object;Ljava/util/Collection;)Ljava/lang/Boolean; public abstract fun removeAt (Ljava/lang/Object;I)Ljava/lang/Object; public abstract fun removeElement (Ljava/lang/Object;Ljava/lang/Object;)Z public abstract fun retainAll (Ljava/lang/Object;Ljava/util/Collection;)Ljava/lang/Boolean; - public abstract fun toMultiMap ()Laws/smithy/kotlin/runtime/collections/MultiMap; + public fun toMultiMap ()Laws/smithy/kotlin/runtime/collections/MultiMap; } public final class aws/smithy/kotlin/runtime/collections/MutableMultiMap$DefaultImpls { @@ -238,12 +244,12 @@ public final class aws/smithy/kotlin/runtime/collections/MutableMultiMapKt { public static final fun mutableMultiMapOf ([Lkotlin/Pair;)Laws/smithy/kotlin/runtime/collections/MutableMultiMap; } -public final class aws/smithy/kotlin/runtime/collections/ReadThroughCache { +public final class aws/smithy/kotlin/runtime/collections/PeriodicSweepCache : aws/smithy/kotlin/runtime/collections/ExpiringKeyedCache { public synthetic fun (JLaws/smithy/kotlin/runtime/time/Clock;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (JLaws/smithy/kotlin/runtime/time/Clock;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun get (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun getSize ()I - public final fun invalidate (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun get (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun getSize ()I + public fun invalidate (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class aws/smithy/kotlin/runtime/collections/StackKt { @@ -257,10 +263,10 @@ public final class aws/smithy/kotlin/runtime/collections/StackKt { public abstract interface class aws/smithy/kotlin/runtime/collections/ValuesMap { public abstract fun contains (Ljava/lang/String;)Z - public abstract fun contains (Ljava/lang/String;Ljava/lang/Object;)Z + public fun contains (Ljava/lang/String;Ljava/lang/Object;)Z public abstract fun entries ()Ljava/util/Set; - public abstract fun forEach (Lkotlin/jvm/functions/Function2;)V - public abstract fun get (Ljava/lang/String;)Ljava/lang/Object; + public fun forEach (Lkotlin/jvm/functions/Function2;)V + public fun get (Ljava/lang/String;)Ljava/lang/Object; public abstract fun getAll (Ljava/lang/String;)Ljava/util/List; public abstract fun getCaseInsensitiveName ()Z public abstract fun isEmpty ()Z @@ -721,6 +727,7 @@ public abstract interface class aws/smithy/kotlin/runtime/hashing/HashFunction { public abstract fun getDigestSizeBytes ()I public abstract fun reset ()V public abstract fun update ([BII)V + public static synthetic fun update$default (Laws/smithy/kotlin/runtime/hashing/HashFunction;[BIIILjava/lang/Object;)V } public final class aws/smithy/kotlin/runtime/hashing/HashFunction$DefaultImpls { @@ -921,6 +928,7 @@ public abstract interface class aws/smithy/kotlin/runtime/io/SdkBufferedSink : a public abstract fun getBuffer ()Laws/smithy/kotlin/runtime/io/SdkBuffer; public abstract fun write (Laws/smithy/kotlin/runtime/io/SdkSource;J)V public abstract fun write ([BII)V + public static synthetic fun write$default (Laws/smithy/kotlin/runtime/io/SdkBufferedSink;[BIIILjava/lang/Object;)V public abstract fun writeAll (Laws/smithy/kotlin/runtime/io/SdkSource;)J public abstract fun writeByte (B)V public abstract fun writeInt (I)V @@ -930,6 +938,7 @@ public abstract interface class aws/smithy/kotlin/runtime/io/SdkBufferedSink : a public abstract fun writeShort (S)V public abstract fun writeShortLe (S)V public abstract fun writeUtf8 (Ljava/lang/String;II)V + public static synthetic fun writeUtf8$default (Laws/smithy/kotlin/runtime/io/SdkBufferedSink;Ljava/lang/String;IIILjava/lang/Object;)V } public final class aws/smithy/kotlin/runtime/io/SdkBufferedSink$DefaultImpls { @@ -942,6 +951,7 @@ public abstract interface class aws/smithy/kotlin/runtime/io/SdkBufferedSource : public abstract fun getBuffer ()Laws/smithy/kotlin/runtime/io/SdkBuffer; public abstract fun peek ()Laws/smithy/kotlin/runtime/io/SdkBufferedSource; public abstract fun read ([BII)I + public static synthetic fun read$default (Laws/smithy/kotlin/runtime/io/SdkBufferedSource;[BIIILjava/lang/Object;)I public abstract fun readAll (Laws/smithy/kotlin/runtime/io/SdkSink;)J public abstract fun readByte ()B public abstract fun readByteArray ()[B @@ -964,7 +974,7 @@ public final class aws/smithy/kotlin/runtime/io/SdkBufferedSource$DefaultImpls { } public abstract interface class aws/smithy/kotlin/runtime/io/SdkByteChannel : aws/smithy/kotlin/runtime/io/SdkByteReadChannel, aws/smithy/kotlin/runtime/io/SdkByteWriteChannel { - public abstract fun close ()V + public fun close ()V } public final class aws/smithy/kotlin/runtime/io/SdkByteChannel$DefaultImpls { @@ -1011,6 +1021,7 @@ public abstract interface class aws/smithy/kotlin/runtime/io/SdkByteWriteChannel public abstract fun getTotalBytesWritten ()J public abstract fun isClosedForWrite ()Z public abstract fun write (Laws/smithy/kotlin/runtime/io/SdkBuffer;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun write$default (Laws/smithy/kotlin/runtime/io/SdkByteWriteChannel;Laws/smithy/kotlin/runtime/io/SdkBuffer;JLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } public final class aws/smithy/kotlin/runtime/io/SdkByteWriteChannel$DefaultImpls { @@ -1231,6 +1242,7 @@ public final class aws/smithy/kotlin/runtime/net/HostKt { public abstract interface class aws/smithy/kotlin/runtime/net/HostResolver { public static final field Companion Laws/smithy/kotlin/runtime/net/HostResolver$Companion; public abstract fun purgeCache (Laws/smithy/kotlin/runtime/net/HostAddress;)V + public static synthetic fun purgeCache$default (Laws/smithy/kotlin/runtime/net/HostResolver;Laws/smithy/kotlin/runtime/net/HostAddress;ILjava/lang/Object;)V public abstract fun reportFailure (Laws/smithy/kotlin/runtime/net/HostAddress;)V public abstract fun resolve (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -2108,7 +2120,6 @@ public final class aws/smithy/kotlin/runtime/smoketests/SmokeTestsFunctionsJVMKt public final class aws/smithy/kotlin/runtime/smoketests/SmokeTestsFunctionsKt { public static final fun getDefaultPrinter ()Ljava/lang/Appendable; - public static final fun printExceptionStackTrace (Ljava/lang/Exception;)V } public final class aws/smithy/kotlin/runtime/text/Scanner { @@ -2164,8 +2175,8 @@ public final class aws/smithy/kotlin/runtime/text/encoding/Encodable$Companion { public abstract interface class aws/smithy/kotlin/runtime/text/encoding/Encoding { public static final field Companion Laws/smithy/kotlin/runtime/text/encoding/Encoding$Companion; public abstract fun decode (Ljava/lang/String;)Ljava/lang/String; - public abstract fun encodableFromDecoded (Ljava/lang/String;)Laws/smithy/kotlin/runtime/text/encoding/Encodable; - public abstract fun encodableFromEncoded (Ljava/lang/String;)Laws/smithy/kotlin/runtime/text/encoding/Encodable; + public fun encodableFromDecoded (Ljava/lang/String;)Laws/smithy/kotlin/runtime/text/encoding/Encodable; + public fun encodableFromEncoded (Ljava/lang/String;)Laws/smithy/kotlin/runtime/text/encoding/Encodable; public abstract fun encode (Ljava/lang/String;)Ljava/lang/String; public abstract fun getName ()Ljava/lang/String; } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/ExpiringKeyedCache.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/ExpiringKeyedCache.kt new file mode 100644 index 0000000000..b4588d9280 --- /dev/null +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/ExpiringKeyedCache.kt @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.collections + +import aws.smithy.kotlin.runtime.util.ExpiringValue + +/** + * A multi-value cache which supports retrieval and invalidation via a key paired with each value. The [get] and + * [invalidate] methods are `suspend` functions to allow for cross-context synchronization and potentially-expensive + * value lookup. + * + * Values in the cache _may_ expire and are retrieved as [ExpiringValue]. When a value is absent/expired in the cache, + * invoking [get] will cause a lookup to occur via the function's `valueLookup` parameter. + * + * @param K The type of the keys of this cache + * @param V The type of the values of this cache + */ +public interface ExpiringKeyedCache { + /** + * The number of values currently stored in the cache + */ + public val size: Int + + /** + * Gets the value associated with this key from the cache. If the cache does not contain the given key, + * implementations are expected to invoke [valueLookup], although they _may_ perform other actions such as throw + * exceptions, fall back to other caches, etc. + * @param key The key for which to look up a value + * @param valueLookup A possibly-suspending function which returns the read-through value associated with a given + * key. This function is invoked when the cache does not contain the given [key] or when the value is expired. + */ + public suspend fun get(key: K, valueLookup: suspend (K) -> ExpiringValue): V + + /** + * Invalidates the value (if any) for the given key, removing it from the cache regardless. This method has no + * effect if the given key is not present in the cache. + * @param key The key for which to invalidate a value + */ + public suspend fun invalidate(key: K) +} diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/ReadThroughCache.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/PeriodicSweepCache.kt similarity index 74% rename from runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/ReadThroughCache.kt rename to runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/PeriodicSweepCache.kt index 529a57d450..0940aaec2d 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/ReadThroughCache.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/PeriodicSweepCache.kt @@ -12,8 +12,9 @@ import kotlinx.coroutines.sync.withLock import kotlin.time.Duration /** - * An object which caches values and allows retrieving them by key. The values expire after a time. If a value is - * expired or absent from the cache, it will be read from a `valueLookup` parameter passed to [get] and then cached. + * A cache which allows retrieving values by a key. Looking up a value for a key which does not exist in the cache (or + * where the value has expired) are resolved by calling [valueLookup]. The expiry for a value is included in the result + * returned from [valueLookup]. * * A sweep operation will run prior to a [get] or [invalidate] that happens after [minimumSweepPeriod] has elapsed from * the last sweep (or from the initialization of the cache). This sweep will search for and remove expired entries from @@ -29,10 +30,10 @@ import kotlin.time.Duration * @param clock The [Clock] to use for measuring time. Defaults to [Clock.System]. */ @InternalApi -public class ReadThroughCache( +public class PeriodicSweepCache( private val minimumSweepPeriod: Duration, private val clock: Clock = Clock.System, -) { +) : ExpiringKeyedCache { private val map = mutableMapOf>() private val mutex = Mutex() private var nextSweep = clock.now() + minimumSweepPeriod @@ -44,7 +45,7 @@ public class ReadThroughCache( * @param valueLookup A possibly-suspending function which returns the read-through value associated with a given * key. This function is invoked when the cache, for a given key, does not contain a value or the value is expired. */ - public suspend fun get(key: K, valueLookup: suspend (K) -> ExpiringValue): V = mutex.withLock { + override suspend fun get(key: K, valueLookup: suspend (K) -> ExpiringValue): V = mutex.withLock { if (clock.now() > nextSweep) sweep() val current = map[key] @@ -59,20 +60,29 @@ public class ReadThroughCache( * Invalidates the value (if any) for the given key, removing it from the cache regardless of its expiry. * @param key The key for which to invalidate a value. */ - public suspend fun invalidate(key: K): Unit = mutex.withLock { + override suspend fun invalidate(key: K): Unit = mutex.withLock { map.remove(key) if (clock.now() > nextSweep) sweep() } + /** + * Indicates whether this value is expired according to its [ExpiringValue.expiresAt] property and the cache's + * [clock] + */ private val ExpiringValue<*>.isExpired: Boolean get() = clock.now() >= expiresAt /** - * Gets the number of values currently stored in the cache. + * Gets the number of values currently stored in the cache. Note that this property is non-volatile and may reflect + * stale information in highly-concurrent scenarios. */ - public val size: Int + override val size: Int get() = map.size + /** + * Sweeps the cache to remove expired entries and schedule the next sweep. This method _must_ be invoked under mutex + * lock. + */ private fun sweep() { val iterator = map.iterator() while (iterator.hasNext()) { diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/smoketests/SmokeTestsFunctions.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/smoketests/SmokeTestsFunctions.kt index 3da3bb8c23..9cab81bdbc 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/smoketests/SmokeTestsFunctions.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/smoketests/SmokeTestsFunctions.kt @@ -2,27 +2,6 @@ package aws.smithy.kotlin.runtime.smoketests public expect fun exitProcess(status: Int): Nothing -/** - * Prints an exceptions stack trace using test anything protocol (TAP) format e.g. - * - * #java.lang.ArithmeticException: / by zero - * # at FileKt.main(File.kt:3) - * # at FileKt.main(File.kt) - * # at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) - * # at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) - * # at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) - * # at java.base/java.lang.reflect.Method.invoke(Unknown Source) - * # at executors.JavaRunnerExecutor$Companion.main(JavaRunnerExecutor.kt:27) - * # at executors.JavaRunnerExecutor.main(JavaRunnerExecutor.kt) - */ -@Deprecated( - message = "No longer used, target for removal in 1.5", - replaceWith = ReplaceWith("println(exception.stackTraceToString().prependIndent(\"#\"))"), - level = DeprecationLevel.WARNING, -) -public fun printExceptionStackTrace(exception: Exception): Unit = - println(exception.stackTraceToString().split("\n").joinToString("\n") { "#$it" }) - public class SmokeTestsException(message: String) : Exception(message) /** diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/ReadThroughCacheTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/PeriodicSweepCacheTest.kt similarity index 91% rename from runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/ReadThroughCacheTest.kt rename to runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/PeriodicSweepCacheTest.kt index 4d85ece15f..f070669c1f 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/ReadThroughCacheTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/PeriodicSweepCacheTest.kt @@ -12,13 +12,13 @@ import kotlin.test.assertEquals import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -class ReadThroughCacheTest { +class PeriodicSweepCacheTest { @Test - fun testReadThrough() = runTest { + fun testGet() = runTest { val clock = ManualClock() var counter = 0 fun uncachedValue() = ExpiringValue(counter++, clock.now() + 2.seconds) - val cache = ReadThroughCache(1.minutes, clock) + val cache = PeriodicSweepCache(1.minutes, clock) // Basic read through assertEquals(0, cache.get("a") { uncachedValue() }) @@ -41,7 +41,7 @@ class ReadThroughCacheTest { val clock = ManualClock() var counter = 0 fun uncachedValue() = ExpiringValue(counter++, clock.now() + 2.seconds) - val cache = ReadThroughCache(4.seconds, clock) + val cache = PeriodicSweepCache(4.seconds, clock) // Pre-populate values assertEquals(0, cache.get("a") { uncachedValue() }) diff --git a/runtime/serde/serde-xml/api/serde-xml.api b/runtime/serde/serde-xml/api/serde-xml.api index 736408bd97..eabc8febb8 100644 --- a/runtime/serde/serde-xml/api/serde-xml.api +++ b/runtime/serde/serde-xml/api/serde-xml.api @@ -47,16 +47,6 @@ public final class aws/smithy/kotlin/runtime/serde/xml/XmlCollectionValueNamespa public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V } -public final class aws/smithy/kotlin/runtime/serde/xml/XmlDeserializer : aws/smithy/kotlin/runtime/serde/Deserializer { - public fun (Laws/smithy/kotlin/runtime/serde/xml/XmlStreamReader;Z)V - public synthetic fun (Laws/smithy/kotlin/runtime/serde/xml/XmlStreamReader;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun ([BZ)V - public synthetic fun ([BZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun deserializeList (Laws/smithy/kotlin/runtime/serde/SdkFieldDescriptor;)Laws/smithy/kotlin/runtime/serde/Deserializer$ElementIterator; - public fun deserializeMap (Laws/smithy/kotlin/runtime/serde/SdkFieldDescriptor;)Laws/smithy/kotlin/runtime/serde/Deserializer$EntryIterator; - public fun deserializeStruct (Laws/smithy/kotlin/runtime/serde/SdkObjectDescriptor;)Laws/smithy/kotlin/runtime/serde/Deserializer$FieldIterator; -} - public final class aws/smithy/kotlin/runtime/serde/xml/XmlError : aws/smithy/kotlin/runtime/serde/FieldTrait { public static final field INSTANCE Laws/smithy/kotlin/runtime/serde/xml/XmlError; public final fun getErrorTag ()Laws/smithy/kotlin/runtime/serde/xml/XmlToken$QualifiedName; @@ -155,8 +145,10 @@ public abstract interface class aws/smithy/kotlin/runtime/serde/xml/XmlStreamRea public abstract fun getLastToken ()Laws/smithy/kotlin/runtime/serde/xml/XmlToken; public abstract fun nextToken ()Laws/smithy/kotlin/runtime/serde/xml/XmlToken; public abstract fun peek (I)Laws/smithy/kotlin/runtime/serde/xml/XmlToken; + public static synthetic fun peek$default (Laws/smithy/kotlin/runtime/serde/xml/XmlStreamReader;IILjava/lang/Object;)Laws/smithy/kotlin/runtime/serde/xml/XmlToken; public abstract fun skipNext ()V public abstract fun subTreeReader (Laws/smithy/kotlin/runtime/serde/xml/XmlStreamReader$SubtreeStartDepth;)Laws/smithy/kotlin/runtime/serde/xml/XmlStreamReader; + public static synthetic fun subTreeReader$default (Laws/smithy/kotlin/runtime/serde/xml/XmlStreamReader;Laws/smithy/kotlin/runtime/serde/xml/XmlStreamReader$SubtreeStartDepth;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/serde/xml/XmlStreamReader; } public final class aws/smithy/kotlin/runtime/serde/xml/XmlStreamReader$DefaultImpls { @@ -178,13 +170,17 @@ public final class aws/smithy/kotlin/runtime/serde/xml/XmlStreamReaderKt { public abstract interface class aws/smithy/kotlin/runtime/serde/xml/XmlStreamWriter { public abstract fun attribute (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Laws/smithy/kotlin/runtime/serde/xml/XmlStreamWriter; + public static synthetic fun attribute$default (Laws/smithy/kotlin/runtime/serde/xml/XmlStreamWriter;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/serde/xml/XmlStreamWriter; public abstract fun endDocument ()V public abstract fun endTag (Ljava/lang/String;Ljava/lang/String;)Laws/smithy/kotlin/runtime/serde/xml/XmlStreamWriter; + public static synthetic fun endTag$default (Laws/smithy/kotlin/runtime/serde/xml/XmlStreamWriter;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/serde/xml/XmlStreamWriter; public abstract fun getBytes ()[B public abstract fun getText ()Ljava/lang/String; public abstract fun namespacePrefix (Ljava/lang/String;Ljava/lang/String;)V + public static synthetic fun namespacePrefix$default (Laws/smithy/kotlin/runtime/serde/xml/XmlStreamWriter;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V public abstract fun startDocument ()V public abstract fun startTag (Ljava/lang/String;Ljava/lang/String;)Laws/smithy/kotlin/runtime/serde/xml/XmlStreamWriter; + public static synthetic fun startTag$default (Laws/smithy/kotlin/runtime/serde/xml/XmlStreamWriter;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/serde/xml/XmlStreamWriter; public abstract fun text (Ljava/lang/String;)Laws/smithy/kotlin/runtime/serde/xml/XmlStreamWriter; } diff --git a/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlDeserializer.kt b/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlDeserializer.kt deleted file mode 100644 index b950e888b6..0000000000 --- a/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlDeserializer.kt +++ /dev/null @@ -1,431 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package aws.smithy.kotlin.runtime.serde.xml - -import aws.smithy.kotlin.runtime.InternalApi -import aws.smithy.kotlin.runtime.content.BigDecimal -import aws.smithy.kotlin.runtime.content.BigInteger -import aws.smithy.kotlin.runtime.content.Document -import aws.smithy.kotlin.runtime.serde.* -import aws.smithy.kotlin.runtime.text.encoding.decodeBase64Bytes -import aws.smithy.kotlin.runtime.time.Instant -import aws.smithy.kotlin.runtime.time.TimestampFormat - -private const val FIRST_FIELD_INDEX: Int = 0 - -// Represents aspects of SdkFieldDescriptor that are particular to the Xml format -internal sealed class FieldLocation { - // specifies the mapping to a sdk field index - abstract val fieldIndex: Int - - data class Text(override val fieldIndex: Int) : FieldLocation() // Xml nodes have only one associated Text element - data class Attribute(override val fieldIndex: Int, val names: Set) : FieldLocation() -} - -/** - * Provides a deserializer for XML documents - * - * @param reader underlying [XmlStreamReader] from which tokens are read - * @param validateRootElement Flag indicating if the root XML document [XmlToken.BeginElement] should be validated against - * the descriptor passed to [deserializeStruct]. This only affects the root element, not nested struct elements. Some - * restXml based services DO NOT always send documents with a root element name that matches the shape ID name - * (S3 in particular). This means there is nothing in the model that gives you enough information to validate the tag. - */ -@Deprecated("XmlDeserializer is deprecated and will be removed in a future release") -@InternalApi -public class XmlDeserializer( - private val reader: XmlStreamReader, - private val validateRootElement: Boolean = false, -) : Deserializer { - - public constructor(input: ByteArray, validateRootElement: Boolean = false) : this(xmlStreamReader(input), validateRootElement) - - private var firstStructCall = true - - override fun deserializeStruct(descriptor: SdkObjectDescriptor): Deserializer.FieldIterator { - if (firstStructCall) { - if (!descriptor.hasTrait()) throw DeserializationException("Top-level struct $descriptor requires a XmlSerialName trait but has none.") - - firstStructCall = false - - reader.nextToken() // Matching field descriptors to children tags so consume the start element of top-level struct - - val structToken = if (descriptor.hasTrait()) { - reader.seek { it.name == descriptor.expectTrait().errorTag } - } else { - reader.seek() - } ?: throw DeserializationException("Could not find a begin element for new struct") - - if (validateRootElement) { - descriptor.requireNameMatch(structToken.name.tag) - } - } - - // Consume any remaining terminating tokens from previous deserialization - reader.seek() - - // Because attributes set on the root node of the struct, we must read the values before creating the subtree - val attribFields = reader.tokenAttributesToFieldLocations(descriptor) - val parentToken = if (reader.lastToken is XmlToken.BeginElement) { - reader.lastToken as XmlToken.BeginElement - } else { - throw DeserializationException("Expected last parsed token to be ${XmlToken.BeginElement::class} but was ${reader.lastToken}") - } - - val unwrapped = descriptor.hasTrait() - return XmlStructDeserializer(descriptor, reader.subTreeReader(XmlStreamReader.SubtreeStartDepth.CURRENT), parentToken, attribFields, unwrapped) - } - - override fun deserializeList(descriptor: SdkFieldDescriptor): Deserializer.ElementIterator { - val depth = when (descriptor.hasTrait()) { - true -> XmlStreamReader.SubtreeStartDepth.CURRENT - else -> XmlStreamReader.SubtreeStartDepth.CHILD - } - - return XmlListDeserializer(reader.subTreeReader(depth), descriptor) - } - - override fun deserializeMap(descriptor: SdkFieldDescriptor): Deserializer.EntryIterator { - val depth = when (descriptor.hasTrait()) { - true -> XmlStreamReader.SubtreeStartDepth.CURRENT - else -> XmlStreamReader.SubtreeStartDepth.CHILD - } - - return XmlMapDeserializer(reader.subTreeReader(depth), descriptor) - } -} - -/** - * Deserializes specific XML structures into forms that can produce Maps - * - * @param reader underlying [XmlStreamReader] from which tokens are read - * @param descriptor associated [SdkFieldDescriptor] which represents the expected Map - * @param primitiveDeserializer used to deserialize primitive values - */ -internal class XmlMapDeserializer( - private val reader: XmlStreamReader, - private val descriptor: SdkFieldDescriptor, - private val primitiveDeserializer: PrimitiveDeserializer = XmlPrimitiveDeserializer(reader, descriptor), -) : PrimitiveDeserializer by primitiveDeserializer, - Deserializer.EntryIterator { - private val mapTrait = descriptor.findTrait() ?: XmlMapName.Default - - override fun hasNextEntry(): Boolean { - val compareTo = when (descriptor.hasTrait()) { - true -> descriptor.findTrait()?.name ?: mapTrait.key // Prefer seeking to XmlSerialName if the trait exists - false -> mapTrait.entry - } - - // Seek to either the XML serial name, entry, or key token depending on the flatness of the map and if the name trait is present - val nextEntryToken = when (descriptor.hasTrait()) { - true -> reader.peekSeek { it.name.local == compareTo } - false -> reader.seek { it.name.local == compareTo } - } - - return nextEntryToken != null - } - - override fun key(): String { - // Seek to the key begin token - reader.seek { it.name.local == mapTrait.key } - ?: error("Unable to find key $mapTrait.key in $descriptor") - - val keyValueToken = reader.takeNextAs() - reader.nextToken() // Consume the end wrapper - - return keyValueToken.value ?: throw DeserializationException("Key unspecified in $descriptor") - } - - override fun nextHasValue(): Boolean { - // Expect a begin and value (or another begin) token if Map entry has a value - val peekBeginToken = reader.peek(1) ?: throw DeserializationException("Unexpected termination of token stream in $descriptor") - val peekValueToken = reader.peek(2) ?: throw DeserializationException("Unexpected termination of token stream in $descriptor") - - return peekBeginToken !is XmlToken.EndElement && peekValueToken !is XmlToken.EndElement - } -} - -/** - * Deserializes specific XML structures into forms that can produce Lists - * - * @param reader underlying [XmlStreamReader] from which tokens are read - * @param descriptor associated [SdkFieldDescriptor] which represents the expected Map - * @param primitiveDeserializer used to deserialize primitive values - */ -internal class XmlListDeserializer( - private val reader: XmlStreamReader, - private val descriptor: SdkFieldDescriptor, - private val primitiveDeserializer: PrimitiveDeserializer = XmlPrimitiveDeserializer(reader, descriptor), -) : PrimitiveDeserializer by primitiveDeserializer, - Deserializer.ElementIterator { - private var firstCall = true - private val flattened = descriptor.hasTrait() - private val elementName = (descriptor.findTrait() ?: XmlCollectionName.Default).element - - override fun hasNextElement(): Boolean { - if (!flattened && firstCall) { - val nextToken = reader.peek() - val matchedListDescriptor = nextToken is XmlToken.BeginElement && descriptor.nameMatches(nextToken.name.tag) - val hasChildren = if (nextToken == null) false else nextToken.depth >= reader.lastToken!!.depth - - if (!matchedListDescriptor && !hasChildren) return false - - // Discard the wrapper and move to the first element in the list - if (matchedListDescriptor) reader.nextToken() - - firstCall = false - } - - if (flattened) { - // Because our subtree is not CHILD, we cannot rely on the subtree boundary to determine end of collection. - // Rather, we search for either the next begin token matching the (flat) list member name which should - // be immediately after the current token - - // peek at the next token if there is one, in the case of a list of structs, the next token is actually - // the end of the current flat list element in which case we need to peek twice - val next = when (val peeked = reader.peek()) { - is XmlToken.EndElement -> { - if (peeked.name.local == descriptor.serialName.name) { - // consume the end token - reader.nextToken() - reader.peek() - } else { - peeked - } - } - else -> peeked - } - - val tokens = listOfNotNull(reader.lastToken, next) - - // Iterate over the token stream until begin token matching name is found or end element matching list is found. - return tokens - .filterIsInstance() - .any { it.name.local == descriptor.serialName.name } - } else { - // If we can find another begin token w/ the element name, we have more elements to process - return reader.seek { it.name.local == elementName }.isNotTerminal() - } - } - - override fun nextHasValue(): Boolean = reader.peek() !is XmlToken.EndElement -} - -/** - * Deserializes specific XML structures into forms that can produce structures - * - * @param objDescriptor associated [SdkObjectDescriptor] which represents the expected structure - * @param reader underlying [XmlStreamReader] from which tokens are read - * @param parentToken initial token of associated structure - * @param parsedFieldLocations list of [FieldLocation] representing values able to be loaded into deserialized instances - */ -private class XmlStructDeserializer( - private val objDescriptor: SdkObjectDescriptor, - reader: XmlStreamReader, - private val parentToken: XmlToken.BeginElement, - private val parsedFieldLocations: MutableList = mutableListOf(), - private val unwrapped: Boolean, -) : Deserializer.FieldIterator { - // Used to track direct deserialization or further nesting between calls to findNextFieldIndex() and deserialize() - private var reentryFlag: Boolean = false - - private val reader: XmlStreamReader = if (unwrapped) reader else reader.subTreeReader(XmlStreamReader.SubtreeStartDepth.CHILD) - - override fun findNextFieldIndex(): Int? { - if (unwrapped) { - return if (reader.peek() is XmlToken.Text) FIRST_FIELD_INDEX else null - } - if (inNestedMode()) { - // Returning from a nested struct call. Nested deserializer consumed - // tokens so clear them here to avoid processing stale state - parsedFieldLocations.clear() - } - - if (parsedFieldLocations.isEmpty()) { - val matchedFieldLocations = when (val token = reader.nextToken()) { - null, is XmlToken.EndDocument -> return null - is XmlToken.EndElement -> return findNextFieldIndex() - is XmlToken.BeginElement -> { - val nextToken = reader.peek() ?: return null - val objectFields = objDescriptor.fields - val memberFields = objectFields.filter { field -> objDescriptor.fieldTokenMatcher(field, token) } - val matchingFields = memberFields.mapNotNull { it.findFieldLocation(token, nextToken) } - matchingFields - } - else -> return findNextFieldIndex() - } - - // Sorting ensures attribs are processed before text, as processing the Text token pushes the parser on to the next token. - parsedFieldLocations.addAll(matchedFieldLocations.sortedBy { it is FieldLocation.Text }) - } - - return parsedFieldLocations.firstOrNull()?.fieldIndex ?: Deserializer.FieldIterator.UNKNOWN_FIELD - } - - private fun deserializeValue(transform: ((String) -> T)): T { - if (unwrapped) { - val value = reader.takeNextAs().value ?: "" - return transform(value) - } - // Set and validate mode - reentryFlag = false - if (parsedFieldLocations.isEmpty()) throw DeserializationException("matchedFields is empty, was findNextFieldIndex() called?") - - // Take the first FieldLocation and attempt to parse it into the value specified by the descriptor. - return when (val nextField = parsedFieldLocations.removeFirst()) { - is FieldLocation.Text -> { - val value = when (val peekToken = reader.peek()) { - is XmlToken.Text -> reader.takeNextAs().value ?: "" - is XmlToken.EndElement -> "" - else -> throw DeserializationException("Unexpected token $peekToken") - } - transform(value) - } - is FieldLocation.Attribute -> { - transform( - nextField - .names - .mapNotNull { parentToken.attributes[it] } - .firstOrNull() ?: throw DeserializationException("Expected attrib value ${nextField.names.first()} not found in ${parentToken.name}"), - ) - } - } - } - - override fun skipValue() = reader.skipNext() - - override fun deserializeByte(): Byte = deserializeValue { it.toIntOrNull()?.toByte() ?: throw DeserializationException("Unable to deserialize $it") } - - override fun deserializeInt(): Int = deserializeValue { it.toIntOrNull() ?: throw DeserializationException("Unable to deserialize $it") } - - override fun deserializeShort(): Short = deserializeValue { it.toIntOrNull()?.toShort() ?: throw DeserializationException("Unable to deserialize $it") } - - override fun deserializeLong(): Long = deserializeValue { it.toLongOrNull() ?: throw DeserializationException("Unable to deserialize $it") } - - override fun deserializeFloat(): Float = deserializeValue { it.toFloatOrNull() ?: throw DeserializationException("Unable to deserialize $it") } - - override fun deserializeDouble(): Double = deserializeValue { it.toDoubleOrNull() ?: throw DeserializationException("Unable to deserialize $it") } - - override fun deserializeBigInteger(): BigInteger = deserializeValue { - runCatching { BigInteger(it) } - .getOrElse { throw DeserializationException("Unable to deserialize $it as BigInteger") } - } - - override fun deserializeBigDecimal(): BigDecimal = deserializeValue { - runCatching { BigDecimal(it) } - .getOrElse { throw DeserializationException("Unable to deserialize $it as BigDecimal") } - } - - override fun deserializeString(): String = deserializeValue { it } - - override fun deserializeBoolean(): Boolean = deserializeValue { it.toBoolean() } - - override fun deserializeDocument(): Document = throw DeserializationException("cannot deserialize unsupported Document type in xml") - - override fun deserializeByteArray(): ByteArray = deserializeString().decodeBase64Bytes() - - override fun deserializeInstant(format: TimestampFormat): Instant = when (format) { - TimestampFormat.EPOCH_SECONDS -> deserializeString().let { Instant.fromEpochSeconds(it) } - TimestampFormat.ISO_8601 -> deserializeString().let { Instant.fromIso8601(it) } - TimestampFormat.RFC_5322 -> deserializeString().let { Instant.fromRfc5322(it) } - else -> throw DeserializationException("unknown timestamp format: $format") - } - - override fun deserializeNull(): Nothing? { - reader.takeNextAs() - return null - } - - // A struct deserializer can be called in two "modes": - // 1. to deserialize a value. This calls findNextFieldIndex() followed by deserialize() - // 2. to deserialize a nested container. This calls findNextFieldIndex() followed by a call to another deserialize() - // Because state is built in findNextFieldIndex() that is intended to be used directly in deserialize() (mode 1) - // and there is no explicit way that this type knows which mode is in use, the state built must be cleared. - // this is done by flipping a bit between the two calls. If the bit has not been flipped on any call to findNextFieldIndex() - // it is determined that the nested mode was used and any existing state should be cleared. - // if the state is not cleared, deserialization goes into an infinite loop because the deserializer sees pending fields to pull from the stream - // which are never consumed by the (missing) call to deserialize() - private fun inNestedMode(): Boolean = when (reentryFlag) { - true -> true - false -> { - reentryFlag = true - false - } - } -} - -// Extract the attributes from the last-read token and match them to [FieldLocation] on the [SdkObjectDescriptor]. -private fun XmlStreamReader.tokenAttributesToFieldLocations(descriptor: SdkObjectDescriptor): MutableList = - if (descriptor.hasXmlAttributes && lastToken is XmlToken.BeginElement) { - val attribFields = descriptor.fields.filter { it.hasTrait() } - val matchedAttribFields = attribFields.filter { it.findFieldLocation(lastToken as XmlToken.BeginElement, peek() ?: throw DeserializationException("Unexpected end of tokens")) != null } - matchedAttribFields.map { FieldLocation.Attribute(it.index, it.toQualifiedNames()) } - .toMutableList() - } else { - mutableListOf() - } - -// Returns a [FieldLocation] if the field maps to the current token -private fun SdkFieldDescriptor.findFieldLocation( - currentToken: XmlToken.BeginElement, - nextToken: XmlToken, -): FieldLocation? = when (val property = toFieldLocation()) { - is FieldLocation.Text -> { - when { - nextToken is XmlToken.Text -> property - nextToken is XmlToken.BeginElement -> property - // The following allows for struct primitives to remain unvisited if no value - // but causes nested deserializers to be called even if they contain no value - nextToken is XmlToken.EndElement && currentToken.name == nextToken.name -> property - else -> null - } - } - is FieldLocation.Attribute -> { - val foundMatch = property.names.any { currentToken.attributes[it]?.isNotBlank() == true } - if (foundMatch) property else null - } -} - -// Produce a [FieldLocation] type based on presence of traits of field -// A field without an attribute trait is assumed to be a text token -private fun SdkFieldDescriptor.toFieldLocation(): FieldLocation = - when (findTrait()) { - null -> FieldLocation.Text(index) // Assume a text value if no attributes defined. - else -> FieldLocation.Attribute(index, toQualifiedNames()) - } - -// Matches fields and tokens with matching qualified name -private fun SdkObjectDescriptor.fieldTokenMatcher(fieldDescriptor: SdkFieldDescriptor, beginElement: XmlToken.BeginElement): Boolean { - if (fieldDescriptor.kind == SerialKind.List && fieldDescriptor.hasTrait()) { - val fieldName = fieldDescriptor.findTrait() ?: XmlCollectionName.Default - val tokenQname = beginElement.name - - // It may be that we are matching a flattened list element or matching a list itself. In the latter - // case the following predicate will not work, so if we fail to match the member - // try again (below) to match against the container. - if (fieldName.element == tokenQname.local) return true - } - - return fieldDescriptor.nameMatches(beginElement.name.tag) -} - -/** - * Return the next token of the specified type or throw [DeserializationException] if incorrect type. - */ -internal inline fun XmlStreamReader.takeNextAs(): TExpected { - val token = this.nextToken() ?: throw DeserializationException("Expected ${TExpected::class} but instead found null") - requireToken(token) - return token as TExpected -} - -/** - * Require that the given token be of type [TExpected] or else throw an exception - */ -internal inline fun requireToken(token: XmlToken) { - if (token::class != TExpected::class) { - throw DeserializationException("Expected ${TExpected::class}; found ${token::class} ($token)") - } -} diff --git a/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlStreamReader.kt b/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlStreamReader.kt index a43250b267..760f52a833 100644 --- a/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlStreamReader.kt +++ b/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlStreamReader.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.serde.xml import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.serde.DeserializationException import aws.smithy.kotlin.runtime.serde.xml.deserialization.LexingXmlStreamReader import aws.smithy.kotlin.runtime.serde.xml.deserialization.StringTextStream import aws.smithy.kotlin.runtime.serde.xml.deserialization.XmlLexer @@ -114,6 +115,24 @@ public inline fun XmlStreamReader.peekSeek(selectionPredi return null } +/** + * Return the next token of the specified type or throw [DeserializationException] if incorrect type. + */ +internal inline fun XmlStreamReader.takeNextAs(): TExpected { + val token = this.nextToken() ?: throw DeserializationException("Expected ${TExpected::class} but instead found null") + requireToken(token) + return token as TExpected +} + +/** + * Require that the given token be of type [TExpected] or else throw an exception + */ +private inline fun requireToken(token: XmlToken) { + if (token::class != TExpected::class) { + throw DeserializationException("Expected ${TExpected::class}; found ${token::class} ($token)") + } +} + /** * Creates an [XmlStreamReader] instance */ diff --git a/runtime/smithy-client/api/smithy-client.api b/runtime/smithy-client/api/smithy-client.api index b6132b26b8..823ff0467d 100644 --- a/runtime/smithy-client/api/smithy-client.api +++ b/runtime/smithy-client/api/smithy-client.api @@ -30,47 +30,25 @@ public final class aws/smithy/kotlin/runtime/client/IdempotencyTokenProvider$Com } public abstract interface class aws/smithy/kotlin/runtime/client/Interceptor { - public abstract fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun modifyBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun modifyBeforeRetryLoop (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun modifyBeforeSerialization (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun modifyBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun readAfterAttempt (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V - public abstract fun readAfterDeserialization (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V - public abstract fun readAfterExecution (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V - public abstract fun readAfterSerialization (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V - public abstract fun readAfterSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V - public abstract fun readAfterTransmit (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V - public abstract fun readBeforeAttempt (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V - public abstract fun readBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V - public abstract fun readBeforeExecution (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V - public abstract fun readBeforeSerialization (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V - public abstract fun readBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V - public abstract fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V -} - -public final class aws/smithy/kotlin/runtime/client/Interceptor$DefaultImpls { - public static fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun modifyBeforeDeserialization (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun modifyBeforeRetryLoop (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun modifyBeforeSerialization (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun modifyBeforeTransmit (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun readAfterAttempt (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V - public static fun readAfterDeserialization (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V - public static fun readAfterExecution (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V - public static fun readAfterSerialization (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V - public static fun readAfterSigning (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V - public static fun readAfterTransmit (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V - public static fun readBeforeAttempt (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V - public static fun readBeforeDeserialization (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V - public static fun readBeforeExecution (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V - public static fun readBeforeSerialization (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V - public static fun readBeforeSigning (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V - public static fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/Interceptor;Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeRetryLoop (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeSerialization (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun readAfterAttempt (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V + public fun readAfterDeserialization (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V + public fun readAfterExecution (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V + public fun readAfterSerialization (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readAfterSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readAfterTransmit (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V + public fun readBeforeAttempt (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V + public fun readBeforeExecution (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V + public fun readBeforeSerialization (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V + public fun readBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } public abstract class aws/smithy/kotlin/runtime/client/LogMode { @@ -187,7 +165,7 @@ public abstract interface class aws/smithy/kotlin/runtime/client/SdkClientConfig public abstract interface class aws/smithy/kotlin/runtime/client/SdkClientFactory { public abstract fun builder ()Laws/smithy/kotlin/runtime/client/SdkClient$Builder; - public abstract fun invoke (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/client/SdkClient; + public fun invoke (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/client/SdkClient; } public final class aws/smithy/kotlin/runtime/client/SdkClientFactory$DefaultImpls { @@ -225,7 +203,7 @@ public abstract interface class aws/smithy/kotlin/runtime/client/config/Compress public abstract interface class aws/smithy/kotlin/runtime/client/config/CompressionClientConfig$Builder { public abstract fun getRequestCompression ()Laws/smithy/kotlin/runtime/client/config/RequestCompressionConfig$Builder; - public abstract fun requestCompression (Lkotlin/jvm/functions/Function1;)V + public fun requestCompression (Lkotlin/jvm/functions/Function1;)V } public final class aws/smithy/kotlin/runtime/client/config/CompressionClientConfig$Builder$DefaultImpls { @@ -353,3 +331,15 @@ public final class aws/smithy/kotlin/runtime/client/endpoints/functions/Url { public fun toString ()Ljava/lang/String; } +public abstract interface class aws/smithy/kotlin/runtime/client/region/RegionProvider { + public abstract fun getRegion (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public class aws/smithy/kotlin/runtime/client/region/RegionProviderChain : aws/smithy/kotlin/runtime/client/region/RegionProvider { + public fun (Ljava/util/List;)V + public fun ([Laws/smithy/kotlin/runtime/client/region/RegionProvider;)V + protected final fun getProviders ()[Laws/smithy/kotlin/runtime/client/region/RegionProvider; + public fun getRegion (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; +} + diff --git a/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/Interceptor.kt b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/Interceptor.kt index 8d2b5c68bc..4fab16b9a7 100644 --- a/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/Interceptor.kt +++ b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/Interceptor.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.client +import aws.smithy.kotlin.runtime.client.util.MpJvmDefaultWithoutCompatibility import aws.smithy.kotlin.runtime.operation.ExecutionContext /** @@ -20,6 +21,7 @@ import aws.smithy.kotlin.runtime.operation.ExecutionContext * **MUST** not modify state even if it is possible to do so (it is not always possible or performant to provide an * immutable view of every type). */ +@MpJvmDefaultWithoutCompatibility public interface Interceptor< Input, Output, diff --git a/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/region/RegionProvider.kt b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/region/RegionProvider.kt new file mode 100644 index 0000000000..9e82ec4509 --- /dev/null +++ b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/region/RegionProvider.kt @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.client.region + +/** + * Interface for providing AWS region information. Implementations are free to use any strategy for + * providing region information + */ +public interface RegionProvider { + /** + * Return the region name to use. If region information is not available, implementations should return null + */ + public suspend fun getRegion(): String? +} diff --git a/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/region/RegionProviderChain.kt b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/region/RegionProviderChain.kt new file mode 100644 index 0000000000..0df388d11f --- /dev/null +++ b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/region/RegionProviderChain.kt @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.client.region + +import aws.smithy.kotlin.runtime.telemetry.logging.logger +import aws.smithy.kotlin.runtime.util.asyncLazy +import kotlin.coroutines.coroutineContext + +/** + * Composite [RegionProvider] that delegates to a chain of providers. + * [providers] are consulted in the order given and the first region found is returned + * + * @param providers the list of providers to delegate to + */ +public open class RegionProviderChain( + protected vararg val providers: RegionProvider, +) : RegionProvider { + + public constructor(providers: List) : this(*providers.toTypedArray()) + + private val resolvedRegion = asyncLazy(::resolveRegion) + + init { + require(providers.isNotEmpty()) { "at least one provider must be in the chain" } + } + + override fun toString(): String = + (listOf(this) + providers).map { it::class.simpleName }.joinToString(" -> ") + + override suspend fun getRegion(): String? = resolvedRegion.get() + + private suspend fun resolveRegion(): String? { + val logger = coroutineContext.logger() + for (provider in providers) { + try { + val region = provider.getRegion() + if (region != null) { + logger.debug { "resolved region ($region) from $provider " } + return region + } + logger.debug { "failed to resolve region from $provider" } + } catch (ex: Exception) { + logger.debug { "unable to load region from $provider: ${ex.message}" } + } + } + + return null + } +} diff --git a/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/util/Annotations.kt b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/util/Annotations.kt new file mode 100644 index 0000000000..f7b9449ebc --- /dev/null +++ b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/util/Annotations.kt @@ -0,0 +1,13 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.client.util + +/** + * A KMP-compatible variant of [kotlin.jvm.JvmDefaultWithoutCompatibility] + */ +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.CLASS) +public expect annotation class MpJvmDefaultWithoutCompatibility() diff --git a/runtime/smithy-client/common/test/aws/smithy/kotlin/runtime/client/region/RegionProviderChainTest.kt b/runtime/smithy-client/common/test/aws/smithy/kotlin/runtime/client/region/RegionProviderChainTest.kt new file mode 100644 index 0000000000..361336ac2d --- /dev/null +++ b/runtime/smithy-client/common/test/aws/smithy/kotlin/runtime/client/region/RegionProviderChainTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.client.region + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails + +class RegionProviderChainTest { + @Test + fun testNoProviders() { + assertFails("at least one provider") { + RegionProviderChain() + } + } + data class TestProvider(val region: String? = null) : RegionProvider { + override suspend fun getRegion(): String? = region + } + + @Test + fun testChain() = runTest { + val chain = RegionProviderChain( + TestProvider(null), + TestProvider("us-east-1"), + TestProvider("us-east-2"), + ) + + assertEquals("us-east-1", chain.getRegion()) + } + + @Test + fun testChainList() = runTest { + val providers = listOf( + TestProvider(null), + TestProvider("us-east-1"), + TestProvider("us-east-2"), + ) + + val chain = RegionProviderChain(providers) + + assertEquals("us-east-1", chain.getRegion()) + } +} diff --git a/runtime/smithy-client/jvm/src/aws/smithy/kotlin/runtime/client/util/AnnotationsJVM.kt b/runtime/smithy-client/jvm/src/aws/smithy/kotlin/runtime/client/util/AnnotationsJVM.kt new file mode 100644 index 0000000000..db7512a9df --- /dev/null +++ b/runtime/smithy-client/jvm/src/aws/smithy/kotlin/runtime/client/util/AnnotationsJVM.kt @@ -0,0 +1,8 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.client.util + +public actual typealias MpJvmDefaultWithoutCompatibility = kotlin.jvm.JvmDefaultWithoutCompatibility diff --git a/runtime/smithy-client/native/src/aws/smithy/kotlin/runtime/client/util/AnnotationsNative.kt b/runtime/smithy-client/native/src/aws/smithy/kotlin/runtime/client/util/AnnotationsNative.kt new file mode 100644 index 0000000000..118ea69bda --- /dev/null +++ b/runtime/smithy-client/native/src/aws/smithy/kotlin/runtime/client/util/AnnotationsNative.kt @@ -0,0 +1,10 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.client.util + +public actual annotation class MpJvmDefaultWithoutCompatibility { + // No-op on non-JVM platforms +} diff --git a/tests/codegen/waiter-tests/build.gradle.kts b/tests/codegen/waiter-tests/build.gradle.kts index 72b244a6c9..992a4d471e 100644 --- a/tests/codegen/waiter-tests/build.gradle.kts +++ b/tests/codegen/waiter-tests/build.gradle.kts @@ -38,7 +38,7 @@ kotlin.sourceSets.getByName("main") { tasks.withType { dependsOn(tasks.generateSmithyProjections) - kotlinOptions { + compilerOptions { // generated code has warnings unfortunately allWarningsAsErrors = false }