From 4c0a8f922f7c65896ccad6a863e5cde249284b80 Mon Sep 17 00:00:00 2001 From: yibole Date: Wed, 19 Aug 2026 13:30:48 -0700 Subject: [PATCH 1/3] Read endpoint rule-set and tests from Smithy traits --- .../codegen/maven/plugin/GenerationMojo.java | 2 - .../codegen/smithy/AddSmithyEndpoints.java | 71 +++++++++++ .../SmithyIntermediateModelBuilder.java | 4 +- .../awssdk/codegen/smithy/SmithyModels.java | 33 +---- .../smithy/AddSmithyEndpointsTest.java | 118 ++++++++++++++++++ .../SmithyIntermediateModelBuilderTest.java | 106 ++++++++++++++++ 6 files changed, 299 insertions(+), 35 deletions(-) create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpoints.java create mode 100644 codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpointsTest.java create mode 100644 codegen/src/test/java/software/amazon/awssdk/codegen/smithy/SmithyIntermediateModelBuilderTest.java diff --git a/codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java b/codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java index b9a1608d9079..5aa8a61376eb 100644 --- a/codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java +++ b/codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java @@ -180,8 +180,6 @@ private GenerationParams smithyGenerationParams(ModelRoot r) { SmithyModels smithyModels = SmithyModels.builder() .model(model) .customizationConfig(r.customizationConfig) - .endpointRuleSetModel(loadEndpointRuleSetModel(modelRootPath)) - .endpointTestSuiteModel(loadEndpointTestSuiteModel(modelRootPath)) .build(); IntermediateModel intermediateModel = new SmithyIntermediateModelBuilder(smithyModels).build(); return new GenerationParams().withIntermediateModel(intermediateModel) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpoints.java b/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpoints.java new file mode 100644 index 000000000000..53c554db8ffc --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpoints.java @@ -0,0 +1,71 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.smithy; + +import java.io.IOException; +import software.amazon.awssdk.codegen.internal.Jackson; +import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel; +import software.amazon.awssdk.codegen.model.service.EndpointRuleSetModel; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait; +import software.amazon.smithy.rulesengine.traits.EndpointTestsTrait; + +/** + * Reads the endpoint rule-set and endpoint tests from the service's Smithy traits, where C2J reads + * them from the {@code endpoint-rule-set.json} and {@code endpoint-tests.json} sidecar files. + * + *

The trait content already uses the schema these POJOs expect, so this is a parse rather than a + * translation. It goes through the same {@link Jackson} mapper the sidecar path uses, so the two + * sources produce the same concrete types and tolerate unknown keys the same way. + * + *

The {@code endpointBdd} trait encodes the same rules as a binary decision diagram. Every + * service also carries {@code endpointRuleSet} in tree form, so it is ignored. + */ +final class AddSmithyEndpoints { + + private AddSmithyEndpoints() { + } + + /** + * Returns null when the service has no rule-set trait, letting the caller fall back to the + * sidecar file. + */ + static EndpointRuleSetModel endpointRuleSet(ServiceShape service) { + return service.getTrait(EndpointRuleSetTrait.class) + .map(trait -> parse(EndpointRuleSetModel.class, trait.getRuleSet())) + .orElse(null); + } + + /** + * Returns null when the service has no endpoint-tests trait, letting the caller fall back to + * the sidecar file. Uses {@code toNode()} rather than {@code getTestCases()} because the POJO + * expects the whole {@code {version, testCases}} object. + */ + static EndpointTestSuiteModel endpointTests(ServiceShape service) { + return service.getTrait(EndpointTestsTrait.class) + .map(trait -> parse(EndpointTestSuiteModel.class, trait.toNode())) + .orElse(null); + } + + private static T parse(Class clazz, Node node) { + try { + return Jackson.load(clazz, Node.printJson(node)); + } catch (IOException e) { + throw new RuntimeException("Failed to read " + clazz.getSimpleName() + " from a Smithy trait", e); + } + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/SmithyIntermediateModelBuilder.java b/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/SmithyIntermediateModelBuilder.java index e005fb948968..19960805dae9 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/SmithyIntermediateModelBuilder.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/SmithyIntermediateModelBuilder.java @@ -83,8 +83,8 @@ public SmithyIntermediateModelBuilder(SmithyModels models) { this.typeUtils = new TypeUtils(namingStrategy); this.serviceIndex = ServiceIndex.of(model); this.protocol = ProtocolUtils.resolveProtocol(serviceIndex, service); - this.endpointRuleSet = models.endpointRuleSetModel(); - this.endpointTestSuiteModel = models.endpointTestSuiteModel(); + this.endpointRuleSet = AddSmithyEndpoints.endpointRuleSet(service); + this.endpointTestSuiteModel = AddSmithyEndpoints.endpointTests(service); this.shapeProcessors = createShapeProcessors(); } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/SmithyModels.java b/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/SmithyModels.java index ead0508e8572..076bec36b705 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/SmithyModels.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/SmithyModels.java @@ -16,8 +16,6 @@ package software.amazon.awssdk.codegen.smithy; import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; -import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel; -import software.amazon.awssdk.codegen.model.service.EndpointRuleSetModel; import software.amazon.awssdk.utils.builder.SdkBuilder; import software.amazon.smithy.model.Model; @@ -31,17 +29,10 @@ public final class SmithyModels { private final Model model; private final CustomizationConfig customizationConfig; - private final EndpointRuleSetModel endpointRuleSetModel; - private final EndpointTestSuiteModel endpointTestSuiteModel; - private SmithyModels(Model model, - CustomizationConfig customizationConfig, - EndpointRuleSetModel endpointRuleSetModel, - EndpointTestSuiteModel endpointTestSuiteModel) { + private SmithyModels(Model model, CustomizationConfig customizationConfig) { this.model = model; this.customizationConfig = customizationConfig; - this.endpointRuleSetModel = endpointRuleSetModel; - this.endpointTestSuiteModel = endpointTestSuiteModel; } public static Builder builder() { @@ -56,20 +47,10 @@ public CustomizationConfig customizationConfig() { return customizationConfig; } - public EndpointRuleSetModel endpointRuleSetModel() { - return endpointRuleSetModel; - } - - public EndpointTestSuiteModel endpointTestSuiteModel() { - return endpointTestSuiteModel; - } - public static final class Builder implements SdkBuilder { private Model model; private CustomizationConfig customizationConfig; - private EndpointRuleSetModel endpointRuleSetModel; - private EndpointTestSuiteModel endpointTestSuiteModel; private Builder() { } @@ -84,20 +65,10 @@ public Builder customizationConfig(CustomizationConfig customizationConfig) { return this; } - public Builder endpointRuleSetModel(EndpointRuleSetModel endpointRuleSetModel) { - this.endpointRuleSetModel = endpointRuleSetModel; - return this; - } - - public Builder endpointTestSuiteModel(EndpointTestSuiteModel endpointTestSuiteModel) { - this.endpointTestSuiteModel = endpointTestSuiteModel; - return this; - } - @Override public SmithyModels build() { CustomizationConfig config = customizationConfig != null ? customizationConfig : CustomizationConfig.create(); - return new SmithyModels(model, config, endpointRuleSetModel, endpointTestSuiteModel); + return new SmithyModels(model, config); } } } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpointsTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpointsTest.java new file mode 100644 index 000000000000..061b49173dd2 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpointsTest.java @@ -0,0 +1,118 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.smithy; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel; +import software.amazon.awssdk.codegen.model.service.EndpointRuleSetModel; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; + +/** + * Unit tests for {@link AddSmithyEndpoints}, covering trait present, trait absent, and the + * lower-case parameter {@code type} the traits use where the sidecar files capitalise it. + */ +class AddSmithyEndpointsTest { + + private static final String RULE_SET = + "@smithy.rules#endpointRuleSet({\n" + + " version: \"1.0\"\n" + + " parameters: {\n" + + " Region: { builtIn: \"AWS::Region\", required: false, documentation: \"The region\", type: \"string\" }\n" + + " UseFIPS: { builtIn: \"AWS::UseFIPS\", required: true, default: false, type: \"boolean\" }\n" + + " }\n" + + " rules: [\n" + + " { conditions: [], endpoint: { url: \"https://example.amazonaws.com\" }, type: \"endpoint\" }\n" + + " ]\n" + + "})\n"; + + private static final String TESTS = + "@smithy.rules#endpointTests({\n" + + " version: \"1.0\"\n" + + " testCases: [\n" + + " {\n" + + " documentation: \"basic\"\n" + + " params: { Region: \"us-east-1\", UseFIPS: false }\n" + + " expect: { endpoint: { url: \"https://example.amazonaws.com\" } }\n" + + " }\n" + + " ]\n" + + "})\n"; + + private static ServiceShape serviceOf(String serviceTraits) { + String src = + "$version: \"2.0\"\nnamespace demo\n\n" + + "use aws.api#service\n" + + "use aws.auth#sigv4\n" + + "use aws.protocols#restJson1\n" + + "@service(sdkId: \"Demo\", arnNamespace: \"demo\")\n" + + "@sigv4(name: \"demo\")\n" + + "@restJson1\n" + + serviceTraits + + "service DemoService { version: \"2024-01-01\", operations: [Op] }\n\n" + + "@http(method: \"POST\", uri: \"/op\")\n" + + "operation Op { input: OpRequest, output: OpResponse }\n" + + "structure OpRequest {}\n" + + "structure OpResponse {}\n"; + Model model = Model.assembler() + .discoverModels(Model.class.getClassLoader()) + .addUnparsedModel("test.smithy", src) + .assemble() + .unwrap(); + return model.getServiceShapes().iterator().next(); + } + + @Test + void ruleSetTraitPresent_isTranslated() { + EndpointRuleSetModel ruleSet = AddSmithyEndpoints.endpointRuleSet(serviceOf(RULE_SET)); + + assertThat(ruleSet).isNotNull(); + assertThat(ruleSet.getVersion()).isEqualTo("1.0"); + assertThat(ruleSet.getParameters()).containsOnlyKeys("Region", "UseFIPS"); + assertThat(ruleSet.getRules()).hasSize(1); + } + + @Test + void ruleSetTraitAbsent_isNull() { + assertThat(AddSmithyEndpoints.endpointRuleSet(serviceOf(""))).isNull(); + } + + @Test + void endpointTestsTraitPresent_isTranslated() { + EndpointTestSuiteModel tests = AddSmithyEndpoints.endpointTests(serviceOf(RULE_SET + TESTS)); + + assertThat(tests).isNotNull(); + assertThat(tests.getTestCases()).hasSize(1); + } + + @Test + void endpointTestsTraitAbsent_isNull() { + assertThat(AddSmithyEndpoints.endpointTests(serviceOf(""))).isNull(); + } + + /** + * The traits write {@code "string"} where the sidecar files write {@code "String"}. The value is + * carried through verbatim; every consumer lower-cases before switching on it. + */ + @Test + void parameterType_keepsTheTraitsLowerCaseForm() { + EndpointRuleSetModel ruleSet = AddSmithyEndpoints.endpointRuleSet(serviceOf(RULE_SET)); + + assertThat(ruleSet.getParameters().get("Region").getType()).isEqualTo("string"); + assertThat(ruleSet.getParameters().get("UseFIPS").getType()).isEqualTo("boolean"); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/SmithyIntermediateModelBuilderTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/SmithyIntermediateModelBuilderTest.java new file mode 100644 index 000000000000..bc9886f6640a --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/SmithyIntermediateModelBuilderTest.java @@ -0,0 +1,106 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.smithy; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.service.EndpointRuleSetModel; +import software.amazon.smithy.model.Model; + +/** + * Covers the endpoint models reaching the intermediate model from the service's traits. The Smithy + * path takes no sidecar input, so a service without the traits lands on the same defaults the C2J + * path uses when the sidecar files are absent. + */ +class SmithyIntermediateModelBuilderTest { + + private static final String RULE_SET = + "@smithy.rules#endpointRuleSet({\n" + + " version: \"1.0\"\n" + + " parameters: {\n" + + " Region: { builtIn: \"AWS::Region\", required: false, type: \"string\" }\n" + + " }\n" + + " rules: [\n" + + " { conditions: [], endpoint: { url: \"https://example.amazonaws.com\" }, type: \"endpoint\" }\n" + + " ]\n" + + "})\n"; + + private static final String TESTS = + "@smithy.rules#endpointTests({\n" + + " version: \"1.0\"\n" + + " testCases: [\n" + + " {\n" + + " documentation: \"from the trait\"\n" + + " params: { Region: \"us-east-1\" }\n" + + " expect: { endpoint: { url: \"https://example.amazonaws.com\" } }\n" + + " }\n" + + " ]\n" + + "})\n"; + + private static IntermediateModel build(String serviceTraits) { + String src = + "$version: \"2.0\"\nnamespace demo\n\n" + + "use aws.api#service\n" + + "use aws.auth#sigv4\n" + + "use aws.protocols#restJson1\n" + + "@service(sdkId: \"Demo\", arnNamespace: \"demo\")\n" + + "@sigv4(name: \"demo\")\n" + + "@restJson1\n" + + serviceTraits + + "service DemoService { version: \"2024-01-01\", operations: [Op] }\n\n" + + "@http(method: \"POST\", uri: \"/op\")\n" + + "operation Op { input: OpRequest, output: OpResponse }\n" + + "structure OpRequest {}\n" + + "structure OpResponse {}\n"; + Model model = Model.assembler() + .discoverModels(Model.class.getClassLoader()) + .addUnparsedModel("test.smithy", src) + .assemble() + .unwrap(); + return new SmithyIntermediateModelBuilder( + SmithyModels.builder() + .model(model) + .customizationConfig(CustomizationConfig.create()) + .build()).build(); + } + + @Test + void endpointTraitsPresent_reachTheIntermediateModel() { + IntermediateModel model = build(RULE_SET + TESTS); + + assertThat(model.getEndpointRuleSetModel().getVersion()).isEqualTo("1.0"); + assertThat(model.getEndpointRuleSetModel().getParameters()).containsOnlyKeys("Region"); + assertThat(model.getEndpointTestSuiteModel().getTestCases()).hasSize(1); + } + + /** + * Neither getter returns null. {@code IntermediateModel} substitutes an empty test suite, and + * {@code EndpointRuleSetModel.defaultRules(endpointPrefix)} for the rule-set, so a service + * without the traits degrades to the generic rules exactly as a C2J service with no sidecar does. + */ + @Test + void endpointTraitsAbsent_fallBackToTheDefaultRules() { + IntermediateModel model = build(""); + + assertThat(model.getEndpointRuleSetModel()) + .usingRecursiveComparison() + .isEqualTo(EndpointRuleSetModel.defaultRules(model.getMetadata().getEndpointPrefix())); + assertThat(model.getEndpointTestSuiteModel().getTestCases()).isEmpty(); + } +} From 653c0ad29cb9cc18fed7f2375f0bd159e910d78e Mon Sep 17 00:00:00 2001 From: yibole Date: Wed, 19 Aug 2026 13:31:19 -0700 Subject: [PATCH 2/3] fix test --- .../codegen/smithy/AddSmithyEndpointsTest.java | 15 ++++++++++++--- .../SmithyIntermediateModelBuilderTest.java | 11 +++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpointsTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpointsTest.java index 061b49173dd2..5665b4a681e7 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpointsTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpointsTest.java @@ -29,12 +29,21 @@ */ class AddSmithyEndpointsTest { + /** + * Rule-set parameters must be bound in the service model, and AWS built-ins like + * {@code AWS::Region} are registered by smithy-aws-endpoints, which codegen does not depend on. + * Binding through {@code clientContextParams} keeps the model self-contained. + */ private static final String RULE_SET = - "@smithy.rules#endpointRuleSet({\n" + "@smithy.rules#clientContextParams(\n" + + " Region: { type: \"string\", documentation: \"The region\" }\n" + + " UseFIPS: { type: \"boolean\", documentation: \"Use FIPS endpoints\" }\n" + + ")\n" + + "@smithy.rules#endpointRuleSet({\n" + " version: \"1.0\"\n" + " parameters: {\n" - + " Region: { builtIn: \"AWS::Region\", required: false, documentation: \"The region\", type: \"string\" }\n" - + " UseFIPS: { builtIn: \"AWS::UseFIPS\", required: true, default: false, type: \"boolean\" }\n" + + " Region: { required: false, documentation: \"The region\", type: \"string\" }\n" + + " UseFIPS: { required: true, default: false, type: \"boolean\" }\n" + " }\n" + " rules: [\n" + " { conditions: [], endpoint: { url: \"https://example.amazonaws.com\" }, type: \"endpoint\" }\n" diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/SmithyIntermediateModelBuilderTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/SmithyIntermediateModelBuilderTest.java index bc9886f6640a..8307ae52d4eb 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/SmithyIntermediateModelBuilderTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/SmithyIntermediateModelBuilderTest.java @@ -30,11 +30,18 @@ */ class SmithyIntermediateModelBuilderTest { + /** + * {@code clientContextParams} binds the rule-set parameter to the service model, which the + * rules-engine validator requires for any parameter that is not a registered built-in. + */ private static final String RULE_SET = - "@smithy.rules#endpointRuleSet({\n" + "@smithy.rules#clientContextParams(\n" + + " Region: { type: \"string\", documentation: \"The region\" }\n" + + ")\n" + + "@smithy.rules#endpointRuleSet({\n" + " version: \"1.0\"\n" + " parameters: {\n" - + " Region: { builtIn: \"AWS::Region\", required: false, type: \"string\" }\n" + + " Region: { required: false, type: \"string\" }\n" + " }\n" + " rules: [\n" + " { conditions: [], endpoint: { url: \"https://example.amazonaws.com\" }, type: \"endpoint\" }\n" From 0b9faf64be76b28ed851a5604c8785db4cb74c8d Mon Sep 17 00:00:00 2001 From: yibole Date: Fri, 21 Aug 2026 14:24:47 -0700 Subject: [PATCH 3/3] address feedback --- .../codegen/smithy/AddSmithyEndpoints.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpoints.java b/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpoints.java index 53c554db8ffc..54483a85f4b0 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpoints.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyEndpoints.java @@ -41,31 +41,32 @@ private AddSmithyEndpoints() { } /** - * Returns null when the service has no rule-set trait, letting the caller fall back to the - * sidecar file. + * Returns null when the service has no rule-set trait. {@code IntermediateModel} then + * substitutes the default rules. */ static EndpointRuleSetModel endpointRuleSet(ServiceShape service) { return service.getTrait(EndpointRuleSetTrait.class) - .map(trait -> parse(EndpointRuleSetModel.class, trait.getRuleSet())) + .map(trait -> parse(EndpointRuleSetModel.class, trait.getRuleSet(), service)) .orElse(null); } /** - * Returns null when the service has no endpoint-tests trait, letting the caller fall back to - * the sidecar file. Uses {@code toNode()} rather than {@code getTestCases()} because the POJO - * expects the whole {@code {version, testCases}} object. + * Returns null when the service has no endpoint-tests trait. {@code IntermediateModel} then + * substitutes an empty test suite. Uses {@code toNode()} rather than {@code getTestCases()} + * because the POJO expects the whole {@code {version, testCases}} object. */ static EndpointTestSuiteModel endpointTests(ServiceShape service) { return service.getTrait(EndpointTestsTrait.class) - .map(trait -> parse(EndpointTestSuiteModel.class, trait.toNode())) + .map(trait -> parse(EndpointTestSuiteModel.class, trait.toNode(), service)) .orElse(null); } - private static T parse(Class clazz, Node node) { + private static T parse(Class clazz, Node node, ServiceShape service) { try { return Jackson.load(clazz, Node.printJson(node)); } catch (IOException e) { - throw new RuntimeException("Failed to read " + clazz.getSimpleName() + " from a Smithy trait", e); + throw new RuntimeException("Failed to read " + clazz.getSimpleName() + " from a Smithy trait on " + + service.getId(), e); } } }