Repository: smithy-lang/smithy
Pull request #2065 (2023-12-08 17:11:07)

Add builtin for AccountIdEndpointMode
*Description of changes:*
Adds a new AWS Builtin for AccoutnIdEndpointMode.

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/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e4f6029fc..96d1e5a61 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -36,7 +36,7 @@ jobs:
         if: matrix.java >= 17
         run: ./gradlew :smithy-cli:integ -Plog-tests --stacktrace
 
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         if: failure()
         with:
           name: java-${{ matrix.java }}-${{ matrix.os }}-test-report
@@ -49,7 +49,7 @@ jobs:
     - uses: actions/checkout@v4
 
     - name: Set up Python 3.x
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: '3.x'
 
@@ -59,7 +59,7 @@ jobs:
     - name: Build docs
       run: cd docs && make html
 
-    - uses: actions/upload-artifact@v3
+    - uses: actions/upload-artifact@v4
       with:
         name: built-docs
         path: docs/build/html
diff --git a/.github/workflows/codeguru-reviewer.yml b/.github/workflows/codeguru-reviewer.yml
index 5a2c81847..c1b8f456d 100644
--- a/.github/workflows/codeguru-reviewer.yml
+++ b/.github/workflows/codeguru-reviewer.yml
@@ -55,6 +55,6 @@ jobs:
 
     - name: Upload review result
       if: steps.iam-role.outcome == 'success' &&steps.guruscan.outcome == 'success'
-      uses: github/codeql-action/upload-sarif@v2
+      uses: github/codeql-action/upload-sarif@v3
       with:
         sarif_file: codeguru-results.sarif.json
diff --git a/.github/workflows/sdk-codegen-ci.yml b/.github/workflows/sdk-codegen-ci.yml
index 50cada842..d94ec9446 100644
--- a/.github/workflows/sdk-codegen-ci.yml
+++ b/.github/workflows/sdk-codegen-ci.yml
@@ -52,7 +52,7 @@ jobs:
           distribution: 'zulu'
 
       - name: Set up Go 1.19
-        uses: actions/setup-go@v4
+        uses: actions/setup-go@v5
         with:
           go-version: 1.19
 
diff --git a/.github/workflows/update-brew.yml b/.github/workflows/update-brew.yml
index 7f9f78cf9..9a86b65de 100644
--- a/.github/workflows/update-brew.yml
+++ b/.github/workflows/update-brew.yml
@@ -40,7 +40,7 @@ jobs:
           echo $hash_url
           curl -fJLO $hash_url.sha256
       - name: Save artifacts
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: brew-artifacts
           path: '*.sha256'
@@ -54,7 +54,7 @@ jobs:
     steps:
       - name: Get artifacts
         id: download
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: brew-artifacts
       - name: Checkout bottle repo
diff --git a/build.gradle b/build.gradle
index 2e7c14a3f..f246dd12b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -24,7 +24,8 @@ plugins {
     id "com.github.spotbugs" version "4.7.1"
     id "io.codearte.nexus-staging" version "0.30.0"
     id "me.champeau.jmh" version "0.7.1"
-    id 'com.github.johnrengelman.shadow' version "7.1.2"
+    id "com.github.johnrengelman.shadow" version "7.1.2"
+    id "org.jreleaser" version "1.9.0"
 }
 
 ext {
@@ -39,6 +40,14 @@ allprojects {
     version = libraryVersion
 }
 
+// Only using JReleaser based on a flag allows going back to the old
+// release process if necessary. We can remove this later.
+def useJreleaser = project.hasProperty("useJreleaser")
+
+// JReleaser publishes artifacts from a local staging repository, rather than maven local.
+// https://jreleaser.org/guide/latest/examples/maven/staging-artifacts.html#_gradle
+def stagingDirectory = rootProject.layout.buildDirectory.dir("staging")
+
 subprojects {
     apply plugin: "java-library"
 
@@ -127,14 +136,23 @@ subprojects {
 
     publishing {
         repositories {
-            mavenCentral {
-                url = uri("https://aws.oss.sonatype.org/service/local/staging/deploy/maven2/")
-                if (project.hasProperty("sonatypeUser")) {
-                    credentials {
-                        username = project.property("sonatypeUser")
-                        password = project.property("sonatypePassword")
-                    }
+            // JReleaser's `publish` task publishes to all repositories, so only configure one.
+            if (useJreleaser) {
+                maven {
+                    name = "localStaging"
+                    url = stagingDirectory
                 }
+            } else {
+               mavenCentral {
+                   name = "sonatypeNexus"
+                   url = uri("https://aws.oss.sonatype.org/service/local/staging/deploy/maven2/")
+                   if (project.hasProperty("sonatypeUser")) {
+                       credentials {
+                           username = project.property("sonatypeUser")
+                           password = project.property("sonatypePassword")
+                       }
+                   }
+               }
             }
         }
 
@@ -271,6 +289,53 @@ allprojects {
     }
 }
 
+if (useJreleaser) {
+    jreleaser {
+        dryrun = false
+
+        // Used for creating a tagged release, uploading files and generating changelog.
+        // In the future we can set this up to push release tags to GitHub, but for now it's
+        // set up to do nothing.
+        // https://jreleaser.org/guide/latest/reference/release/index.html
+        release {
+            generic {
+                enabled = true
+                skipRelease = true
+            }
+        }
+
+        // Used to announce a release to configured announcers.
+        // https://jreleaser.org/guide/latest/reference/announce/index.html
+        announce {
+            active = "NEVER"
+        }
+
+        // Signing configuration.
+        // https://jreleaser.org/guide/latest/reference/signing.html
+        signing {
+            active = "ALWAYS"
+            armored = true
+        }
+
+        // Configuration for deploying to Maven Central.
+        // https://jreleaser.org/guide/latest/examples/maven/maven-central.html#_gradle
+        deploy {
+            maven {
+                nexus2 {
+                    'maven-central' {
+                        active = "ALWAYS"
+                        url = "https://aws.oss.sonatype.org/service/local"
+                        snapshotUrl = "https://aws.oss.sonatype.org/content/repositories/snapshots"
+                        closeRepository = false
+                        releaseRepository = false
+                        stagingRepository(stagingDirectory.get().toString())
+                    }
+                }
+            }
+        }
+    }
+}
+
 /*
  * Sonatype Staging Finalization
  * ====================================================
@@ -280,7 +345,7 @@ allprojects {
  * validated. This configuration is for the root project because
  * it operates at the "group" level.
  */
-if (project.hasProperty("sonatypeUser") && project.hasProperty("sonatypePassword")) {
+if (!useJreleaser && project.hasProperty("sonatypeUser") && project.hasProperty("sonatypePassword")) {
     apply plugin: "io.codearte.nexus-staging"
     nexusStaging {
         packageGroup = "software.amazon"
diff --git a/docs/source-2.0/aws/rules-engine/built-ins.rst b/docs/source-2.0/aws/rules-engine/built-ins.rst
index c91a63203..cc8deefe3 100644
--- a/docs/source-2.0/aws/rules-engine/built-ins.rst
+++ b/docs/source-2.0/aws/rules-engine/built-ins.rst
@@ -78,6 +78,16 @@ Description
 Type
     ``string``
 
+.. _rules-engine-aws-built-ins-account-id-endpoint-mode:
+
+``AWS::Auth::AccountIdEndpointMode`` built-in
+=============================================
+
+Description
+    The AccountId Endpoint Mode.
+Type
+    ``string``
+
 .. _rules-engine-aws-built-ins-credential-scope:
 
 ``AWS::Auth::CredentialScope`` built-in
diff --git a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/AwsBuiltIns.java b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/AwsBuiltIns.java
index f5fb0c459..5a47c5bb1 100644
--- a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/AwsBuiltIns.java
+++ b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/AwsBuiltIns.java
@@ -64,6 +64,17 @@ public final class AwsBuiltIns {
                     .documentation("The AWS AccountId used for the request.")
                     .build();
 
+    /**
+     * Built-in parameter representing the AccountId Endpoint Mode.
+     */
+    public static final Parameter ACCOUNT_ID_ENDPOINT_MODE =
+            Parameter.builder()
+                    .name("AccountIdEndpointMode")
+                    .type(ParameterType.STRING)
+                    .builtIn("AWS::Auth::AccountIdEndpointMode")
+                    .documentation("The AccountId Endpoint Mode.")
+                    .build();
+
     /**
      * Built-in parameter representing the Credential Scope.
      */
diff --git a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/AwsRuleSetExtension.java b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/AwsRuleSetExtension.java
index fd151a5a7..7fceddb7e 100644
--- a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/AwsRuleSetExtension.java
+++ b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/AwsRuleSetExtension.java
@@ -23,6 +23,7 @@ public final class AwsRuleSetExtension implements EndpointRuleSetExtension {
                 AwsBuiltIns.FIPS,
                 AwsBuiltIns.REGION,
                 AwsBuiltIns.ACCOUNT_ID,
+                AwsBuiltIns.ACCOUNT_ID_ENDPOINT_MODE,
                 AwsBuiltIns.CREDENTIAL_SCOPE,
                 AwsBuiltIns.S3_ACCELERATE,
                 AwsBuiltIns.S3_DISABLE_MRAP,
diff --git a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/validators/RuleSetAwsBuiltInValidator.java b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/validators/RuleSetAwsBuiltInValidator.java
index df0375a01..b4131e749 100644
--- a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/validators/RuleSetAwsBuiltInValidator.java
+++ b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/validators/RuleSetAwsBuiltInValidator.java
@@ -27,6 +27,7 @@ import software.amazon.smithy.utils.SetUtils;
 public class RuleSetAwsBuiltInValidator extends AbstractValidator {
     private static final Set<String> ADDITIONAL_CONSIDERATION_BUILT_INS = SetUtils.of(
             AwsBuiltIns.ACCOUNT_ID.getBuiltIn().get(),
+            AwsBuiltIns.ACCOUNT_ID_ENDPOINT_MODE.getBuiltIn().get(),
             AwsBuiltIns.CREDENTIAL_SCOPE.getBuiltIn().get());
     private static final String ADDITIONAL_CONSIDERATION_MESSAGE = "The `%s` built-in used requires additional "
            + "consideration of the rules that use it.";
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/OperationIndex.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/OperationIndex.java
index 531c20765..0b0fc2acc 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/OperationIndex.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/OperationIndex.java
@@ -18,6 +18,7 @@ package software.amazon.smithy.model.knowledge;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -37,6 +38,7 @@ import software.amazon.smithy.model.traits.InputTrait;
 import software.amazon.smithy.model.traits.OutputTrait;
 import software.amazon.smithy.model.traits.UnitTypeTrait;
 import software.amazon.smithy.utils.ListUtils;
+import software.amazon.smithy.utils.SetUtils;
 
 /**
  * Index of operation IDs to their resolved input, output, and error
@@ -50,11 +52,20 @@ public final class OperationIndex implements KnowledgeIndex {
     private final Map<ShapeId, StructureShape> inputs = new HashMap<>();
     private final Map<ShapeId, StructureShape> outputs = new HashMap<>();
     private final Map<ShapeId, List<StructureShape>> errors = new HashMap<>();
+    private final Map<ShapeId, Set<OperationShape>> boundInputOperations = new HashMap<>();
+    private final Map<ShapeId, Set<OperationShape>> boundOutputOperations = new HashMap<>();
+    private final Map<ShapeId, Set<Shape>> boundErrorShapes = new HashMap<>();
 
     public OperationIndex(Model model) {
         for (OperationShape operation : model.getOperationShapes()) {
-            getStructure(model, operation.getInputShape()).ifPresent(shape -> inputs.put(operation.getId(), shape));
-            getStructure(model, operation.getOutputShape()).ifPresent(shape -> outputs.put(operation.getId(), shape));
+            getStructure(model, operation.getInputShape()).ifPresent(shape -> {
+                inputs.put(operation.getId(), shape);
+                boundInputOperations.computeIfAbsent(shape.getId(), id -> new HashSet<>()).add(operation);
+            });
+            getStructure(model, operation.getOutputShape()).ifPresent(shape -> {
+                outputs.put(operation.getId(), shape);
+                boundOutputOperations.computeIfAbsent(shape.getId(), id -> new HashSet<>()).add(operation);
+            });
             addErrorsFromShape(model, operation.getId(), operation.getErrors());
         }
 
@@ -65,8 +76,10 @@ public final class OperationIndex implements KnowledgeIndex {
 
     private void addErrorsFromShape(Model model, ShapeId source, List<ShapeId> errorShapeIds) {
         List<StructureShape> errorShapes = new ArrayList<>(errorShapeIds.size());
+        Shape sourceShape = model.expectShape(source);
         for (ShapeId target : errorShapeIds) {
             model.getShape(target).flatMap(Shape::asStructureShape).ifPresent(errorShapes::add);
+            boundErrorShapes.computeIfAbsent(target, id -> new HashSet<>()).add(sourceShape);
         }
         errors.put(source, errorShapes);
     }
@@ -158,6 +171,16 @@ public final class OperationIndex implements KnowledgeIndex {
         return false;
     }
 
+    /**
+     * Gets all the operations that bind the given shape as input.
+     *
+     * @param input The structure that may be used as input.
+     * @return Returns a set of operations that bind the given input shape.
+     */
+    public Set<OperationShape> getInputBindings(ToShapeId input) {
+        return SetUtils.copyOf(boundInputOperations.getOrDefault(input.toShapeId(), Collections.emptySet()));
+    }
+
     /**
      * Gets the optional output structure of an operation, and returns an
      * empty optional if the output targets {@code smithy.api#Unit}.
@@ -241,6 +264,16 @@ public final class OperationIndex implements KnowledgeIndex {
         return false;
     }
 
+    /**
+     * Gets all the operations that bind the given shape as output.
+     *
+     * @param output The structure that may be used as output.
+     * @return Returns a set of operations that bind the given output shape.
+     */
+    public Set<OperationShape> getOutputBindings(ToShapeId output) {
+        return SetUtils.copyOf(boundOutputOperations.getOrDefault(output.toShapeId(), Collections.emptySet()));
+    }
+
     /**
      * Gets the list of error structures defined on an operation.
      *
@@ -275,4 +308,14 @@ public final class OperationIndex implements KnowledgeIndex {
     private Optional<StructureShape> getStructure(Model model, ToShapeId id) {
         return model.getShape(id.toShapeId()).flatMap(Shape::asStructureShape);
     }
+
+    /**
+     * Gets all the operations and services that bind the given shape as an error.
+     *
+     * @param error The structure that may be used as an error.
+     * @return Returns a set of operations and services that bind the given error shape.
+     */
+    public Set<Shape> getErrorBindings(ToShapeId error) {
+        return SetUtils.copyOf(boundErrorShapes.getOrDefault(error.toShapeId(), Collections.emptySet()));
+    }
 }


Test patch:
diff --git a/smithy-aws-protocol-tests/model/restXml/document-maps.smithy b/smithy-aws-protocol-tests/model/restXml/document-maps.smithy
index 253c97aed..fc2541a03 100644
--- a/smithy-aws-protocol-tests/model/restXml/document-maps.smithy
+++ b/smithy-aws-protocol-tests/model/restXml/document-maps.smithy
@@ -72,7 +72,7 @@ apply XmlMaps @httpResponseTests([
         protocol: restXml,
         code: 200,
         body: """
-              <XmlMapsRequest>
+              <XmlMapsResponse>
                   <myMap>
                       <entry>
                           <key>foo</key>
@@ -87,7 +87,7 @@ apply XmlMaps @httpResponseTests([
                           </value>
                       </entry>
                   </myMap>
-              </XmlMapsRequest>
+              </XmlMapsResponse>
               """,
         bodyMediaType: "application/xml",
         headers: {
diff --git a/smithy-aws-protocol-tests/model/restXml/document-structs.smithy b/smithy-aws-protocol-tests/model/restXml/document-structs.smithy
index 8fa9e94ee..b06dcf8d0 100644
--- a/smithy-aws-protocol-tests/model/restXml/document-structs.smithy
+++ b/smithy-aws-protocol-tests/model/restXml/document-structs.smithy
@@ -577,9 +577,9 @@ apply XmlEmptyBlobs @httpResponseTests([
         protocol: restXml,
         code: 200,
         body: """
-              <XmlBlobsRequest>
+              <XmlEmptyBlobsResponse>
                   <data></data>
-              </XmlBlobsRequest>
+              </XmlEmptyBlobsResponse>
               """,
         bodyMediaType: "application/xml",
         headers: {
@@ -596,9 +596,9 @@ apply XmlEmptyBlobs @httpResponseTests([
         protocol: restXml,
         code: 200,
         body: """
-              <XmlBlobsResponse>
+              <XmlEmptyBlobsResponse>
                   <data/>
-              </XmlBlobsResponse>
+              </XmlEmptyBlobsResponse>
               """,
         bodyMediaType: "application/xml",
         headers: {
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/OperationIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/OperationIndexTest.java
index a77709070..435abfa32 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/OperationIndexTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/OperationIndexTest.java
@@ -23,15 +23,18 @@ import static org.hamcrest.Matchers.is;
 
 import java.util.Collections;
 import java.util.Optional;
+import java.util.Set;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.shapes.OperationShape;
 import software.amazon.smithy.model.shapes.ServiceShape;
 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.model.traits.UnitTypeTrait;
+import software.amazon.smithy.utils.SetUtils;
 
 public class OperationIndexTest {
 
@@ -40,7 +43,7 @@ public class OperationIndexTest {
     @BeforeAll
     public static void before() {
         model = Model.assembler()
-                .addImport(OperationIndexTest.class.getResource("operation-index-test.json"))
+                .addImport(OperationIndexTest.class.getResource("operation-index-test.smithy"))
                 .assemble()
                 .unwrap();
     }
@@ -103,6 +106,18 @@ public class OperationIndexTest {
         assertThat(opIndex.isInputStructure(output), is(false));
     }
 
+    @Test
+    public void getsInputBindings() {
+        OperationIndex index = OperationIndex.of(model);
+        Set<OperationShape> actual = index.getInputBindings(ShapeId.from("ns.foo#Input"));
+        Set<OperationShape> expected = SetUtils.of(
+                model.expectShape(ShapeId.from("ns.foo#B"), OperationShape.class),
+                model.expectShape(ShapeId.from("ns.foo#C"), OperationShape.class)
+        );
+        assertThat(actual, equalTo(expected));
+        assertThat(index.getInputBindings(ShapeId.from("ns.foo#Output")), empty());
+    }
+
     @Test
     public void determinesIfShapeIsUsedAsOutput() {
         OperationIndex opIndex = OperationIndex.of(model);
@@ -113,6 +128,18 @@ public class OperationIndexTest {
         assertThat(opIndex.isOutputStructure(input), is(false));
     }
 
+    @Test
+    public void getsOutputBindings() {
+        OperationIndex index = OperationIndex.of(model);
+        Set<OperationShape> actual = index.getOutputBindings(ShapeId.from("ns.foo#Output"));
+        Set<OperationShape> expected = SetUtils.of(
+                model.expectShape(ShapeId.from("ns.foo#B"), OperationShape.class),
+                model.expectShape(ShapeId.from("ns.foo#C"), OperationShape.class)
+        );
+        assertThat(actual, equalTo(expected));
+        assertThat(index.getOutputBindings(ShapeId.from("ns.foo#Input")), empty());
+    }
+
     @Test
     public void getsOperationErrorsAndInheritedErrors() {
         OperationIndex opIndex = OperationIndex.of(model);
@@ -130,4 +157,21 @@ public class OperationIndexTest {
         assertThat(opIndex.getErrors(b), containsInAnyOrder(error1, error2));
         assertThat(opIndex.getErrors(service, b), containsInAnyOrder(error1, error2, common1, common2));
     }
+
+    @Test
+    public void getsErrorBindings() {
+        OperationIndex index = OperationIndex.of(model);
+        Set<Shape> actual = index.getErrorBindings(ShapeId.from("ns.foo#CommonError1"));
+        Set<Shape> expected = SetUtils.of(
+                model.expectShape(ShapeId.from("ns.foo#MyService"), ServiceShape.class),
+                model.expectShape(ShapeId.from("ns.foo#C"), OperationShape.class)
+        );
+        assertThat(actual, equalTo(expected));
+
+        actual = index.getErrorBindings(ShapeId.from("ns.foo#Error1"));
+        expected = SetUtils.of(model.expectShape(ShapeId.from("ns.foo#B"), OperationShape.class));
+        assertThat(actual, equalTo(expected));
+
+        assertThat(index.getOutputBindings(ShapeId.from("ns.foo#UnusedError")), empty());
+    }
 }
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/operation-index-test.json b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/operation-index-test.json
deleted file mode 100644
index 77f427877..000000000
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/operation-index-test.json
+++ /dev/null
@@ -1,81 +0,0 @@
-{
-    "smithy": "2.0",
-    "shapes": {
-        "ns.foo#MyService": {
-            "type": "service",
-            "version": "2017-01-17",
-            "operations": [
-                {
-                    "target": "ns.foo#A"
-                },
-                {
-                    "target": "ns.foo#B"
-                }
-            ],
-            "errors": [
-                {
-                    "target": "ns.foo#CommonError1"
-                },
-                {
-                    "target": "ns.foo#CommonError2"
-                }
-            ]
-        },
-        "ns.foo#A": {
-            "type": "operation",
-            "traits": {
-                "smithy.api#readonly": {}
-            }
-        },
-        "ns.foo#B": {
-            "type": "operation",
-            "input": {
-                "target": "ns.foo#Input"
-            },
-            "output": {
-                "target": "ns.foo#Output"
-            },
-            "errors": [
-                {
-                    "target": "ns.foo#Error1"
-                },
-                {
-                    "target": "ns.foo#Error2"
-                }
-            ],
-            "traits": {
-                "smithy.api#readonly": {}
-            }
-        },
-        "ns.foo#Input": {
-            "type": "structure"
-        },
-        "ns.foo#Output": {
-            "type": "structure"
-        },
-        "ns.foo#Error1": {
-            "type": "structure",
-            "traits": {
-                "smithy.api#error": "client"
-            }
-        },
-        "ns.foo#Error2": {
-            "type": "structure",
-            "traits": {
-                "smithy.api#error": "server"
-            }
-        },
-        "ns.foo#CommonError1": {
-            "type": "structure",
-            "traits": {
-                "smithy.api#error": "server"
-            }
-        },
-        "ns.foo#CommonError2": {
-            "type": "structure",
-            "traits": {
-                "smithy.api#error": "server"
-            }
-        }
-    }
-}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/operation-index-test.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/operation-index-test.smithy
new file mode 100644
index 000000000..d633de21b
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/operation-index-test.smithy
@@ -0,0 +1,58 @@
+$version: "2.0"
+
+namespace ns.foo
+
+service MyService {
+    version: "2017-01-17"
+    operations: [
+        A
+        B
+    ]
+    errors: [
+        CommonError1
+        CommonError2
+    ]
+}
+
+@readonly
+operation A {
+    input: Unit
+    output: Unit
+}
+
+@readonly
+operation B {
+    input: Input
+    output: Output
+    errors: [
+        Error1
+        Error2
+    ]
+}
+
+operation C {
+    input: Input
+    output: Output,
+    errors: [
+        CommonError1
+    ]
+}
+
+@error("server")
+structure CommonError1 {}
+
+@error("server")
+structure CommonError2 {}
+
+@error("client")
+structure Error1 {}
+
+@error("server")
+structure Error2 {}
+
+@error("client")
+structure UnusedError {}
+
+structure Input {}
+
+structure Output {}