Skip to content

Commit

Permalink
Add transforms to remove deprecated shapes
Browse files Browse the repository at this point in the history
  • Loading branch information
hpmellema committed Nov 12, 2024
1 parent d8c83fa commit 624deb6
Show file tree
Hide file tree
Showing 18 changed files with 542 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,32 @@ public void createDedicatedInputsAndOutputs(String inputSuffix, String outputSuf
});
}

/**
* 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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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])$"
);

public 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;
}

public 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)) {
if (shape.hasTrait(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();
}
}
Original file line number Diff line number Diff line change
@@ -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-]+)*))?$"
);

public final String relativeVersion;

FilterDeprecatedRelativeVersion(String relativeVersion) {
if (relativeVersion != null && !isSemVer(relativeVersion)) {
throw new IllegalArgumentException("Provided relativeDate: `"
+ relativeVersion
+ "` is not a valid ."
);
}
this.relativeVersion = relativeVersion;
}

public 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);
}

public 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -677,4 +677,37 @@ public Model removeInvalidDefaults(Model model) {
public Model deconflictErrorsWithSharedStatusCode(Model model, ServiceShape forService) {
return new DeconflictErrorsWithSharedStatusCode(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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 624deb6

Please sign in to comment.