Skip to content

Commit

Permalink
feat: add protocol resolution priority system (#1370)
Browse files Browse the repository at this point in the history
* feat(cbor): add protocol resolution priority system

* test: update test code comments

* subordinate ProtocolPriorityConfig to TypeScriptSettings

* use immutable config for protocol priority
  • Loading branch information
kuhe authored Aug 19, 2024
1 parent 4c692ae commit 8b3d752
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 34 deletions.
2 changes: 2 additions & 0 deletions .changeset/young-eagles-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,19 +185,21 @@ By default, the Smithy TypeScript code generators provide the code generation fr
[`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`.

|Setting|Required|Description|
|---|---|---|
|`package`|Yes|Name of the package in `package.json`.|
|`packageVersion`|Yes|Version of the package in `package.json`.|
|`packageDescription`|No|Description of the package in `package.json`. The default value is `${package} client`|
|`packageJson`|No|Custom `package.json` properties that will be merged with the base `package.json`. The default value is an empty object.|
|`packageManager`|No|Configured package manager for the package. The default value is `yarn`.|
|`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.|
|`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.|
|`private`|No|Whether the package is `private` in `package.json`. The default value is `false`.|
|`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`.|
|`createDefaultReadme`|No|Whether to generate a default `README.md` for the package. The default value is `false`.|
|`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.|
| Setting |Required| Description |
|---------------------------|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `package` |Yes| Name of the package in `package.json`. |
| `packageVersion` |Yes| Version of the package in `package.json`. |
| `packageDescription` |No| Description of the package in `package.json`. The default value is `${package} client` |
| `packageJson` |No| Custom `package.json` properties that will be merged with the base `package.json`. The default value is an empty object. |
| `packageManager` |No| Configured package manager for the package. The default value is `yarn`. |
| `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. |
| `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. |
| `private` |No| Whether the package is `private` in `package.json`. The default value is `false`. |
| `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`. |
| `createDefaultReadme` |No| Whether to generate a default `README.md` for the package. The default value is `false`. |
| `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. |
| `serviceProtocolPriority` |No| Map of service `ShapeId` strings to lists of protocol `ShapeId` strings. Used to override protocol selection behavior. |
| `defaultProtocolPriority` |No| List of protocol `ShapeId` strings. Lower precedence than `serviceProtocolPriority` but applies to all services. |

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

Expand Down
10 changes: 5 additions & 5 deletions private/smithy-rpcv2-cbor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,25 @@
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/types": "latest",
"@smithy/config-resolver": "^3.0.5",
"@smithy/core": "^2.3.2",
"@smithy/core": "^2.4.0",
"@smithy/fetch-http-handler": "^3.2.4",
"@smithy/hash-node": "^3.0.3",
"@smithy/invalid-dependency": "^3.0.3",
"@smithy/middleware-content-length": "^3.0.5",
"@smithy/middleware-retry": "^3.0.14",
"@smithy/middleware-retry": "^3.0.15",
"@smithy/middleware-serde": "^3.0.3",
"@smithy/middleware-stack": "^3.0.3",
"@smithy/node-config-provider": "^3.1.4",
"@smithy/node-http-handler": "^3.1.4",
"@smithy/protocol-http": "^4.1.0",
"@smithy/smithy-client": "^3.1.12",
"@smithy/smithy-client": "^3.2.0",
"@smithy/types": "^3.3.0",
"@smithy/url-parser": "^3.0.3",
"@smithy/util-base64": "^3.0.0",
"@smithy/util-body-length-browser": "^3.0.0",
"@smithy/util-body-length-node": "^3.0.0",
"@smithy/util-defaults-mode-browser": "^3.0.14",
"@smithy/util-defaults-mode-node": "^3.0.14",
"@smithy/util-defaults-mode-browser": "^3.0.15",
"@smithy/util-defaults-mode-node": "^3.0.15",
"@smithy/util-middleware": "^3.0.3",
"@smithy/util-retry": "^3.0.3",
"@smithy/util-utf8": "^3.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -136,7 +136,9 @@ private ProtocolGenerator resolveProtocolGenerator(
TypeScriptSettings settings
) {
// Collect all of the supported protocol generators.
Map<ShapeId, ProtocolGenerator> generators = new HashMap<>();
// Preserve insertion order as default priority order.
Map<ShapeId, ProtocolGenerator> generators = new LinkedHashMap<>();

for (TypeScriptIntegration integration : integrations) {
for (ProtocolGenerator generator : integration.getProtocolGenerators()) {
generators.put(generator.getProtocol(), generator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@

package software.amazon.smithy.typescript.codegen;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.logging.Logger;
Expand All @@ -27,6 +32,7 @@
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.ServiceIndex;
import software.amazon.smithy.model.node.ArrayNode;
import software.amazon.smithy.model.node.BooleanNode;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
Expand All @@ -36,6 +42,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.ProtocolPriorityConfig;
import software.amazon.smithy.utils.SmithyUnstableApi;

/**
Expand All @@ -59,6 +66,8 @@ public final class TypeScriptSettings {
private static final String CREATE_DEFAULT_README = "createDefaultReadme";
private static final String USE_LEGACY_AUTH = "useLegacyAuth";
private static final String GENERATE_TYPEDOC = "generateTypeDoc";
private static final String SERVICE_PROTOCOL_PRIORITY = "serviceProtocolPriority";
private static final String DEFAULT_PROTOCOL_PRIORITY = "defaultProtocolPriority";

private String packageName;
private String packageDescription = "";
Expand All @@ -77,6 +86,7 @@ public final class TypeScriptSettings {
private boolean createDefaultReadme = false;
private boolean useLegacyAuth = false;
private boolean generateTypeDoc = false;
private ProtocolPriorityConfig protocolPriorityConfig = new ProtocolPriorityConfig(null, null);

@Deprecated
public static TypeScriptSettings from(Model model, ObjectNode config) {
Expand Down Expand Up @@ -128,6 +138,9 @@ public static TypeScriptSettings from(Model model, ObjectNode config, ArtifactTy
.orElse(RequiredMemberMode.NULLABLE));

settings.setPluginSettings(config);

settings.readProtocolPriorityConfiguration(config);

return settings;
}

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

return resolvedProtocols.stream()
.filter(supportedProtocols::contains)
List<ShapeId> protocolPriority = this.protocolPriorityConfig.getProtocolPriority(service.toShapeId());
List<ShapeId> 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 "
Expand Down Expand Up @@ -482,6 +500,17 @@ public String getDefaultSigningName() {
return defaultSigningName;
}

/**
* @return config container for service and/or default protocol selection priority overrides.
*/
public ProtocolPriorityConfig getProtocolPriority() {
return protocolPriorityConfig;
}

public void setProtocolPriority(ProtocolPriorityConfig protocolPriorityConfig) {
this.protocolPriorityConfig = protocolPriorityConfig;
}

/**
* An enum indicating the type of artifact the code generator will produce.
*/
Expand Down Expand Up @@ -586,4 +615,52 @@ public static PackageManager fromString(String s) {
throw new CodegenException(String.format("Unsupported package manager: %s", s));
}
}

/**
* Reads serviceProtocolPriority and defaultProtocolPriority configuration fields.
* {
* serviceProtocolPriority: {
* "namespace#Service": ["namespace#Protocol1", "namespace#Protocol2"]
* },
* defaultProtocolPriority: ["namespace#Protocol"]
* }
*/
private void readProtocolPriorityConfiguration(ObjectNode config) {
Map<ShapeId, List<ShapeId>> serviceProtocolPriorityCustomizations = new HashMap<>();
List<ShapeId> customDefaultPriority = new LinkedList<>();
try {
Optional<ObjectNode> protocolPriorityNode = config.getObjectMember(SERVICE_PROTOCOL_PRIORITY);
if (protocolPriorityNode.isPresent()) {
ObjectNode objectNode = protocolPriorityNode.get();
objectNode.getMembers().forEach((StringNode k, Node v) -> {
ShapeId serviceShapeId = ShapeId.from(k.getValue());
List<ShapeId> protocolList = v.asArrayNode().get().getElementsAs(
e -> ShapeId.from(e.asStringNode().get().getValue())
);
serviceProtocolPriorityCustomizations.put(
serviceShapeId,
protocolList
);
});
}
Optional<ArrayNode> defaultProtocolPriorityOpt = config.getArrayMember(DEFAULT_PROTOCOL_PRIORITY);
if (defaultProtocolPriorityOpt.isPresent()) {
ArrayNode defaultProtocolPriorityStringArr = defaultProtocolPriorityOpt.get();
customDefaultPriority.addAll(
defaultProtocolPriorityStringArr.getElementsAs(
e -> ShapeId.from(e.asStringNode().get().getValue())
)
);
}
} catch (Exception e) {
throw new IllegalArgumentException(
"Error while parsing serviceProtocolPriority or defaultProtocolPriority configuration fields",
e
);
}
protocolPriorityConfig = new ProtocolPriorityConfig(
serviceProtocolPriorityCustomizations,
customDefaultPriority.isEmpty() ? null : customDefaultPriority
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 java.util.Objects;
import software.amazon.smithy.model.shapes.ShapeId;


/**
* Allows customization of protocol selection for specific services or a global default ordering.
*/
public final class ProtocolPriorityConfig {
private final Map<ShapeId, List<ShapeId>> serviceProtocolPriorityCustomizations;
private final List<ShapeId> customDefaultPriority;

public ProtocolPriorityConfig(
Map<ShapeId, List<ShapeId>> serviceProtocolPriorityCustomizations,
List<ShapeId> customDefaultPriority
) {
this.serviceProtocolPriorityCustomizations =
Objects.requireNonNullElseGet(serviceProtocolPriorityCustomizations, HashMap::new);
this.customDefaultPriority = customDefaultPriority;
}

/**
* @param serviceShapeId - service scope.
* @return priority order of protocols or null if no override exists.
*/
public List<ShapeId> getProtocolPriority(ShapeId serviceShapeId) {
return serviceProtocolPriorityCustomizations.getOrDefault(
serviceShapeId,
customDefaultPriority != null ? new ArrayList<>(customDefaultPriority) : null
);
}
}
Loading

0 comments on commit 8b3d752

Please sign in to comment.