diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/NewAndLegacyEventStreamProcessor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/NewAndLegacyEventStreamProcessor.java index a0c935a6170a..ba4cea869d0e 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/NewAndLegacyEventStreamProcessor.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/NewAndLegacyEventStreamProcessor.java @@ -74,6 +74,21 @@ public void postprocess(IntermediateModel intermediateModel) { // no-op } + /** + * Applies the same check to event streams found in a Smithy model. + */ + static void validateEventStreams(String serviceId, Iterable eventStreamNames) { + Set approvedStreams = APPROVED_EVENT_STREAMS.get(serviceId); + if (approvedStreams == null) { + return; + } + for (String name : eventStreamNames) { + if (!approvedStreams.contains(name)) { + throw unknownStreamError(serviceId, name); + } + } + } + private static RuntimeException unknownStreamError(String serviceId, String evenstreamName) { String msg = String.format("Encountered a new eventstream for service %s: %s. This service contains " + "evenstreams that are code generated using an older style that requires a customization. Please " + diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/SmithySourceModelValidator.java b/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/SmithySourceModelValidator.java new file mode 100644 index 000000000000..0ffc4946e8e9 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/customization/processors/SmithySourceModelValidator.java @@ -0,0 +1,107 @@ +/* + * 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.customization.processors; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.codegen.validation.ModelInvalidException; +import software.amazon.awssdk.codegen.validation.ValidationEntry; +import software.amazon.awssdk.codegen.validation.ValidationErrorId; +import software.amazon.awssdk.codegen.validation.ValidationErrorSeverity; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.neighbor.Walker; +import software.amazon.smithy.model.shapes.MemberShape; +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.StringShape; +import software.amazon.smithy.model.traits.HttpPayloadTrait; +import software.amazon.smithy.model.traits.StreamingTrait; +import software.amazon.smithy.model.traits.TraitDefinition; + +/** + * The Smithy path's counterpart to the validation-only C2J preprocessors: {@link NewAndLegacyEventStreamProcessor}, + * {@link ExplicitStringPayloadQueryProtocolProcessor}, and {@link LowercaseShapeValidatorProcessor}. Checks the shapes + * the service uses, after smithy-build transforms, and fails with the same errors as C2J. + */ +@SdkInternalApi +public final class SmithySourceModelValidator { + + private SmithySourceModelValidator() { + } + + public static void validate(Model model, ServiceShape service, String protocol) { + Set serviceShapes = new Walker(model).walkShapes(service); + validateEventStreams(service, serviceShapes); + validateNoExplicitStringPayload(model, service, protocol); + validateShapeNames(serviceShapes); + } + + private static void validateEventStreams(ServiceShape service, Set serviceShapes) { + service.getTrait(ServiceTrait.class).ifPresent(serviceTrait -> { + List eventStreams = serviceShapes.stream() + .filter(s -> s.isUnionShape() && s.hasTrait(StreamingTrait.class)) + .map(s -> s.getId().getName()) + .sorted() + .collect(Collectors.toList()); + NewAndLegacyEventStreamProcessor.validateEventStreams(serviceTrait.getSdkId(), eventStreams); + }); + } + + private static void validateNoExplicitStringPayload(Model model, ServiceShape service, String protocol) { + if (!"ec2".equals(protocol) && !"query".equals(protocol)) { + return; + } + for (ShapeId operationId : service.getAllOperations()) { + OperationShape operation = model.expectShape(operationId, OperationShape.class); + if (hasExplicitStringPayload(model, operation.getInputShape()) + || hasExplicitStringPayload(model, operation.getOutputShape())) { + throw new RuntimeException("Operations with explicit String payloads are not supported for Query " + + "protocols. Unsupported operation: " + operationId.getName()); + } + } + } + + private static boolean hasExplicitStringPayload(Model model, ShapeId structureId) { + for (MemberShape member : model.expectShape(structureId).members()) { + if (member.hasTrait(HttpPayloadTrait.class) && model.expectShape(member.getTarget()) instanceof StringShape) { + return true; + } + } + return false; + } + + private static void validateShapeNames(Set serviceShapes) { + serviceShapes.stream() + .filter(s -> (s.isStructureShape() || s.isUnionShape()) && !s.hasTrait(TraitDefinition.class)) + .map(s -> s.getId().getName()) + .filter(name -> Character.isLowerCase(name.charAt(0))) + .sorted() + .findFirst() + .ifPresent(name -> { + String errorMsg = String.format("Shape name '%s' starts with a lowercase character. Shape names " + + "must start with an uppercase character. Please update the " + + "shape name in your service model", name); + throw ModelInvalidException.fromEntry( + ValidationEntry.create(ValidationErrorId.INVALID_IDENTIFIER_NAME, + ValidationErrorSeverity.DANGER, errorMsg)); + }); + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyInputShapes.java b/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyInputShapes.java index de8ff812e0cd..cd2a6c477ebe 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyInputShapes.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyInputShapes.java @@ -120,12 +120,16 @@ private ShapeMarshaller buildMarshaller(OperationShape op, HttpBindingIndex bind .withAction(op.toShapeId().getName()) .withProtocol(protocol); - // RPC protocols ignore any @http trait and always POST to "/". For smithy-rpc-v2-cbor the - // "/service/{id}/operation/{op}" URI is applied later by the deferred rpcv2Cbor processor. + // RPC protocols ignore any @http trait and always POST, to "/" except for smithy-rpc-v2-cbor, + // which addresses each operation by path. C2J sets that path in SmithyRpcV2CborProtocolProcessor. if (httpBindingsHonored() && op.hasTrait(HttpTrait.class)) { HttpTrait http = op.expectTrait(HttpTrait.class); marshaller.withVerb(http.getMethod()); marshaller.withRequestUri(http.getUri().toString()); + } else if (Protocol.SMITHY_RPC_V2_CBOR.getValue().equals(protocol)) { + marshaller.withVerb("POST"); + marshaller.withRequestUri(String.format("/service/%s/operation/%s", + getService().getId().getName(), op.toShapeId().getName())); } else { marshaller.withVerb("POST"); marshaller.withRequestUri("/"); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyMetadata.java b/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyMetadata.java index 82105501bc7f..565041e009e4 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyMetadata.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/smithy/AddSmithyMetadata.java @@ -26,7 +26,6 @@ import software.amazon.awssdk.codegen.model.intermediate.Protocol; import software.amazon.awssdk.codegen.model.service.AuthType; import software.amazon.awssdk.codegen.naming.NamingStrategy; -import software.amazon.awssdk.codegen.utils.ProtocolUtils; import software.amazon.awssdk.utils.Pair; import software.amazon.awssdk.utils.StringUtils; import software.amazon.smithy.aws.traits.ServiceTrait; @@ -66,14 +65,13 @@ static Metadata constructMetadata(Model model, ServiceShape service, ServiceIndex serviceIndex, NamingStrategy namingStrategy, - CustomizationConfig customizationConfig) { + CustomizationConfig customizationConfig, + String protocol) { Metadata metadata = new Metadata(); String serviceName = namingStrategy.getServiceName(); configurePackageName(metadata, namingStrategy, customizationConfig); - String protocol = ProtocolUtils.resolveProtocol(serviceIndex, service); - metadata.withApiVersion(service.getVersion()) .withAsyncClient(String.format(Constant.ASYNC_CLIENT_CLASS_NAME_PATTERN, serviceName)) .withAsyncInterface(String.format(Constant.ASYNC_CLIENT_INTERFACE_NAME_PATTERN, serviceName)) 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 9112905c09fd..bab7be06b131 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 @@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory; import software.amazon.awssdk.codegen.IntermediateModelShapeProcessor; import software.amazon.awssdk.codegen.customization.processors.SmithyIntermediateModelPostprocessor; +import software.amazon.awssdk.codegen.customization.processors.SmithySourceModelValidator; import software.amazon.awssdk.codegen.internal.Constant; import software.amazon.awssdk.codegen.internal.TypeUtils; import software.amazon.awssdk.codegen.internal.Utils; @@ -90,7 +91,7 @@ public SmithyIntermediateModelBuilder(SmithyModels models) { this.namingStrategy = new DefaultSmithyNamingStrategy(model, service, customConfig); this.typeUtils = new TypeUtils(namingStrategy); this.serviceIndex = ServiceIndex.of(model); - this.protocol = ProtocolUtils.resolveProtocol(serviceIndex, service); + this.protocol = ProtocolUtils.resolveProtocol(serviceIndex, service, customConfig); this.endpointRuleSet = AddSmithyEndpoints.endpointRuleSet(service); this.endpointTestSuiteModel = AddSmithyEndpoints.endpointTests(service); this.shapeProcessors = createShapeProcessors(); @@ -139,6 +140,8 @@ private List createShapeProcessors() { } public IntermediateModel build() { + SmithySourceModelValidator.validate(model, service, protocol); + Map operations = new TreeMap<>(new AddSmithyOperations(model, service, namingStrategy, customConfig, protocol) .constructOperations()); @@ -170,7 +173,7 @@ public IntermediateModel build() { Map clientContextParams = clientContextParams(); IntermediateModel fullModel = new IntermediateModel( - AddSmithyMetadata.constructMetadata(model, service, serviceIndex, namingStrategy, customConfig), + AddSmithyMetadata.constructMetadata(model, service, serviceIndex, namingStrategy, customConfig, protocol), operations, shapes, customConfig, endpointOperation, Collections.emptyMap(), namingStrategy, Collections.emptyMap(), endpointRuleSet, endpointTestSuiteModel, clientContextParams); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/utils/ProtocolUtils.java b/codegen/src/main/java/software/amazon/awssdk/codegen/utils/ProtocolUtils.java index 1a1a5e281076..be43187b12ec 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/utils/ProtocolUtils.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/utils/ProtocolUtils.java @@ -17,9 +17,12 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; +import software.amazon.awssdk.codegen.model.config.customization.MetadataConfig; import software.amazon.awssdk.codegen.model.service.ServiceMetadata; import software.amazon.smithy.model.knowledge.ServiceIndex; import software.amazon.smithy.model.shapes.ServiceShape; @@ -113,4 +116,23 @@ public static String resolveProtocol(ServiceIndex serviceIndex, ServiceShape ser "The SDK does not support any of the protocols applied to service " + service.getId() + ": " + protocolTraits.keySet()); } + + /** + * Resolves the protocol a Smithy service is generated with. A {@code customServiceMetadata.protocol} override in + * the customization config wins over the protocol traits, the same way it replaces the {@code protocols} list on + * the C2J path; Kinesis uses it to select the legacy {@code cbor} protocol, which has no Smithy trait. + * + * @throws IllegalArgumentException if the override, or every applied protocol trait, is unsupported. + */ + public static String resolveProtocol(ServiceIndex serviceIndex, ServiceShape service, + CustomizationConfig customizationConfig) { + MetadataConfig metadataConfig = customizationConfig.getCustomServiceMetadata(); + String override = metadataConfig == null ? null : metadataConfig.getProtocol(); + if (override == null) { + return resolveProtocol(serviceIndex, service); + } + ServiceMetadata overridden = new ServiceMetadata(); + overridden.setProtocols(Collections.singletonList(override)); + return resolveProtocol(overridden); + } } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyInputShapesTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyInputShapesTest.java index 30cbe4bc2c2d..b665633ef179 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyInputShapesTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyInputShapesTest.java @@ -137,7 +137,7 @@ void restXml_bodyRequest_honorsXmlNameOverrideForRootElement() { @Test void rpcProtocol_ignoresHttpBindings_postToRootWithBodyMembers() { // An RPC service may still carry @http/@httpLabel; the bindings must be ignored, so every - // member goes in the body. The rpcv2Cbor URI is applied later by the deferred processor. + // member goes in the body and the request URI is the rpcv2Cbor operation path. Model model = modelOf( "use smithy.protocols#rpcv2Cbor\n", "@rpcv2Cbor\n", @@ -153,7 +153,7 @@ void rpcProtocol_ignoresHttpBindings_postToRootWithBodyMembers() { ShapeModel req = inputs(model, "smithy-rpc-v2-cbor").get("BodyOpRequest"); assertThat(req.getMarshaller().getVerb()).isEqualTo("POST"); - assertThat(req.getMarshaller().getRequestUri()).isEqualTo("/"); + assertThat(req.getMarshaller().getRequestUri()).isEqualTo("/service/DemoService/operation/BodyOp"); MemberModel id = req.getMembersAsMap().get("Id"); // @httpLabel ignored: no URI location, and the wire name is the member name, not a label. diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyMetadataTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyMetadataTest.java index 54933bfdcfd1..d545b1831383 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyMetadataTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/AddSmithyMetadataTest.java @@ -23,6 +23,7 @@ import software.amazon.awssdk.codegen.model.service.AuthType; import software.amazon.awssdk.codegen.naming.DefaultSmithyNamingStrategy; import software.amazon.awssdk.codegen.naming.NamingStrategy; +import software.amazon.awssdk.codegen.utils.ProtocolUtils; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.ServiceIndex; import software.amazon.smithy.model.shapes.ServiceShape; @@ -58,8 +59,9 @@ private static Model modelOf(String protocolUse, String serviceTraits, String ex private static Metadata metadataOf(Model model) { ServiceShape service = model.getServiceShapes().iterator().next(); NamingStrategy naming = new DefaultSmithyNamingStrategy(model, service, CustomizationConfig.create()); - return AddSmithyMetadata.constructMetadata(model, service, ServiceIndex.of(model), naming, - CustomizationConfig.create()); + ServiceIndex serviceIndex = ServiceIndex.of(model); + return AddSmithyMetadata.constructMetadata(model, service, serviceIndex, naming, CustomizationConfig.create(), + ProtocolUtils.resolveProtocol(serviceIndex, service)); } // ---- jsonVersion ------------------------------------------------------ diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/SmithySourceModelValidationTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/SmithySourceModelValidationTest.java new file mode 100644 index 000000000000..70145748619e --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/smithy/SmithySourceModelValidationTest.java @@ -0,0 +1,181 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; +import software.amazon.awssdk.codegen.model.config.customization.MetadataConfig; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.intermediate.Protocol; +import software.amazon.awssdk.codegen.validation.ModelInvalidException; +import software.amazon.smithy.model.Model; + +/** + * Covers the C2J preprocess behavior with no customization config that the Smithy builder reproduces: source-model + * validation and the configured protocol override. + */ +class SmithySourceModelValidationTest { + + @Test + void lowercaseStructureName_fails() { + assertThatThrownBy(() -> build(restJson("structure OpRequest { detail: detail }\nstructure detail {}\n"), + CustomizationConfig.create())) + .isInstanceOf(ModelInvalidException.class) + .hasMessageContaining("Shape name 'detail' starts with a lowercase character"); + } + + @Test + void uppercaseStructureNames_pass() { + IntermediateModel model = build(restJson("structure OpRequest { detail: Detail }\nstructure Detail {}\n"), + CustomizationConfig.create()); + + assertThat(model.getShapes()).containsKey("Detail"); + } + + @Test + void unapprovedEventStreamForLegacyService_fails() { + assertThatThrownBy(() -> build(eventStreamService("Kinesis", "NewEventStream"), CustomizationConfig.create())) + .hasMessageContaining("Encountered a new eventstream for service Kinesis: NewEventStream"); + } + + @Test + void approvedEventStreamForLegacyService_passes() { + IntermediateModel model = build(eventStreamService("Kinesis", "SubscribeToShardEventStream"), + CustomizationConfig.create()); + + assertThat(model.getShapes().get("SubscribeToShardEventStream").isEventStream()).isTrue(); + } + + @Test + void eventStreamForOtherService_passes() { + IntermediateModel model = build(eventStreamService("Demo", "NewEventStream"), CustomizationConfig.create()); + + assertThat(model.getShapes().get("NewEventStream").isEventStream()).isTrue(); + } + + @Test + void explicitStringPayloadOnQueryProtocol_fails() { + String smithy = + "$version: \"2.0\"\nnamespace demo\n\n" + + "use aws.api#service\nuse aws.auth#sigv4\nuse aws.protocols#awsQuery\n" + + "@service(sdkId: \"Demo\", arnNamespace: \"demo\")\n@sigv4(name: \"demo\")\n" + + "@awsQuery @xmlNamespace(uri: \"https://demo.amazonaws.com/doc/2024-01-01/\")\n" + + "service DemoService { version: \"2024-01-01\", operations: [Op] }\n" + + "operation Op { input: OpRequest, output: OpResponse }\n" + + "structure OpRequest { @httpPayload body: String }\n" + + "structure OpResponse {}\n"; + + assertThatThrownBy(() -> build(smithy, CustomizationConfig.create())) + .hasMessage("Operations with explicit String payloads are not supported for Query protocols. " + + "Unsupported operation: Op"); + } + + @Test + void explicitStringPayloadOnRestProtocol_passes() { + IntermediateModel model = build(restJson("structure OpRequest { @httpPayload body: String }\n"), + CustomizationConfig.create()); + + assertThat(model.getOperation("Op")).isNotNull(); + } + + @Test + void protocolOverride_selectsLegacyCborForTheWholeModel() { + IntermediateModel model = build(awsJson("Kinesis"), protocolOverride("cbor")); + + assertThat(model.getMetadata().getProtocol()).isEqualTo(Protocol.CBOR); + assertThat(model.getMetadata().getJsonVersion()).isEqualTo("1.1"); + assertThat(model.getOperation("Op").getServiceProtocol()).isEqualTo("cbor"); + assertThat(model.getShapes().get("OpRequest").getMarshaller().getTarget()).isEqualTo("DemoService.Op"); + } + + @Test + void protocolOverride_restJson_honorsHttpBindings() { + String smithy = awsJson("Demo").replace("operation Op {", + "@http(method: \"POST\", uri: \"/op/{Id}\")\noperation Op {") + .replace("structure OpRequest {}", + "structure OpRequest { @required @httpLabel Id: String }"); + + IntermediateModel model = build(smithy, protocolOverride("rest-json")); + + assertThat(model.getMetadata().getProtocol()).isEqualTo(Protocol.REST_JSON); + assertThat(model.getShapes().get("OpRequest").getMarshaller().getRequestUri()).isEqualTo("/op/{Id}"); + } + + @Test + void unsupportedProtocolOverride_fails() { + assertThatThrownBy(() -> build(awsJson("Demo"), protocolOverride("api-gateway"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The SDK does not support any of provided protocols: [api-gateway]"); + } + + private static CustomizationConfig protocolOverride(String protocol) { + MetadataConfig metadata = new MetadataConfig(); + metadata.setProtocol(protocol); + CustomizationConfig config = CustomizationConfig.create(); + config.setCustomServiceMetadata(metadata); + return config; + } + + private static String restJson(String requestShapes) { + return "$version: \"2.0\"\nnamespace demo\n\n" + + "use aws.api#service\nuse aws.auth#sigv4\nuse aws.protocols#restJson1\n" + + "@service(sdkId: \"Demo\", arnNamespace: \"demo\")\n@sigv4(name: \"demo\")\n@restJson1\n" + + "service DemoService { version: \"2024-01-01\", operations: [Op] }\n" + + "@http(method: \"POST\", uri: \"/op\")\n" + + "operation Op { input: OpRequest, output: OpResponse }\n" + + requestShapes + + "structure OpResponse {}\n"; + } + + private static String awsJson(String sdkId) { + return "$version: \"2.0\"\nnamespace demo\n\n" + + "use aws.api#service\nuse aws.auth#sigv4\nuse aws.protocols#awsJson1_1\n" + + "@service(sdkId: \"" + sdkId + "\", arnNamespace: \"demo\")\n@sigv4(name: \"demo\")\n@awsJson1_1\n" + + "service DemoService { version: \"2024-01-01\", operations: [Op] }\n" + + "operation Op { input: OpRequest, output: OpResponse }\n" + + "structure OpRequest {}\n" + + "structure OpResponse {}\n"; + } + + private static String eventStreamService(String sdkId, String streamName) { + return "$version: \"2.0\"\nnamespace demo\n\n" + + "use aws.api#service\nuse aws.auth#sigv4\nuse aws.protocols#restJson1\n" + + "@service(sdkId: \"" + sdkId + "\", arnNamespace: \"demo\")\n@sigv4(name: \"demo\")\n@restJson1\n" + + "service DemoService { version: \"2024-01-01\", operations: [Subscribe] }\n" + + "@http(method: \"POST\", uri: \"/subscribe\")\n" + + "operation Subscribe { input: SubscribeRequest, output: SubscribeResponse }\n" + + "structure SubscribeRequest {}\n" + + "structure SubscribeResponse { @httpPayload events: " + streamName + " }\n" + + "@streaming union " + streamName + " { Event: Event }\n" + + "structure Event { value: String }\n"; + } + + private static IntermediateModel build(String smithy, CustomizationConfig config) { + Model model = Model.assembler() + .discoverModels(Model.class.getClassLoader()) + .addUnparsedModel("test.smithy", smithy) + .assemble() + .unwrap(); + return new SmithyIntermediateModelBuilder(SmithyModels.builder() + .model(model) + .customizationConfig(config) + .build()) + .build(); + } +}