diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java index fea50a6e42d..74fadce5244 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java @@ -18,7 +18,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -136,7 +136,9 @@ private ProtocolGenerator resolveProtocolGenerator( TypeScriptSettings settings ) { // Collect all of the supported protocol generators. - Map generators = new HashMap<>(); + // Preserve insertion order as default priority order. + Map generators = new LinkedHashMap<>(); + for (TypeScriptIntegration integration : integrations) { for (ProtocolGenerator generator : integration.getProtocolGenerators()) { generators.put(generator.getProtocol(), generator); diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java index 0ed8bff53d7..c82cab8ba76 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java @@ -15,6 +15,7 @@ package software.amazon.smithy.typescript.codegen; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -36,6 +37,7 @@ import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.traits.RequiredTrait; +import software.amazon.smithy.typescript.codegen.protocols.ProtocolPriority; import software.amazon.smithy.utils.SmithyUnstableApi; /** @@ -450,8 +452,13 @@ public ShapeId resolveServiceProtocol(Model model, ServiceShape service, Set protocolPriority = ProtocolPriority.getProtocolPriority(service.toShapeId()); + List protocolPriorityList = protocolPriority != null && !protocolPriority.isEmpty() + ? protocolPriority + : new ArrayList<>(supportedProtocols); + + return protocolPriorityList.stream() + .filter(resolvedProtocols::contains) .findFirst() .orElseThrow(() -> new UnresolvableProtocolException(String.format( "The %s service supports the following unsupported protocols %s. The following protocol " diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/protocols/ProtocolPriority.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/protocols/ProtocolPriority.java new file mode 100644 index 00000000000..89cdf55f815 --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/protocols/ProtocolPriority.java @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.typescript.codegen.protocols; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import software.amazon.smithy.model.shapes.ShapeId; + + +/** + * Allows customization of protocol selection for specific services or a global default ordering. + */ +public final class ProtocolPriority { + private static final Map> SERVICE_PROTOCOL_PRIORITY_CUSTOMIZATIONS = new HashMap<>(); + private static List customDefaultPriority = null; + + private ProtocolPriority() {} + + /** + * @param serviceShapeId - service scope. + * @param protocolPriorityOrder - priority order of protocols. + */ + public static void setProtocolPriority(ShapeId serviceShapeId, List protocolPriorityOrder) { + SERVICE_PROTOCOL_PRIORITY_CUSTOMIZATIONS.put(serviceShapeId, protocolPriorityOrder); + } + + /** + * @param defaultProtocolPriorityOrder - use for all services that don't have a more specific priority order. + */ + public static void setCustomDefaultProtocolPriority(List defaultProtocolPriorityOrder) { + customDefaultPriority = new ArrayList<>(defaultProtocolPriorityOrder); + } + + /** + * @param serviceShapeId - service scope. + * @return priority order of protocols or null if no override exists. + */ + public static List getProtocolPriority(ShapeId serviceShapeId) { + return SERVICE_PROTOCOL_PRIORITY_CUSTOMIZATIONS.getOrDefault( + serviceShapeId, + customDefaultPriority != null ? new ArrayList<>(customDefaultPriority) : null + ); + } + + /** + * @param serviceShapeId - to unset. + */ + public static void deleteProtocolPriority(ShapeId serviceShapeId) { + SERVICE_PROTOCOL_PRIORITY_CUSTOMIZATIONS.remove(serviceShapeId); + } + + /** + * Unset the custom default priority order. + */ + public static void deleteCustomDefaultProtocolPriority() { + customDefaultPriority = null; + } +} diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/TypeScriptSettingsTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/TypeScriptSettingsTest.java index 6373bafca4b..fa34c26862d 100644 --- a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/TypeScriptSettingsTest.java +++ b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/TypeScriptSettingsTest.java @@ -1,20 +1,31 @@ package software.amazon.smithy.typescript.codegen; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.ServiceIndex; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.ShapeId; - -import java.util.stream.Stream; +import software.amazon.smithy.typescript.codegen.protocols.ProtocolPriority; +import software.amazon.smithy.utils.MapUtils; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +@ExtendWith(MockitoExtension.class) public class TypeScriptSettingsTest { @Test @@ -87,6 +98,121 @@ private static Stream providePackageDescriptionTestCases() { ); } + @Test + public void resolveServiceProtocol(@Mock Model model, + @Mock ServiceShape service, + @Mock ServiceIndex serviceIndex) { + TypeScriptSettings subject = new TypeScriptSettings(); + + // note: these are mock protocol names. + ShapeId rpcv2Cbor = ShapeId.from("namespace#rpcv2Cbor"); + ShapeId json1_0 = ShapeId.from("namespace#json1_0"); + ShapeId json1_1 = ShapeId.from("namespace#json1_1"); + ShapeId restJson1 = ShapeId.from("namespace#restJson1"); + ShapeId restXml = ShapeId.from("namespace#restXml"); + ShapeId query = ShapeId.from("namespace#query"); + ShapeId serviceQuery = ShapeId.from("namespace#serviceQuery"); + + when(model.getKnowledge(any(), any())).thenReturn(serviceIndex); + ShapeId serviceShapeId = ShapeId.from("namespace#Service"); + when(service.toShapeId()).thenReturn(serviceShapeId); + + LinkedHashSet protocolShapeIds = new LinkedHashSet<>( + List.of( + json1_0, json1_1, restJson1, rpcv2Cbor, restXml, query, serviceQuery + ) + ); + + { + // spec case 1. + when(serviceIndex.getProtocols(service)).thenReturn(MapUtils.of( + rpcv2Cbor, null, + json1_0, null + )); + ShapeId protocol = subject.resolveServiceProtocol(model, service, protocolShapeIds); + // Note: JS customization JSON higher default priority than CBOR. + assertEquals(json1_0, protocol); + } + + { + // spec case 2. + when(serviceIndex.getProtocols(service)).thenReturn(MapUtils.of( + rpcv2Cbor, null + )); + ShapeId protocol = subject.resolveServiceProtocol(model, service, protocolShapeIds); + assertEquals(rpcv2Cbor, protocol); + } + + { + // spec case 3. + when(serviceIndex.getProtocols(service)).thenReturn(MapUtils.of( + rpcv2Cbor, null, + json1_0, null, + query, null + )); + ShapeId protocol = subject.resolveServiceProtocol(model, service, protocolShapeIds); + // Note: JS customization JSON higher default priority than CBOR. + assertEquals(json1_0, protocol); + } + + { + // spec case 4. + when(serviceIndex.getProtocols(service)).thenReturn(MapUtils.of( + json1_0, null, + query, null + )); + ShapeId protocol = subject.resolveServiceProtocol(model, service, protocolShapeIds); + assertEquals(json1_0, protocol); + } + + { + // spec case 5. + when(serviceIndex.getProtocols(service)).thenReturn(MapUtils.of( + query, null + )); + ShapeId protocol = subject.resolveServiceProtocol(model, service, protocolShapeIds); + assertEquals(query, protocol); + } + + { + // service override, non-spec + when(serviceIndex.getProtocols(service)).thenReturn(MapUtils.of( + json1_0, null, + json1_1, null, + restJson1, null, + rpcv2Cbor, null, + restXml, null, + query, null, + serviceQuery, null + )); + ProtocolPriority.setProtocolPriority(serviceShapeId, List.of( + serviceQuery, rpcv2Cbor, json1_1, restJson1, restXml, query + )); + ShapeId protocol = subject.resolveServiceProtocol(model, service, protocolShapeIds); + ProtocolPriority.deleteProtocolPriority(serviceShapeId); + assertEquals(serviceQuery, protocol); + } + + { + // global default override + when(serviceIndex.getProtocols(service)).thenReturn(MapUtils.of( + json1_0, null, + json1_1, null, + restJson1, null, + rpcv2Cbor, null, + restXml, null, + query, null, + serviceQuery, null + )); + ProtocolPriority.setCustomDefaultProtocolPriority(List.of( + rpcv2Cbor, json1_1, restJson1, restXml, query + )); + ShapeId protocol = subject.resolveServiceProtocol(model, service, protocolShapeIds); + ProtocolPriority.deleteCustomDefaultProtocolPriority(); + assertEquals(rpcv2Cbor, protocol); + } + } + @Test public void resolvesSupportProtocols() { // TODO