Skip to content

Commit

Permalink
Process Operation Context Params in Endpoints (#1379)
Browse files Browse the repository at this point in the history
  • Loading branch information
trivikr committed Aug 29, 2024
1 parent e94028b commit a2bb933
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,14 @@ private void generateEndpointParameterInstructionProvider() {
}
paramNames.add(name);
});

parameterFinder.getOperationContextParamValues(operation).forEach((name, jmesPathForInputInJs) -> {
writer.write(
"""
$L: { type: \"operationContextParams\", name: $L },
""",
name, jmesPathForInputInJs);
});
}
writer.write("})")
.dedent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import software.amazon.smithy.rulesengine.traits.ClientContextParamsTrait;
import software.amazon.smithy.rulesengine.traits.ContextParamTrait;
import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait;
import software.amazon.smithy.rulesengine.traits.OperationContextParamsTrait;
import software.amazon.smithy.rulesengine.traits.StaticContextParamsTrait;
import software.amazon.smithy.utils.SmithyInternalApi;

Expand Down Expand Up @@ -152,6 +153,68 @@ public Map<String, String> getContextParams(Shape operationInput) {
return map;
}

/**
* Get map of params to JavaScript equivalent of provided JMESPath expressions.
*/
public Map<String, String> getOperationContextParamValues(OperationShape operation) {
Map<String, String> map = new HashMap<>();

Optional<OperationContextParamsTrait> trait = operation.getTrait(OperationContextParamsTrait.class);
if (trait.isPresent()) {
trait.get().getParameters().forEach((name, definition) -> {
String separator = ".";
String value = "this" + separator + "input";
String path = definition.getPath();

// Split JMESPath expression string on separator and add JavaScript equivalent.
for (String part : path.split("[" + separator + "]")) {
if (value.endsWith(")")) {
// The value is an object, which needs to run on map.
value += ".map(obj => obj";
}

// Process keys https://jmespath.org/specification.html#keys
if (part.startsWith("keys(")) {
// Get provided object for which keys are to be extracted.
String object = part.substring(5, part.length() - 1);
value = "Object.keys(" + value + separator + object + ")";
continue;
}

// Process list wildcard expression https://jmespath.org/specification.html#wildcard-expressions
if (part.equals("*") || part.equals("[*]")) {
value = "Object.values(" + value + ")";
continue;
}

// Process hash wildcard expression https://jmespath.org/specification.html#wildcard-expressions
if (part.endsWith("[*]")) {
// Get key to run hash wildcard on.
String key = part.substring(0, part.length() - 3);
value = value + separator + key + separator + "map(obj => obj";
continue;
}

// Treat remaining part as identifier without spaces https://jmespath.org/specification.html#identifiers
value += separator + part;
}

// Remove no-op map, if it exists.
if (value.endsWith(separator + "map(obj => obj")) {
value = value.substring(0, value.length() - 15);
}

// Close all open brackets.
value += ")".repeat((int) (
value.chars().filter(ch -> ch == '(').count() - value.chars().filter(ch -> ch == ')').count()));

map.put(name, value);
});
}

return map;
}

private static class RuleSetParameterFinderVisitor extends NodeVisitor.Default<Void> {
private final Map<String, String> map;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,64 @@ public class CommandGeneratorTest {
public void addsCommandSpecificPlugins() {
testCommmandCodegen(
"output-structure.smithy",
"getSerdePlugin(config, this.serialize, this.deserialize)"
new String[] {"getSerdePlugin(config, this.serialize, this.deserialize)"}
);
}

@Test
public void writesSerializer() {
testCommmandCodegen(
"output-structure.smithy",
".ser("
new String[] {".ser("}
);
}

@Test
public void writesDeserializer() {
testCommmandCodegen(
"output-structure.smithy",
".de("
new String[] {".de("}
);
}

private void testCommmandCodegen(String file, String expectedType) {
Model model = Model.assembler()
.addImport(getClass().getResource(file))
.assemble()
.unwrap();
@Test
public void writesOperationContextParamValues() {
testCommmandCodegen(
"endpointsV2/endpoints-operation-context-params.smithy",
new String[] {
"opContextParamIdentifier: { type: \"operationContextParams\", name: this.input.fooString }",
"opContextParamSubExpression: { type: \"operationContextParams\", name: this.input.fooObj.bar }",
"opContextParamWildcardExpressionList: { type: \"operationContextParams\", name: this.input.fooList }",
"opContextParamWildcardExpressionListObj: { type: \"operationContextParams\", name: this.input.fooListObj.map(obj => obj.key) }",
"opContextParamWildcardExpressionHash: { type: \"operationContextParams\", name: Object.values(this.input.fooObjObj).map(obj => obj.bar) }",
"opContextParamKeys: { type: \"operationContextParams\", name: Object.keys(this.input.fooKeys) }",
}
);
}

private void testCommmandCodegen(String filename, String[] expectedTypeArray) {
MockManifest manifest = new MockManifest();
PluginContext context = PluginContext.builder()
.model(model)
.fileManifest(manifest)
.settings(Node.objectNodeBuilder()
.withMember("service", Node.from("smithy.example#Example"))
.withMember("package", Node.from("example"))
.withMember("packageVersion", Node.from("1.0.0"))
.build())
.build();
.pluginClassLoader(getClass().getClassLoader())
.model(Model.assembler()
.addImport(getClass().getResource(filename))
.discoverModels()
.assemble()
.unwrap())
.fileManifest(manifest)
.settings(Node.objectNodeBuilder()
.withMember("service", Node.from("smithy.example#Example"))
.withMember("package", Node.from("example"))
.withMember("packageVersion", Node.from("1.0.0"))
.build())
.build();

new TypeScriptCodegenPlugin().execute(context);
String contents = manifest.getFileString(CodegenUtils.SOURCE_FOLDER + "//commands/GetFooCommand.ts").get();

assertThat(contents, containsString("as __MetadataBearer"));
assertThat(contents, containsString(expectedType));
for (String expectedType : expectedTypeArray) {
assertThat(contents, containsString(expectedType));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
$version: "2.0"

namespace smithy.example

@smithy.rules#endpointRuleSet({
version: "1.0",
parameters: {
opContextParamIdentifier: {
type: "string",
},
opContextParamSubExpression: {
type: "string",
},
opContextParamWildcardExpressionList: {
type: "stringArray",
},
opContextParamWildcardExpressionListObj: {
type: "stringArray",
},
opContextParamWildcardExpressionHash: {
type: "stringArray",
},
opContextParamKeys: {
type: "stringArray",
},
},
rules: []
})
service Example {
version: "1.0.0",
operations: [GetFoo]
}

@smithy.rules#operationContextParams(
"opContextParamIdentifier": { path: "fooString" }
"opContextParamSubExpression": { path: "fooObj.bar" }
"opContextParamWildcardExpressionList": { path: "fooList[*]" }
"opContextParamWildcardExpressionListObj": { path: "fooListObj[*].key" }
"opContextParamWildcardExpressionHash": { path: "fooObjObj.*.bar" }
"opContextParamKeys": { path: "keys(fooKeys)" }
)
operation GetFoo {
input: GetFooInput,
output: GetFooOutput,
errors: [GetFooError]
}

structure GetFooInput {
fooKeys: FooObject,
fooList: FooList,
fooListObj: FooListObj,
fooObj: FooObject,
fooObjObj: FooObjectObject,
fooString: String,
}

structure FooObject {
bar: String
}

structure FooObjectObject {
baz: FooObject
}

list FooList {
member: String
}

list FooListObj {
member: FooListObjMember
}

structure FooListObjMember {
key: String
}

structure GetFooOutput {}

@error("client")
structure GetFooError {}

0 comments on commit a2bb933

Please sign in to comment.