Repository: smithy-lang/smithy
Pull request #2466 (2024-11-15 18:53:39)

Add transform to mark required idempotency tokens client optional
### Description
Adds a transform that marks required Idempotency tokens `@clientOptional`. This is important because it changes the nullability of idempotency tokens for code generators, allow customers to leave the field unset and have their client inject the value.

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.


hpmellema: > Looks good, but I have a question for my knowledge: Has this been causing issues for customers?

This is not a concern for existing customers but opens up the ability to easily inject these tokens in newer code-generated clients.

Patch:
diff --git a/docs/source-2.0/guides/smithy-build-json.rst b/docs/source-2.0/guides/smithy-build-json.rst
index cba39798b..16dbb5c92 100644
--- a/docs/source-2.0/guides/smithy-build-json.rst
+++ b/docs/source-2.0/guides/smithy-build-json.rst
@@ -573,6 +573,31 @@ applied.
     use an :ref:`enum shape <enum>` instead to avoid needing to use this
     transform.
 
+.. _makeIdempotencyTokensClientOptional:
+
+makeIdempotencyTokensClientOptional
+-----------------------------------
+
+Makes required :ref:`@idempotencyToken <idempotencyToken-trait>` members :ref:`@clientOptional <clientOptional-trait>`.
+
+Idempotency tokens that are required should fail validation, but shouldn't be required to create a type.
+This allows a default value to get injected when missing.
+
+.. code-block:: json
+
+    {
+        "version": "1.0",
+        "projections": {
+            "exampleProjection": {
+                "transforms": [
+                    {
+                        "name": "makeIdempotencyTokensClientOptional"
+                    }
+                ]
+            }
+        }
+    }
+
 .. _changeTypes:
 
 changeTypes
diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/MakeIdempotencyTokensClientOptional.java b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/MakeIdempotencyTokensClientOptional.java
new file mode 100644
index 000000000..afab58be7
--- /dev/null
+++ b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/MakeIdempotencyTokensClientOptional.java
@@ -0,0 +1,27 @@
+/*
+ * 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.ProjectionTransformer;
+import software.amazon.smithy.build.TransformContext;
+import software.amazon.smithy.model.Model;
+
+/**
+ * {@code makeIdempotencyTokensClientOptional} makes {@code @idempotencyToken} fields {@code @clientOptional}.
+ */
+public final class MakeIdempotencyTokensClientOptional implements ProjectionTransformer {
+
+    @Override
+    public String getName() {
+        return "makeIdempotencyTokensClientOptional";
+    }
+
+    @Override
+    public Model transform(TransformContext context) {
+        Model model = context.getModel();
+        return context.getTransformer().makeIdempotencyTokensClientOptional(model);
+    }
+}
diff --git a/smithy-build/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer b/smithy-build/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer
index 9a251b195..560add083 100644
--- a/smithy-build/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer
+++ b/smithy-build/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer
@@ -18,6 +18,8 @@ software.amazon.smithy.build.transforms.IncludeShapesByTag
 software.amazon.smithy.build.transforms.IncludeTags
 software.amazon.smithy.build.transforms.IncludeTraits
 software.amazon.smithy.build.transforms.IncludeTraitsByTag
+software.amazon.smithy.build.transforms.MakeIdempotencyTokensClientOptional
+software.amazon.smithy.build.transforms.RemoveDeprecatedShapes
 software.amazon.smithy.build.transforms.RemoveTraitDefinitions
 software.amazon.smithy.build.transforms.RemoveUnusedShapes
 software.amazon.smithy.build.transforms.RenameShapes
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 13aedaeb8..b9e9e767b 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
@@ -315,6 +315,18 @@ public final class CodegenDirector<
         });
     }
 
+    /**
+     * Makes {@code idempotencyToken} fields {@code clientOptional}.
+     *
+     * @see ModelTransformer#makeIdempotencyTokensClientOptional(Model)
+     */
+    public void makeIdempotencyTokensClientOptional() {
+        transforms.add((model, transformer) -> {
+            LOGGER.finest("Making `@idempotencyToken` fields `@clientOptional`");
+            return transformer.makeIdempotencyTokensClientOptional(model);
+        });
+    }
+
     /**
      * 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/MakeIdempotencyTokenClientOptional.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/MakeIdempotencyTokenClientOptional.java
new file mode 100644
index 000000000..fb8d01896
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/MakeIdempotencyTokenClientOptional.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.transform;
+
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.traits.ClientOptionalTrait;
+import software.amazon.smithy.model.traits.IdempotencyTokenTrait;
+import software.amazon.smithy.model.traits.RequiredTrait;
+
+/**
+ * Makes {@code idempotencyToken} members {@code clientOptional}, so they can be injected if missing.
+ */
+final class MakeIdempotencyTokenClientOptional {
+    private MakeIdempotencyTokenClientOptional() {}
+
+    public static Model transform(Model model) {
+        return ModelTransformer.create().mapShapes(model, shape -> {
+            if (shape.isMemberShape()
+                    && shape.hasTrait(RequiredTrait.class)
+                    && shape.hasTrait(IdempotencyTokenTrait.class)
+                    && !shape.hasTrait(ClientOptionalTrait.class)) {
+                return Shape.shapeToBuilder(shape).addTrait(new ClientOptionalTrait()).build();
+            }
+            return shape;
+        });
+    }
+}
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 ed2123608..0c671fd5b 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
@@ -720,4 +720,17 @@ public final class ModelTransformer {
     public Model filterDeprecatedRelativeVersion(Model model, String relativeVersion) {
         return new FilterDeprecatedRelativeVersion(relativeVersion).transform(this, model);
     }
+
+    /**
+     * Makes any {@code @idempotencyToken} fields {@code @clientOptional} so that missing tokens can be injected.
+     *
+     * <p>Idempotency tokens that are required should fail validation, but shouldn't be required to create a type,
+     * allowing for a default value to be injected when missing.
+     *
+     * @param model Model to transform.
+     * @return Returns the transformed model.
+     */
+    public Model makeIdempotencyTokensClientOptional(Model model) {
+        return MakeIdempotencyTokenClientOptional.transform(model);
+    }
 }


Test patch:
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/MakeIdempotencyTokenClientOptionalTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/MakeIdempotencyTokenClientOptionalTest.java
new file mode 100644
index 000000000..cf6b3e0ad
--- /dev/null
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/MakeIdempotencyTokenClientOptionalTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.traits.ClientOptionalTrait;
+import software.amazon.smithy.model.traits.IdempotencyTokenTrait;
+import software.amazon.smithy.model.traits.RequiredTrait;
+
+public class MakeIdempotencyTokenClientOptionalTest {
+    private static final ShapeId operationInput = ShapeId.from("smithy.example#IdempotencyTokenRequiredInput");
+
+    @Test
+    void compareTransform() {
+        Model before = Model.assembler()
+                .addImport(FlattenPaginationInfoTest.class.getResource("idempotency-token.smithy"))
+                .assemble()
+                .unwrap();
+        Model result = ModelTransformer.create().makeIdempotencyTokensClientOptional(before);
+
+        Shape input = result.expectShape(operationInput);
+        Shape member = result.expectShape(input.getMember("token").get().getId());
+
+        assertTrue(member.hasTrait(ClientOptionalTrait.class));
+        assertTrue(member.hasTrait(RequiredTrait.class));
+        assertTrue(member.hasTrait(IdempotencyTokenTrait.class));
+    }
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/idempotency-token.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/idempotency-token.smithy
new file mode 100644
index 000000000..b6975966c
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/idempotency-token.smithy
@@ -0,0 +1,11 @@
+$version: "2.0"
+
+namespace smithy.example
+
+operation IdempotencyTokenRequired {
+    input := {
+        @idempotencyToken
+        @required
+        token: String
+    }
+}