Repository: smithy-lang/smithy
Pull request #2528 (2025-02-18 10:07:53)

Expand allowed tagging arn member names
Allows for the resource name component, the arn name component, or both name components to be specified. Empty names, non-matching names, or names that have repeat components will fail.

Other minor interface cleanup is included.

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/tagging/TaggingShapeUtils.java b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/tagging/TaggingShapeUtils.java
index a5cd82acc..1740132be 100644
--- a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/tagging/TaggingShapeUtils.java
+++ b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/tagging/TaggingShapeUtils.java
@@ -18,6 +18,7 @@ import software.amazon.smithy.model.shapes.ResourceShape;
 import software.amazon.smithy.model.shapes.Shape;
 import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.shapes.StructureShape;
+import software.amazon.smithy.utils.StringUtils;
 
 /**
  * Logic for validating that a shape looks like a tag.
@@ -30,7 +31,7 @@ final class TaggingShapeUtils {
     private static final Pattern TAG_PROPERTY_REGEX = Pattern
             .compile("^[T|t]ag(s|[L|l]ist)$");
     private static final Pattern RESOURCE_ARN_REGEX = Pattern
-            .compile("^([R|r]esource)?([A|a]rn|ARN)$");
+            .compile("^([R|r]esource)?([A|a]rn|ARN)?$");
     private static final Pattern TAG_KEYS_REGEX = Pattern
             .compile("^[T|t]ag[K|k]eys$");
 
@@ -41,16 +42,6 @@ final class TaggingShapeUtils {
         return "[T|t]ags";
     }
 
-    // Recommended name is more limited than the accepted regular expression.
-    static String getDesiredArnName() {
-        return "[R|r]esourceArn";
-    }
-
-    // Recommended name is more limited than the accepted regular expression.
-    static String getDesiredTagKeysName() {
-        return "[T|t]agKeys";
-    }
-
     // Used to validate tag property name and tag member name.
     static boolean isTagDesiredName(String memberName) {
         return TAG_PROPERTY_REGEX.matcher(memberName).matches();
@@ -58,7 +49,14 @@ final class TaggingShapeUtils {
 
     // Used for checking if member name is good for resource ARN input.
     static boolean isArnMemberDesiredName(String memberName) {
-        return RESOURCE_ARN_REGEX.matcher(memberName).matches();
+        if (StringUtils.isEmpty(memberName)) {
+            return false;
+        }
+
+        return memberName
+                .replaceFirst("^[R|r]esource", "")
+                .replaceFirst("[A|a]rn|ARN$", "")
+                .isEmpty();
     }
 
     // Used for checking if member name is good for tag keys input for untag operation.
@@ -66,7 +64,7 @@ final class TaggingShapeUtils {
         return TAG_KEYS_REGEX.matcher(memberName).matches();
     }
 
-    static boolean hasResourceArnInput(Map<String, MemberShape> inputMembers, Model model) {
+    private static boolean hasResourceArnInput(Map<String, MemberShape> inputMembers, Model model) {
         for (Map.Entry<String, MemberShape> memberEntry : inputMembers.entrySet()) {
             if (isArnMemberDesiredName(memberEntry.getKey())
                     && model.expectShape(memberEntry.getValue().getTarget()).isStringShape()) {
@@ -89,7 +87,7 @@ final class TaggingShapeUtils {
         return verifyTagListShape(model, tagShape) || verifyTagMapShape(model, tagShape);
     }
 
-    static boolean verifyTagListShape(Model model, Shape tagShape) {
+    private static boolean verifyTagListShape(Model model, Shape tagShape) {
         if (tagShape.isListShape()) {
             ListShape listShape = tagShape.asListShape().get();
             Shape listTargetShape = model.expectShape(listShape.getMember().getTarget());
@@ -108,7 +106,7 @@ final class TaggingShapeUtils {
         return false;
     }
 
-    static boolean verifyTagMapShape(Model model, Shape tagShape) {
+    private static boolean verifyTagMapShape(Model model, Shape tagShape) {
         if (tagShape.isMapShape()) {
             MapShape mapShape = tagShape.asMapShape().get();
             Shape valueTargetShape = model.expectShape(mapShape.getValue().getTarget());
@@ -189,7 +187,7 @@ final class TaggingShapeUtils {
         return false;
     }
 
-    static boolean isTagPropertyInShape(
+    private static boolean isTagPropertyInShape(
             String tagPropertyName,
             Shape shape,
             PropertyBindingIndex propertyBindingIndex
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java
index 49ee19ddd..518b1fedf 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java
@@ -4,6 +4,7 @@
  */
 package software.amazon.smithy.traitcodegen.generators;
 
+import java.util.Collections;
 import java.util.Optional;
 import software.amazon.smithy.codegen.core.Symbol;
 import software.amazon.smithy.codegen.core.SymbolProvider;
@@ -136,12 +137,16 @@ final class GetterGenerator implements Runnable {
                     // If the member targets a collection shape and is optional then generate an unwrapped
                     // getter as a convenience method as well.
                     Shape target = model.expectShape(member.getTarget());
-                    if (target.isListShape() || target.isMapShape()) {
+                    boolean isListShape = target.isListShape();
+                    if (isListShape || target.isMapShape()) {
                         writer.openBlock("public $T get$UOrEmpty() {",
                                 "}",
                                 symbolProvider.toSymbol(member),
                                 symbolProvider.toMemberName(member),
-                                () -> writer.write("return $L;", symbolProvider.toMemberName(member)));
+                                () -> writer.write("return $1L == null ? $2T.$3L() : $1L;",
+                                        symbolProvider.toMemberName(member),
+                                        Collections.class,
+                                        isListShape ? "emptyList" : "emptyMap"));
                     }
                 } else {
                     writer.openBlock("public $T get$U() {",


Test patch:
diff --git a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/tagging/TaggingShapeUtilsTest.java b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/tagging/TaggingShapeUtilsTest.java
new file mode 100644
index 000000000..d73ddd1ab
--- /dev/null
+++ b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/tagging/TaggingShapeUtilsTest.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.aws.traits.tagging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+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.utils.ListUtils;
+
+public class TaggingShapeUtilsTest {
+    public static List<Arguments> arnMembers() {
+        return ListUtils.of(
+                Arguments.of("", false),
+                Arguments.of("foo", false),
+                Arguments.of("fooArn", false),
+                Arguments.of("resourceFoo", false),
+                Arguments.of("arnResource", false),
+                Arguments.of("resourceResourceArn", false),
+                Arguments.of("resourceArnARN", false),
+                Arguments.of("resourceArn", true),
+                Arguments.of("resource", true),
+                Arguments.of("Resource", true),
+                Arguments.of("arn", true),
+                Arguments.of("Arn", true),
+                Arguments.of("ARN", true),
+                Arguments.of("ResourceARN", true));
+    }
+
+    @ParameterizedTest
+    @MethodSource("arnMembers")
+    public void isArnMemberDesiredName(String arnMember, boolean expected) {
+        assertEquals(expected, TaggingShapeUtils.isArnMemberDesiredName(arnMember));
+    }
+}