Repository: smithy-lang/smithy
Pull request #2452 (2024-11-12 18:00:46)

Add transforms to remove deprecated shapes
### Description 
Adds two transforms to `smithy-model` and `directed codegen` that allow users to filter out deprecated shapes by version or date. 

These transforms allow a user of directed codegen to quickly implement the recommended [`relativeVersion`](https://smithy.io/2.0/guides/building-codegen/configuring-the-generator.html#relativeversion-client-and-type-codegen-only) and [`relativeDate`](https://smithy.io/2.0/guides/building-codegen/configuring-the-generator.html#relativedate-client-and-type-codegen-only) settings for client and type code generators.

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.


mtdowling: Can we get a smithy-build.json transform for this too?

Patch:
diff --git a/README.md b/README.md
index 007c0ca27..4e1c42985 100644
--- a/README.md
+++ b/README.md
@@ -83,5 +83,5 @@ This library is licensed under the Apache 2.0 License.
 [Smithy Gradle Plugin]: https://github.com/awslabs/smithy-gradle-plugin/
 [Smithy CLI]: https://smithy.io/2.0/guides/smithy-cli/index.html
 [`smithy-build.json`]: https://smithy.io/2.0/guides/building-models/build-config.html#using-smithy-build-json
-[building]: https://smithy.io/2.0/guides/building-models/index.html
+[building]: https://smithy.io/2.0/guides/smithy-build-json.html
 [awesome-smithy]: https://github.com/smithy-lang/awesome-smithy
diff --git a/docs/requirements.txt b/docs/requirements.txt
index e50794f67..54c82aab9 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -11,3 +11,6 @@ sphinx-tabs>=3.4.4
 
 sphinx_copybutton==0.5.0
 sphinx-autobuild>=2024.4.16
+
+# needs to be added as it is removed in python3.13
+standard-imghdr==3.13.0
diff --git a/docs/source-2.0/guides/building-codegen/using-the-semantic-model.rst b/docs/source-2.0/guides/building-codegen/using-the-semantic-model.rst
index 319bfb30b..90900870c 100644
--- a/docs/source-2.0/guides/building-codegen/using-the-semantic-model.rst
+++ b/docs/source-2.0/guides/building-codegen/using-the-semantic-model.rst
@@ -284,7 +284,7 @@ the model and might rely on protocol-specific information.
     HttpBindingIndex index = HttpBindingIndex.of(model);
 
     String defaultPayloadType = "application/json";
-    String eventStreamType = "application/vnd.amazon.event-stream");
+    String eventStreamType = "application/vnd.amazon.eventstream");
     String contentType = index
         .determineRequestContentType(operation, defaultPayloadType, eventStreamType)
         .orElseNull();
@@ -302,7 +302,7 @@ the model and might rely on protocol-specific information.
    HttpBindingIndex index = HttpBindingIndex.of(model);
 
    String defaultPayloadType = "application/json";
-   String eventStreamType = "application/vnd.amazon.event-stream");
+   String eventStreamType = "application/vnd.amazon.eventstream");
    String contentType = index
        .determineResponseContentType(operation, defaultPayloadType, eventStreamType)
        .orElseNull();
diff --git a/docs/source-2.0/guides/smithy-build-json.rst b/docs/source-2.0/guides/smithy-build-json.rst
index ebdcf35a4..cba39798b 100644
--- a/docs/source-2.0/guides/smithy-build-json.rst
+++ b/docs/source-2.0/guides/smithy-build-json.rst
@@ -1650,6 +1650,61 @@ but keeps the shape if it has any of the provided tags:
         }
     }
 
+.. _removeDeprecatedShapes-transform:
+
+removeDeprecatedShapes
+-----------------------
+
+Removes any shapes that were marked as ``@deprecated`` before the specified version or date.
+
+This transform can be used to filter out shapes with the :ref:`deprecated trait <deprecated-trait>` applied if the
+``since`` property of the trait specifies a version or date. Versions are expected to follow the
+`SemVer Specification`_ and dates are expected to be `ISO 8601`_ calendar dates (YYYY-MM-DD).
+If the value of the ``since`` property is not a valid SemVer version or ISO 8601 calendar date then the
+``@deprecated`` trait will be ignored by this transform.
+
+If the version or date in the ``since`` property of a ``@deprecated`` trait is before the configured version or date, then
+the shape the ``@deprecated`` trait is applied to will be removed from the model. Shapes with the ``@deprecated`` trait,
+but no ``since`` property will be ignored by this transform.
+
+.. list-table::
+    :header-rows: 1
+    :widths: 10 20 70
+
+    * - Property
+      - Type
+      - Description
+    * - relativeDate
+      - ``string``
+      - Date, in `ISO 8601`_ calendar date format (YYYY-MM-DD), to use to filter out deprecated shapes.
+        Any shapes deprecated before this date will be removed from the model.
+    * - relativeVersion
+      - ``string``
+      - Version, following the `SemVer Specification`_, to use to filter out deprecated shapes.
+        Any shapes deprecated in an earlier version will be removed from the model.
+
+The following example removes any deprecated shapes that were deprecated before ``2024-10-10``
+or before version ``1.1.0``.
+
+.. code-block:: json
+
+    {
+        "version": "1.0",
+        "projections": {
+            "exampleProjection": {
+                "transforms": [
+                    {
+                        "name": "removeDeprecatedShapes",
+                        "args": {
+                            "relativeDate": "2024-10-10",
+                            "relativeVersion": "1.1.0"
+                        }
+                    }
+                ]
+            }
+        }
+    }
+
 .. _renameShapes-transform:
 
 renameShapes
@@ -1964,3 +2019,5 @@ Assuming ``hello.sh`` is on the PATH and might look something like:
 .. _Apache Maven: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html
 .. _Maven Central: https://search.maven.org
 .. _official Maven documentation: https://maven.apache.org/pom.html#dependency-version-requirement-specification
+.. _SemVer Specification: https://semver.org/
+.. _ISO 8601: https://www.iso.org/iso-8601-date-and-time-format.html
diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/RemoveDeprecatedShapes.java b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/RemoveDeprecatedShapes.java
new file mode 100644
index 000000000..c0b08ab87
--- /dev/null
+++ b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/RemoveDeprecatedShapes.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.build.transforms;
+
+import software.amazon.smithy.build.TransformContext;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.transform.ModelTransformer;
+
+/**
+ * {@code removeDeprecatedShapes} removes shapes from a model if they have been deprecated before a version or date.
+ */
+public final class RemoveDeprecatedShapes extends ConfigurableProjectionTransformer<RemoveDeprecatedShapes.Config> {
+
+    /**
+     * {@code RemoveDeprecatedShapes} configuration settings.
+     */
+    public static final class Config {
+        private String relativeDate;
+        private String relativeVersion;
+
+        /**
+         * Gets the date used to filter deprecated shapes.
+         *
+         * @return The date used to filter deprecated shapes.
+         */
+        public String getRelativeDate() {
+            return relativeDate;
+        }
+
+        /**
+         * Sets the date used to filter deprecated shapes.
+         *
+         * @param relativeDate The date used to filter deprecated shapes.
+         */
+        public void setRelativeDate(String relativeDate) {
+            this.relativeDate = relativeDate;
+        }
+
+        /**
+         * Gets the version used to filter deprecated shapes.
+         *
+         * @return The version used to filter deprecated shapes.
+         */
+        public String getRelativeVersion() {
+            return relativeVersion;
+        }
+
+        /**
+         * Sets the version used to filter deprecated shapes.
+         *
+         * @param relativeVersion The version used to filter deprecated shapes.
+         */
+        public void setRelativeVersion(String relativeVersion) {
+            this.relativeVersion = relativeVersion;
+        }
+    }
+
+    @Override
+    public String getName() {
+        return "removeDeprecatedShapes";
+    }
+
+    @Override
+    public Class<Config> getConfigType() {
+        return Config.class;
+    }
+
+    @Override
+    protected Model transformWithConfig(TransformContext context, Config config) {
+        Model model = context.getModel();
+        ModelTransformer transformer = context.getTransformer();
+        model = transformer.filterDeprecatedRelativeDate(model, config.getRelativeDate());
+        model = transformer.filterDeprecatedRelativeVersion(model, config.getRelativeVersion());
+        return model;
+    }
+}
diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/directed/CodegenDirector.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/directed/CodegenDirector.java
index 795fc54fb..13aedaeb8 100644
--- a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/directed/CodegenDirector.java
+++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/directed/CodegenDirector.java
@@ -289,6 +289,32 @@ public final class CodegenDirector<
         });
     }
 
+    /**
+     * Removes any shapes deprecated before the specified date.
+     *
+     * @param relativeDate Relative date, in YYYY-MM-DD format, to use to filter out deprecated shapes.
+     * @see ModelTransformer#filterDeprecatedRelativeDate(Model, String)
+     */
+    public void removeShapesDeprecatedBeforeDate(String relativeDate) {
+        transforms.add((model, transformer) -> {
+            LOGGER.finest("Removing shapes deprecated before date: " + relativeDate);
+            return transformer.filterDeprecatedRelativeDate(model, relativeDate);
+        });
+    }
+
+    /**
+     * Removes any shapes deprecated before the specified version.
+     *
+     * @param relativeVersion Version, in SemVer format, to use to filter out deprecated shapes.
+     * @see ModelTransformer#filterDeprecatedRelativeVersion(Model, String)
+     */
+    public void removeShapesDeprecatedBeforeVersion(String relativeVersion) {
+        transforms.add((model, transformer) -> {
+            LOGGER.finest("Removing shapes deprecated before version: " + relativeVersion);
+            return transformer.filterDeprecatedRelativeVersion(model, relativeVersion);
+        });
+    }
+
     /**
      * Changes each compatible string shape with the enum trait to an enum shape.
      *
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeDate.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeDate.java
new file mode 100644
index 000000000..49a048183
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeDate.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.transform;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.traits.DeprecatedTrait;
+
+final class FilterDeprecatedRelativeDate {
+    private static final Logger LOGGER = Logger.getLogger(FilterDeprecatedRelativeDate.class.getName());
+    // YYYY-MM-DD calendar date with optional hyphens.
+    private static final Pattern ISO_8601_DATE_REGEX = Pattern.compile(
+            "^([0-9]{4})-?(1[0-2]|0[1-9])-?(3[01]|0[1-9]|[12][0-9])$"
+    );
+
+    private final String relativeDate;
+
+    FilterDeprecatedRelativeDate(String relativeDate) {
+        if (relativeDate != null && !isIso8601Date(relativeDate)) {
+            throw new IllegalArgumentException("Provided relativeDate: `"
+                    + relativeDate
+                    + "` does not match ISO8601 calendar date format (YYYY-MM-DD)."
+            );
+        }
+        this.relativeDate = relativeDate != null ? relativeDate.replace("-", "") : null;
+    }
+
+    Model transform(ModelTransformer transformer, Model model) {
+        // If there is no filter. Exit without traversing shapes
+        if (relativeDate == null) {
+            return model;
+        }
+
+        Set<Shape> shapesToRemove = new HashSet<>();
+        for (Shape shape : model.getShapesWithTrait(DeprecatedTrait.class)) {
+            Optional<String> sinceOptional = shape.expectTrait(DeprecatedTrait.class).getSince();
+            if (!sinceOptional.isPresent()) {
+                continue;
+            }
+            String since = sinceOptional.get();
+
+            if (isIso8601Date(since)) {
+                // Compare lexicographical ordering without hyphens.
+                if (relativeDate.compareTo(since.replace("-", "")) > 0) {
+                    LOGGER.fine("Filtering deprecated shape: `"
+                            + shape + "`"
+                            + ". Shape was deprecated as of: " + since
+                    );
+                    shapesToRemove.add(shape);
+                }
+            }
+        }
+
+        return transformer.removeShapes(model, shapesToRemove);
+    }
+
+    private static boolean isIso8601Date(String string) {
+        return ISO_8601_DATE_REGEX.matcher(string).matches();
+    }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeVersion.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeVersion.java
new file mode 100644
index 000000000..ed00b28aa
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeVersion.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.transform;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.traits.DeprecatedTrait;
+
+final class FilterDeprecatedRelativeVersion {
+    private static final Logger LOGGER = Logger.getLogger(FilterDeprecatedRelativeVersion.class.getName());
+
+    /**
+     * SemVer regex from semantic version spec.
+     * @see <a href="https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string">SemVer</a>
+     */
+    private static final Pattern SEMVER_REGEX = Pattern.compile(
+            "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)"
+            + "(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
+    );
+
+    private final String relativeVersion;
+
+    FilterDeprecatedRelativeVersion(String relativeVersion) {
+        if (relativeVersion != null && !isSemVer(relativeVersion)) {
+            throw new IllegalArgumentException("Provided relativeDate: `"
+                    + relativeVersion
+                    + "` is not a valid ."
+            );
+        }
+        this.relativeVersion = relativeVersion;
+    }
+
+    Model transform(ModelTransformer transformer, Model model) {
+        // If there are no filters. Exit without traversing shapes
+        if (relativeVersion == null) {
+            return model;
+        }
+
+        Set<Shape> shapesToRemove = new HashSet<>();
+        for (Shape shape : model.getShapesWithTrait(DeprecatedTrait.class)) {
+            Optional<String> sinceOptional = shape.expectTrait(DeprecatedTrait.class).getSince();
+
+            if (!sinceOptional.isPresent()) {
+                continue;
+            }
+
+            String since = sinceOptional.get();
+            // Remove any shapes that were deprecated before the specified version.
+            if (isSemVer(since)) {
+                if (compareSemVer(relativeVersion, since) > 0) {
+                    LOGGER.fine("Filtering deprecated shape: `"
+                            + shape + "`"
+                            + ". Shape was deprecated as of version: " + since
+                    );
+                    shapesToRemove.add(shape);
+                }
+            }
+        }
+
+        return transformer.removeShapes(model, shapesToRemove);
+    }
+
+    private static boolean isSemVer(String string) {
+        return SEMVER_REGEX.matcher(string).matches();
+    }
+
+    static int compareSemVer(String semVer1, String semVer2) {
+        String[] versionComponents1 = semVer1.split("\\.");
+        String[] versionComponents2 = semVer2.split("\\.");
+
+        int maxLength = Math.max(versionComponents1.length, versionComponents2.length);
+        for (int i = 0; i < maxLength; i++) {
+            // Treat all implicit components as 0's
+            String component1 = i >= versionComponents1.length ? "0" : versionComponents1[i];
+            String component2 = i >= versionComponents2.length ? "0" : versionComponents2[i];
+            int comparison = component1.compareTo(component2);
+            if (comparison != 0) {
+                return comparison;
+            }
+        }
+        return 0;
+    }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java
index 4c6160db4..ed2123608 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java
@@ -687,4 +687,37 @@ public final class ModelTransformer {
     public Model flattenPaginationInfoIntoOperations(Model model, ServiceShape forService) {
         return new FlattenPaginationInfo(forService).transform(this, model);
     }
+
+    /**
+     * Removes any shapes that were deprecated before the specified date.
+     *
+     * <p>The relative date used as a filter should match the ISO 8601 Calendar date format
+     * (i.e. YYYY-MM-DD with optional hyphens).
+     * <p><strong>Note:</strong> Shapes with {@code @deprecated} trait but no {@code since} property are not filtered.
+     *
+     * @param model Model to transform.
+     * @param relativeDate ISO 8601 calendar date to use to filter out deprecated shapes.
+     * @return Returns the transformed model.
+     *
+     * @see <a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601</a>
+     */
+    public Model filterDeprecatedRelativeDate(Model model, String relativeDate) {
+        return new FilterDeprecatedRelativeDate(relativeDate).transform(this, model);
+    }
+
+    /**
+     * Removes any shapes that were deprecated before the specified version.
+     *
+     * <p>The version used should be a valid Semantic Version (SemVer). For example, {@code 1.2.3.1}.
+     * <p><strong>Note:</strong> Shapes with {@code @deprecated} trait but no {@code since} property are not filtered.
+     *
+     * @param model Model to transform.
+     * @param relativeVersion Semantic Version to use to filter out deprecated shapes.
+     * @return Returns the transformed model.
+     *
+     * @see <a href="https://semver.org/">SemVer</a>
+     */
+    public Model filterDeprecatedRelativeVersion(Model model, String relativeVersion) {
+        return new FilterDeprecatedRelativeVersion(relativeVersion).transform(this, model);
+    }
 }


Test patch:
diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/transforms/RemoveDeprecatedShapesTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/RemoveDeprecatedShapesTest.java
new file mode 100644
index 000000000..4e9fa5d9f
--- /dev/null
+++ b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/RemoveDeprecatedShapesTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.build.transforms;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Paths;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.build.TransformContext;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.shapes.ShapeId;
+
+public class RemoveDeprecatedShapesTest {
+    @Test
+    public void removesAllDeprecatedShapes() throws Exception {
+        Model model = Model.assembler()
+                .addImport(Paths.get(getClass().getResource("remove-deprecated.smithy").toURI()))
+                .assemble()
+                .unwrap();
+        TransformContext context = TransformContext.builder()
+                .model(model)
+                .settings(Node.objectNode()
+                        .withMember("relativeDate", Node.from("2024-10-10"))
+                        .withMember("relativeVersion", Node.from("1.1.0")))
+                .build();
+        Model result = new RemoveDeprecatedShapes().transform(context);
+
+        // Deprecated by date removed
+        assertFalse(result.getShape(ShapeId.from("smithy.example#FilteredBeforeHyphens")).isPresent());
+        assertFalse(result.getShape(ShapeId.from("smithy.example#FilteredVersionBefore")).isPresent());
+
+        // Equal to the filter retained
+        assertTrue(result.getShape(ShapeId.from("smithy.example#NotFilteredDateEquals")).isPresent());
+        assertTrue(result.getShape(ShapeId.from("smithy.example#NotFilteredVersionEquals")).isPresent());
+
+        // After filter retained
+        assertTrue(result.getShape(ShapeId.from("smithy.example#NotFilteredDateAfter")).isPresent());
+        assertTrue(result.getShape(ShapeId.from("smithy.example#NotFilteredVersionAfter")).isPresent());
+    }
+}
diff --git a/smithy-build/src/test/resources/software/amazon/smithy/build/transforms/remove-deprecated.smithy b/smithy-build/src/test/resources/software/amazon/smithy/build/transforms/remove-deprecated.smithy
new file mode 100644
index 000000000..2e1d85bd2
--- /dev/null
+++ b/smithy-build/src/test/resources/software/amazon/smithy/build/transforms/remove-deprecated.smithy
@@ -0,0 +1,33 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-10-09")
+structure FilteredDateBefore {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+structure NotFilteredDateEquals {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+structure NotFilteredDateAfter {
+    field: String
+}
+
+@deprecated(message: "Should be filtered as it is deprecated before version", since: "1.0.0")
+structure FilteredVersionBefore {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+structure NotFilteredVersionEquals {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+structure NotFilteredVersionAfter {
+    field: String
+}
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeDateTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeDateTest.java
new file mode 100644
index 000000000..6547a4127
--- /dev/null
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeDateTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.transform;
+
+import java.util.List;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.shapes.ModelSerializer;
+import software.amazon.smithy.utils.ListUtils;
+
+public class FilterDeprecatedRelativeDateTest {
+
+    public static List<String> fileSource() {
+        return ListUtils.of("deprecated-shapes", "deprecated-members", "deprecated-traits");
+    }
+
+    @ParameterizedTest
+    @MethodSource("fileSource")
+    void compareTransform(String prefix) {
+        Model before = Model.assembler()
+                .addImport(FilterDeprecatedRelativeDate.class.getResource("deprecated-date/" + prefix + "-before.smithy"))
+                .assemble()
+                .unwrap();
+        Model actualResult = ModelTransformer.create().filterDeprecatedRelativeDate(before, "2024-10-10");
+        Model expectedResult = Model.assembler()
+                .addImport(FilterDeprecatedRelativeDate.class.getResource("deprecated-date/" + prefix + "-after.smithy"))
+                .assemble()
+                .unwrap();
+
+        Node actualNode = ModelSerializer.builder().build().serialize(actualResult);
+        Node expectedNode = ModelSerializer.builder().build().serialize(expectedResult);
+        Node.assertEquals(actualNode, expectedNode);
+    }
+}
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeVersionTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeVersionTest.java
new file mode 100644
index 000000000..e13b2d544
--- /dev/null
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeVersionTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.transform;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.shapes.ModelSerializer;
+import software.amazon.smithy.utils.ListUtils;
+
+public class FilterDeprecatedRelativeVersionTest {
+    static List<Arguments> semverSupplier() {
+        return ListUtils.of(
+                Arguments.of("1.0.0", "1.0.0", 0),
+                Arguments.of("1.0.0", "1.0.1", -1),
+                Arguments.of("1.1.0", "1.1.1", -1),
+                Arguments.of("1.1.0", "1.0.1", 1),
+                Arguments.of("1.1.1", "1.1.1.1", -1),
+                Arguments.of("1.0.0.1", "1.0.0", 1),
+                Arguments.of("1.0.0", "1.0", 0),
+                Arguments.of("20.20.0.1", "20.20.1.0", -1),
+                Arguments.of("20.20.1.0", "20.20.1.0-PATCH", -1)
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("semverSupplier")
+    void testSemverComparison(String semver1, String semver2, int expected) {
+        int result = FilterDeprecatedRelativeVersion.compareSemVer(semver1, semver2);
+        switch (expected) {
+            case 0:
+                assertEquals(result, 0);
+                break;
+            case -1:
+                assertTrue(result < 0);
+                break;
+            case 1:
+                assertTrue(result > 0);
+                break;
+            default:
+                throw new IllegalStateException("Unexpected value: " + expected);
+        }
+    }
+
+    public static List<String> fileSource() {
+        return ListUtils.of("deprecated-shapes", "deprecated-members", "deprecated-traits");
+    }
+
+    @ParameterizedTest
+    @MethodSource("fileSource")
+    void compareTransform(String prefix) {
+        Model before = Model.assembler()
+                .addImport(FilterDeprecatedRelativeDate.class.getResource("deprecated-version/" + prefix + "-before.smithy"))
+                .assemble()
+                .unwrap();
+        Model actualResult = ModelTransformer.create().filterDeprecatedRelativeVersion(before, "1.1.0");
+        Model expectedResult = Model.assembler()
+                .addImport(FilterDeprecatedRelativeDate.class.getResource("deprecated-version/" + prefix + "-after.smithy"))
+                .assemble()
+                .unwrap();
+
+        Node actualNode = ModelSerializer.builder().build().serialize(actualResult);
+        Node expectedNode = ModelSerializer.builder().build().serialize(expectedResult);
+        Node.assertEquals(actualNode, expectedNode);
+    }
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-members-after.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-members-after.smithy
new file mode 100644
index 000000000..ce2ccc52b
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-members-after.smithy
@@ -0,0 +1,11 @@
+$version: "2"
+
+namespace smithy.example
+
+structure MyStruct {
+    @deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+    notFilteredEquals: String
+
+    @deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+    notFilteredAfter: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-members-before.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-members-before.smithy
new file mode 100644
index 000000000..b0a8d0b3d
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-members-before.smithy
@@ -0,0 +1,20 @@
+$version: "2"
+
+namespace smithy.example
+
+structure MyStruct {
+    @deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-10-09")
+    filteredBeforeHyphens: String
+
+    @deprecated(message: "Should be filtered as it is deprecated before date", since: "20241009")
+    filteredBeforeNoHyphens: String
+
+    @deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-1009")
+    filteredBeforeMixedHyphens: String
+
+    @deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+    notFilteredEquals: String
+
+    @deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+    notFilteredAfter: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-shapes-after.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-shapes-after.smithy
new file mode 100644
index 000000000..7b376f706
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-shapes-after.smithy
@@ -0,0 +1,13 @@
+$version: "2"
+
+namespace smithy.example
+
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+structure NotFilteredEquals {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+structure NotFilteredAfter {
+    field: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-shapes-before.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-shapes-before.smithy
new file mode 100644
index 000000000..08e34edd9
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-shapes-before.smithy
@@ -0,0 +1,28 @@
+$version: "2"
+
+namespace smithy.example
+
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-10-09")
+structure FilteredBeforeHyphens {
+    field: String
+}
+
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "20241009")
+structure FilteredBeforeNoHyphens {
+    field: String
+}
+
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-1009")
+structure FilteredBeforeMixedHyphens {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+structure NotFilteredEquals {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+structure NotFilteredAfter {
+    field: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-traits-after.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-traits-after.smithy
new file mode 100644
index 000000000..69b7f1828
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-traits-after.smithy
@@ -0,0 +1,15 @@
+$version: "2"
+
+namespace smithy.example
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+structure NotFilteredEquals {}
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+structure NotFilteredAfter {}
+
+@NotFilteredEquals
+@NotFilteredAfter
+structure MyStruct {}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-traits-before.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-traits-before.smithy
new file mode 100644
index 000000000..dbae56ddb
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-traits-before.smithy
@@ -0,0 +1,30 @@
+$version: "2"
+
+namespace smithy.example
+
+@trait
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-10-09")
+structure FilteredBeforeHyphens {}
+
+@trait
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "20241009")
+structure FilteredBeforeNoHyphens {}
+
+@trait
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-1009")
+structure FilteredBeforeMixedHyphens {}
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+structure NotFilteredEquals {}
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+structure NotFilteredAfter {}
+
+@FilteredBeforeHyphens
+@FilteredBeforeNoHyphens
+@FilteredBeforeMixedHyphens
+@NotFilteredEquals
+@NotFilteredAfter
+structure MyStruct {}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-members-after.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-members-after.smithy
new file mode 100644
index 000000000..1733842b0
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-members-after.smithy
@@ -0,0 +1,11 @@
+$version: "2"
+
+namespace smithy.example
+
+structure MyStruct {
+    @deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+    notFilteredEquals: String
+
+    @deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+    notFilteredAfter: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-members-before.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-members-before.smithy
new file mode 100644
index 000000000..634ba688c
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-members-before.smithy
@@ -0,0 +1,14 @@
+$version: "2"
+
+namespace smithy.example
+
+structure MyStruct {
+    @deprecated(message: "Should be filtered as it is deprecated before version", since: "1.0.0")
+    filteredBefore: String
+
+    @deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+    notFilteredEquals: String
+
+    @deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+    notFilteredAfter: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-shapes-after.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-shapes-after.smithy
new file mode 100644
index 000000000..378e9911a
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-shapes-after.smithy
@@ -0,0 +1,13 @@
+$version: "2"
+
+namespace smithy.example
+
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+structure NotFilteredEquals {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+structure NotFilteredAfter {
+    field: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-shapes-before.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-shapes-before.smithy
new file mode 100644
index 000000000..c763069dd
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-shapes-before.smithy
@@ -0,0 +1,18 @@
+$version: "2"
+
+namespace smithy.example
+
+@deprecated(message: "Should be filtered as it is deprecated before version", since: "1.0.0")
+structure FilteredBefore {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+structure NotFilteredEquals {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+structure NotFilteredAfter {
+    field: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-traits-after.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-traits-after.smithy
new file mode 100644
index 000000000..528931434
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-traits-after.smithy
@@ -0,0 +1,15 @@
+$version: "2"
+
+namespace smithy.example
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+structure NotFilteredEquals {}
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+structure NotFilteredAfter {}
+
+@NotFilteredEquals
+@NotFilteredAfter
+structure MyStruct {}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-traits-before.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-traits-before.smithy
new file mode 100644
index 000000000..ecbad79cc
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-traits-before.smithy
@@ -0,0 +1,20 @@
+$version: "2"
+
+namespace smithy.example
+
+@trait
+@deprecated(message: "Should be filtered as it is deprecated before version", since: "1.0.0")
+structure FilteredBefore {}
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+structure NotFilteredEquals {}
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+structure NotFilteredAfter {}
+
+@FilteredBefore
+@NotFilteredEquals
+@NotFilteredAfter
+structure MyStruct {}