diff --git a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/model/AWSDeprecatedShapeRemover.kt b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/model/AWSDeprecatedShapeRemover.kt new file mode 100644 index 00000000000..0776ad72f72 --- /dev/null +++ b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/model/AWSDeprecatedShapeRemover.kt @@ -0,0 +1,37 @@ +package software.amazon.smithy.aws.swift.codegen.model + +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.traits.DeprecatedTrait +import software.amazon.smithy.model.transform.ModelTransformer +import software.amazon.smithy.swift.codegen.SwiftSettings +import software.amazon.smithy.swift.codegen.integration.SwiftIntegration +import software.amazon.smithy.swift.codegen.model.getTrait +import java.time.DateTimeException +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.function.Predicate + +private val DEPRECATED_SINCE_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd") + +private val REMOVE_BEFORE_DATE = "2024-09-17" + +/** + * Parses a string of yyyy-MM-dd format to [LocalDate], returning `null` if parsing fails. + */ +internal fun String.toLocalDate(): LocalDate? = try { LocalDate.parse(this, DEPRECATED_SINCE_DATE_FORMATTER) } catch (e: DateTimeException) { null } + +class AWSDeprecatedShapeRemover : SwiftIntegration { + override fun preprocessModel(model: Model, settings: SwiftSettings): Model { + return ModelTransformer.create().removeShapesIf( + model, + Predicate { + val since = it.getTrait()?.since?.orElse(null) ?: return@Predicate false + val deprecatedDate = since.toLocalDate() ?: return@Predicate false.also { + println("Failed to parse `since` field $since as a date, skipping removal of deprecated shape $it") + } + return@Predicate deprecatedDate < REMOVE_BEFORE_DATE.toLocalDate() + } + ) + } +} diff --git a/codegen/smithy-aws-swift-codegen/src/main/resources/META-INF/services/software.amazon.smithy.swift.codegen.integration.SwiftIntegration b/codegen/smithy-aws-swift-codegen/src/main/resources/META-INF/services/software.amazon.smithy.swift.codegen.integration.SwiftIntegration index 7dc543aaea0..5be00c33f94 100644 --- a/codegen/smithy-aws-swift-codegen/src/main/resources/META-INF/services/software.amazon.smithy.swift.codegen.integration.SwiftIntegration +++ b/codegen/smithy-aws-swift-codegen/src/main/resources/META-INF/services/software.amazon.smithy.swift.codegen.integration.SwiftIntegration @@ -20,5 +20,6 @@ software.amazon.smithy.aws.swift.codegen.PresignerGenerator software.amazon.smithy.aws.swift.codegen.model.AWSClientContextParamsTransformer software.amazon.smithy.aws.swift.codegen.model.AWSHttpTraitTransformer software.amazon.smithy.aws.swift.codegen.model.AWSEndpointTraitTransformer +software.amazon.smithy.aws.swift.codegen.model.AWSDeprecatedShapeRemover software.amazon.smithy.aws.swift.codegen.AWSClientConfigurationIntegration software.amazon.smithy.swift.codegen.swiftintegrations.InitialRequestIntegration diff --git a/codegen/smithy-aws-swift-codegen/src/test/kotlin/software/amazon/smithy/aws/swift/codegen/model/AWSDeprecatedShapeRemoverTests.kt b/codegen/smithy-aws-swift-codegen/src/test/kotlin/software/amazon/smithy/aws/swift/codegen/model/AWSDeprecatedShapeRemoverTests.kt new file mode 100644 index 00000000000..97736108520 --- /dev/null +++ b/codegen/smithy-aws-swift-codegen/src/test/kotlin/software/amazon/smithy/aws/swift/codegen/model/AWSDeprecatedShapeRemoverTests.kt @@ -0,0 +1,65 @@ +package software.amazon.smithy.aws.swift.codegen.model + +import io.kotest.matchers.string.shouldContainOnlyOnce +import io.kotest.matchers.string.shouldNotContain +import org.junit.jupiter.api.Test +import software.amazon.smithy.aws.swift.codegen.TestContext +import software.amazon.smithy.aws.swift.codegen.TestUtils +import software.amazon.smithy.aws.swift.codegen.shouldSyntacticSanityCheck +import software.amazon.smithy.aws.traits.protocols.RestJson1Trait + +class AWSDeprecatedShapeRemoverTests { + @Test + fun `Shape deprecated before the cutoff date based on deprecated trait's since field gets removed`() { + val context = setupTests("deprecated-shape-removal-test.smithy", "com.test#Example") + val contents = TestUtils.getModelFileContents("Example", "OperationWithDeprecatedInputMembersInput.swift", context.manifest) + contents.shouldSyntacticSanityCheck() + val removedContent = """ + @available(*, deprecated, message: "API deprecated since 2024-09-01") + public var deprecatedMemberWithCorrectlyFormedSinceField: Swift.String? +""" + contents.shouldNotContain(removedContent) + } + + @Test + fun `Shape deprecated after the cutoff date remains unremoved`() { + val context = setupTests("deprecated-shape-removal-test.smithy", "com.test#Example") + val contents = TestUtils.getModelFileContents("Example", "OperationWithDeprecatedInputMembersInput.swift", context.manifest) + contents.shouldSyntacticSanityCheck() + val expectedContents = """ + @available(*, deprecated, message: "API deprecated since 2024-10-01") + public var deprecatedMemberWithCorrectlyFormedSinceFieldButDeprecatedAfterCutoff: Swift.String? +""" + contents.shouldContainOnlyOnce(expectedContents) + } + + @Test + fun `Shape with deprecated trait that has malformed since field remains unremoved`() { + val context = setupTests("deprecated-shape-removal-test.smithy", "com.test#Example") + val contents = TestUtils.getModelFileContents("Example", "OperationWithDeprecatedInputMembersInput.swift", context.manifest) + contents.shouldSyntacticSanityCheck() + val expectedContents = """ + @available(*, deprecated, message: "API deprecated since 4.2.0") + public var deprecatedMemberWithMalformedSinceField: Swift.String? +""" + contents.shouldContainOnlyOnce(expectedContents) + } + + @Test + fun `Shape with deprecated trait missing since field remains unremoved`() { + val context = setupTests("deprecated-shape-removal-test.smithy", "com.test#Example") + val contents = TestUtils.getModelFileContents("Example", "OperationWithDeprecatedInputMembersInput.swift", context.manifest) + contents.shouldSyntacticSanityCheck() + val expectedContents = """ + @available(*, deprecated) + public var deprecatedMemberWithoutSinceField: Swift.String? +""" + contents.shouldContainOnlyOnce(expectedContents) + } + + private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { + val context = TestUtils.executeDirectedCodegen(smithyFile, serviceShapeId, RestJson1Trait.ID) + context.ctx.delegator.flushWriters() + return context + } +} diff --git a/codegen/smithy-aws-swift-codegen/src/test/resources/software.amazon.smithy.aws.swift.codegen/deprecated-shape-removal-test.smithy b/codegen/smithy-aws-swift-codegen/src/test/resources/software.amazon.smithy.aws.swift.codegen/deprecated-shape-removal-test.smithy new file mode 100644 index 00000000000..cfed6f47337 --- /dev/null +++ b/codegen/smithy-aws-swift-codegen/src/test/resources/software.amazon.smithy.aws.swift.codegen/deprecated-shape-removal-test.smithy @@ -0,0 +1,35 @@ +$version: "2.0" +namespace com.test + +use aws.api#service +use aws.protocols#restJson1 +use aws.auth#sigv4 + +@service(sdkId: "Example") +@restJson1 +@sigv4(name: "Example") +service Example { + version: "1.0.0", + operations: [ + OperationWithDeprecatedInputMembers + ] +} + +@http(method: "POST", uri: "/foo") +operation OperationWithDeprecatedInputMembers { + input: InputWithDeprecatedMembers +} + +structure InputWithDeprecatedMembers { + @deprecated(since: "2024-09-01") + deprecatedMemberWithCorrectlyFormedSinceField: String + + @deprecated(since: "2024-10-01") + deprecatedMemberWithCorrectlyFormedSinceFieldButDeprecatedAfterCutoff: String + + @deprecated(since: "4.2.0") + deprecatedMemberWithMalformedSinceField: String + + @deprecated + deprecatedMemberWithoutSinceField: String +}