Skip to content

Commit 8b3d752

Browse files
authored
feat: add protocol resolution priority system (#1370)
* feat(cbor): add protocol resolution priority system * test: update test code comments * subordinate ProtocolPriorityConfig to TypeScriptSettings * use immutable config for protocol priority
1 parent 4c692ae commit 8b3d752

File tree

8 files changed

+376
-34
lines changed

8 files changed

+376
-34
lines changed

.changeset/young-eagles-wait.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

README.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -185,19 +185,21 @@ By default, the Smithy TypeScript code generators provide the code generation fr
185185
186186
[`TypeScriptSettings`](smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java) contains all of the settings enabled from `smithy-build.json` and helper methods and types. The up-to-date list of top-level properties enabled for `typescript-client-codegen` can be found in `TypeScriptSettings.ArtifactType.CLIENT`.
187187

188-
|Setting|Required|Description|
189-
|---|---|---|
190-
|`package`|Yes|Name of the package in `package.json`.|
191-
|`packageVersion`|Yes|Version of the package in `package.json`.|
192-
|`packageDescription`|No|Description of the package in `package.json`. The default value is `${package} client`|
193-
|`packageJson`|No|Custom `package.json` properties that will be merged with the base `package.json`. The default value is an empty object.|
194-
|`packageManager`|No|Configured package manager for the package. The default value is `yarn`.|
195-
|`service`|No|The Shape ID of the service to generate a client for. If not provided, the code generator will attempt to infer the service Shape ID. If there is exactly 1 service found in the model, then the service is used as the inferred Shape ID. If no services are found, then code generation fails. If more than 1 service is found, then code generation fails.|
196-
|`protocol`|No|The Shape ID of the protocol used to generate serialization and deserialization. If not provided, the code generator will attempt to resolve the highest priority service protocol supported in code generation (registered through `TypeScriptIntegration`). If no protocols are found, code generation will use serialization and deserialization error stubs.|
197-
|`private`|No|Whether the package is `private` in `package.json`. The default value is `false`.|
198-
|`requiredMemberMode`|No|**NOT RECOMMENDED DUE TO BACKWARD COMPATIBILITY CONCERNS.** Sets whether members marked with the `@required` trait are allowed to be `undefined`. See more details on the risks in `TypeScriptSettings.RequiredMemberMode`. The default value is `nullable`.|
199-
|`createDefaultReadme`|No|Whether to generate a default `README.md` for the package. The default value is `false`.|
200-
|`useLegacyAuth`|No|**NOT RECOMMENDED, AVAILABLE ONLY FOR BACKWARD COMPATIBILITY CONCERNS.** Flag that enables using legacy auth. When in doubt, use the default identity and auth behavior (not configuring `useLegacyAuth`) as the golden path.|
188+
| Setting |Required| Description |
189+
|---------------------------|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
190+
| `package` |Yes| Name of the package in `package.json`. |
191+
| `packageVersion` |Yes| Version of the package in `package.json`. |
192+
| `packageDescription` |No| Description of the package in `package.json`. The default value is `${package} client` |
193+
| `packageJson` |No| Custom `package.json` properties that will be merged with the base `package.json`. The default value is an empty object. |
194+
| `packageManager` |No| Configured package manager for the package. The default value is `yarn`. |
195+
| `service` |No| The Shape ID of the service to generate a client for. If not provided, the code generator will attempt to infer the service Shape ID. If there is exactly 1 service found in the model, then the service is used as the inferred Shape ID. If no services are found, then code generation fails. If more than 1 service is found, then code generation fails. |
196+
| `protocol` |No| The Shape ID of the protocol used to generate serialization and deserialization. If not provided, the code generator will attempt to resolve the highest priority service protocol supported in code generation (registered through `TypeScriptIntegration`). If no protocols are found, code generation will use serialization and deserialization error stubs. |
197+
| `private` |No| Whether the package is `private` in `package.json`. The default value is `false`. |
198+
| `requiredMemberMode` |No| **NOT RECOMMENDED DUE TO BACKWARD COMPATIBILITY CONCERNS.** Sets whether members marked with the `@required` trait are allowed to be `undefined`. See more details on the risks in `TypeScriptSettings.RequiredMemberMode`. The default value is `nullable`. |
199+
| `createDefaultReadme` |No| Whether to generate a default `README.md` for the package. The default value is `false`. |
200+
| `useLegacyAuth` |No| **NOT RECOMMENDED, AVAILABLE ONLY FOR BACKWARD COMPATIBILITY CONCERNS.** Flag that enables using legacy auth. When in doubt, use the default identity and auth behavior (not configuring `useLegacyAuth`) as the golden path. |
201+
| `serviceProtocolPriority` |No| Map of service `ShapeId` strings to lists of protocol `ShapeId` strings. Used to override protocol selection behavior. |
202+
| `defaultProtocolPriority` |No| List of protocol `ShapeId` strings. Lower precedence than `serviceProtocolPriority` but applies to all services. |
201203

202204
#### `typescript-client-codegen` plugin artifacts
203205

private/smithy-rpcv2-cbor/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,25 @@
2020
"@aws-crypto/sha256-js": "5.2.0",
2121
"@aws-sdk/types": "latest",
2222
"@smithy/config-resolver": "^3.0.5",
23-
"@smithy/core": "^2.3.2",
23+
"@smithy/core": "^2.4.0",
2424
"@smithy/fetch-http-handler": "^3.2.4",
2525
"@smithy/hash-node": "^3.0.3",
2626
"@smithy/invalid-dependency": "^3.0.3",
2727
"@smithy/middleware-content-length": "^3.0.5",
28-
"@smithy/middleware-retry": "^3.0.14",
28+
"@smithy/middleware-retry": "^3.0.15",
2929
"@smithy/middleware-serde": "^3.0.3",
3030
"@smithy/middleware-stack": "^3.0.3",
3131
"@smithy/node-config-provider": "^3.1.4",
3232
"@smithy/node-http-handler": "^3.1.4",
3333
"@smithy/protocol-http": "^4.1.0",
34-
"@smithy/smithy-client": "^3.1.12",
34+
"@smithy/smithy-client": "^3.2.0",
3535
"@smithy/types": "^3.3.0",
3636
"@smithy/url-parser": "^3.0.3",
3737
"@smithy/util-base64": "^3.0.0",
3838
"@smithy/util-body-length-browser": "^3.0.0",
3939
"@smithy/util-body-length-node": "^3.0.0",
40-
"@smithy/util-defaults-mode-browser": "^3.0.14",
41-
"@smithy/util-defaults-mode-node": "^3.0.14",
40+
"@smithy/util-defaults-mode-browser": "^3.0.15",
41+
"@smithy/util-defaults-mode-node": "^3.0.15",
4242
"@smithy/util-middleware": "^3.0.3",
4343
"@smithy/util-retry": "^3.0.3",
4444
"@smithy/util-utf8": "^3.0.0",

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/DirectedTypeScriptCodegen.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import java.nio.file.Paths;
1919
import java.util.ArrayList;
2020
import java.util.Collection;
21-
import java.util.HashMap;
21+
import java.util.LinkedHashMap;
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Set;
@@ -136,7 +136,9 @@ private ProtocolGenerator resolveProtocolGenerator(
136136
TypeScriptSettings settings
137137
) {
138138
// Collect all of the supported protocol generators.
139-
Map<ShapeId, ProtocolGenerator> generators = new HashMap<>();
139+
// Preserve insertion order as default priority order.
140+
Map<ShapeId, ProtocolGenerator> generators = new LinkedHashMap<>();
141+
140142
for (TypeScriptIntegration integration : integrations) {
141143
for (ProtocolGenerator generator : integration.getProtocolGenerators()) {
142144
generators.put(generator.getProtocol(), generator);

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515

1616
package software.amazon.smithy.typescript.codegen;
1717

18+
import java.util.ArrayList;
1819
import java.util.Arrays;
1920
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.LinkedList;
2023
import java.util.List;
24+
import java.util.Map;
2125
import java.util.Objects;
26+
import java.util.Optional;
2227
import java.util.Set;
2328
import java.util.function.BiFunction;
2429
import java.util.logging.Logger;
@@ -27,6 +32,7 @@
2732
import software.amazon.smithy.codegen.core.SymbolProvider;
2833
import software.amazon.smithy.model.Model;
2934
import software.amazon.smithy.model.knowledge.ServiceIndex;
35+
import software.amazon.smithy.model.node.ArrayNode;
3036
import software.amazon.smithy.model.node.BooleanNode;
3137
import software.amazon.smithy.model.node.Node;
3238
import software.amazon.smithy.model.node.ObjectNode;
@@ -36,6 +42,7 @@
3642
import software.amazon.smithy.model.shapes.ShapeId;
3743
import software.amazon.smithy.model.traits.DefaultTrait;
3844
import software.amazon.smithy.model.traits.RequiredTrait;
45+
import software.amazon.smithy.typescript.codegen.protocols.ProtocolPriorityConfig;
3946
import software.amazon.smithy.utils.SmithyUnstableApi;
4047

4148
/**
@@ -59,6 +66,8 @@ public final class TypeScriptSettings {
5966
private static final String CREATE_DEFAULT_README = "createDefaultReadme";
6067
private static final String USE_LEGACY_AUTH = "useLegacyAuth";
6168
private static final String GENERATE_TYPEDOC = "generateTypeDoc";
69+
private static final String SERVICE_PROTOCOL_PRIORITY = "serviceProtocolPriority";
70+
private static final String DEFAULT_PROTOCOL_PRIORITY = "defaultProtocolPriority";
6271

6372
private String packageName;
6473
private String packageDescription = "";
@@ -77,6 +86,7 @@ public final class TypeScriptSettings {
7786
private boolean createDefaultReadme = false;
7887
private boolean useLegacyAuth = false;
7988
private boolean generateTypeDoc = false;
89+
private ProtocolPriorityConfig protocolPriorityConfig = new ProtocolPriorityConfig(null, null);
8090

8191
@Deprecated
8292
public static TypeScriptSettings from(Model model, ObjectNode config) {
@@ -128,6 +138,9 @@ public static TypeScriptSettings from(Model model, ObjectNode config, ArtifactTy
128138
.orElse(RequiredMemberMode.NULLABLE));
129139

130140
settings.setPluginSettings(config);
141+
142+
settings.readProtocolPriorityConfiguration(config);
143+
131144
return settings;
132145
}
133146

@@ -450,8 +463,13 @@ public ShapeId resolveServiceProtocol(Model model, ServiceShape service, Set<Sha
450463
+ "generate in smithy-build.json to generate this service.");
451464
}
452465

453-
return resolvedProtocols.stream()
454-
.filter(supportedProtocols::contains)
466+
List<ShapeId> protocolPriority = this.protocolPriorityConfig.getProtocolPriority(service.toShapeId());
467+
List<ShapeId> protocolPriorityList = protocolPriority != null && !protocolPriority.isEmpty()
468+
? protocolPriority
469+
: new ArrayList<>(supportedProtocols);
470+
471+
return protocolPriorityList.stream()
472+
.filter(resolvedProtocols::contains)
455473
.findFirst()
456474
.orElseThrow(() -> new UnresolvableProtocolException(String.format(
457475
"The %s service supports the following unsupported protocols %s. The following protocol "
@@ -482,6 +500,17 @@ public String getDefaultSigningName() {
482500
return defaultSigningName;
483501
}
484502

503+
/**
504+
* @return config container for service and/or default protocol selection priority overrides.
505+
*/
506+
public ProtocolPriorityConfig getProtocolPriority() {
507+
return protocolPriorityConfig;
508+
}
509+
510+
public void setProtocolPriority(ProtocolPriorityConfig protocolPriorityConfig) {
511+
this.protocolPriorityConfig = protocolPriorityConfig;
512+
}
513+
485514
/**
486515
* An enum indicating the type of artifact the code generator will produce.
487516
*/
@@ -586,4 +615,52 @@ public static PackageManager fromString(String s) {
586615
throw new CodegenException(String.format("Unsupported package manager: %s", s));
587616
}
588617
}
618+
619+
/**
620+
* Reads serviceProtocolPriority and defaultProtocolPriority configuration fields.
621+
* {
622+
* serviceProtocolPriority: {
623+
* "namespace#Service": ["namespace#Protocol1", "namespace#Protocol2"]
624+
* },
625+
* defaultProtocolPriority: ["namespace#Protocol"]
626+
* }
627+
*/
628+
private void readProtocolPriorityConfiguration(ObjectNode config) {
629+
Map<ShapeId, List<ShapeId>> serviceProtocolPriorityCustomizations = new HashMap<>();
630+
List<ShapeId> customDefaultPriority = new LinkedList<>();
631+
try {
632+
Optional<ObjectNode> protocolPriorityNode = config.getObjectMember(SERVICE_PROTOCOL_PRIORITY);
633+
if (protocolPriorityNode.isPresent()) {
634+
ObjectNode objectNode = protocolPriorityNode.get();
635+
objectNode.getMembers().forEach((StringNode k, Node v) -> {
636+
ShapeId serviceShapeId = ShapeId.from(k.getValue());
637+
List<ShapeId> protocolList = v.asArrayNode().get().getElementsAs(
638+
e -> ShapeId.from(e.asStringNode().get().getValue())
639+
);
640+
serviceProtocolPriorityCustomizations.put(
641+
serviceShapeId,
642+
protocolList
643+
);
644+
});
645+
}
646+
Optional<ArrayNode> defaultProtocolPriorityOpt = config.getArrayMember(DEFAULT_PROTOCOL_PRIORITY);
647+
if (defaultProtocolPriorityOpt.isPresent()) {
648+
ArrayNode defaultProtocolPriorityStringArr = defaultProtocolPriorityOpt.get();
649+
customDefaultPriority.addAll(
650+
defaultProtocolPriorityStringArr.getElementsAs(
651+
e -> ShapeId.from(e.asStringNode().get().getValue())
652+
)
653+
);
654+
}
655+
} catch (Exception e) {
656+
throw new IllegalArgumentException(
657+
"Error while parsing serviceProtocolPriority or defaultProtocolPriority configuration fields",
658+
e
659+
);
660+
}
661+
protocolPriorityConfig = new ProtocolPriorityConfig(
662+
serviceProtocolPriorityCustomizations,
663+
customDefaultPriority.isEmpty() ? null : customDefaultPriority
664+
);
665+
}
589666
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.typescript.codegen.protocols;
7+
8+
import java.util.ArrayList;
9+
import java.util.HashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.Objects;
13+
import software.amazon.smithy.model.shapes.ShapeId;
14+
15+
16+
/**
17+
* Allows customization of protocol selection for specific services or a global default ordering.
18+
*/
19+
public final class ProtocolPriorityConfig {
20+
private final Map<ShapeId, List<ShapeId>> serviceProtocolPriorityCustomizations;
21+
private final List<ShapeId> customDefaultPriority;
22+
23+
public ProtocolPriorityConfig(
24+
Map<ShapeId, List<ShapeId>> serviceProtocolPriorityCustomizations,
25+
List<ShapeId> customDefaultPriority
26+
) {
27+
this.serviceProtocolPriorityCustomizations =
28+
Objects.requireNonNullElseGet(serviceProtocolPriorityCustomizations, HashMap::new);
29+
this.customDefaultPriority = customDefaultPriority;
30+
}
31+
32+
/**
33+
* @param serviceShapeId - service scope.
34+
* @return priority order of protocols or null if no override exists.
35+
*/
36+
public List<ShapeId> getProtocolPriority(ShapeId serviceShapeId) {
37+
return serviceProtocolPriorityCustomizations.getOrDefault(
38+
serviceShapeId,
39+
customDefaultPriority != null ? new ArrayList<>(customDefaultPriority) : null
40+
);
41+
}
42+
}

0 commit comments

Comments
 (0)