Skip to content

Commit

Permalink
feat: xml serde reduction
Browse files Browse the repository at this point in the history
  • Loading branch information
kuhe committed Dec 15, 2023
1 parent 7748297 commit 90c39ed
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 42 deletions.
2 changes: 2 additions & 0 deletions .changeset/selfish-pens-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ smithy-typescript-integ-tests/yarn.lock

# Issue https://github.com/awslabs/smithy-typescript/issues/425
smithy-typescript-codegen/bin/
smithy-typescript-codegen-test/bin/
smithy-typescript-ssdk-codegen-test-utils/bin/
smithy-typescript-codegen-test/example-weather-customizations/bin/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,8 +493,9 @@ private void readEventHeaders(GenerationContext context, StructureShape event) {
.filter(member -> member.hasTrait(EventHeaderTrait.class)).collect(Collectors.toList());
for (MemberShape headerMember : headerMembers) {
String memberName = headerMember.getMemberName();
writer.openBlock("if (output.headers[$S] !== undefined) {", "}", memberName, () -> {
writer.write("contents.$1L = output.headers[$1S].value;", memberName);
String varName = context.getStringStore().var(memberName);
writer.openBlock("if (output.headers[$L] !== undefined) {", "}", varName, () -> {
writer.write("contents[$1L] = output.headers[$1L].value;", varName);
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ public void generateSharedComponents(GenerationContext context) {
HttpProtocolGeneratorUtils.generateMetadataDeserializer(context, getApplicationProtocol().getResponseType());
HttpProtocolGeneratorUtils.generateCollectBodyString(context);
HttpProtocolGeneratorUtils.generateHttpBindingUtils(context);

writer.write(
context.getStringStore().getIncremental()
);
}

@Override
Expand Down Expand Up @@ -673,7 +677,9 @@ private void generateOperationRequestSerializer(
// Get the hostname, path, port, and scheme from client's resolved endpoint.
// Then construct the request from them. The client's resolved endpoint can
// be default one or supplied by users.
writer.write("const {hostname, protocol = $S, port, path: basePath} = await context.endpoint();", "https");

writer.addImport("requestBuilder", "rb", TypeScriptDependency.SMITHY_CORE);
writer.write("const b = rb(input, context);");

writeRequestHeaders(context, operation, bindingIndex);
writeResolvedPath(context, operation, bindingIndex, trait);
Expand All @@ -692,24 +698,17 @@ private void generateOperationRequestSerializer(
boolean hasHostPrefix = operation.hasTrait(EndpointTrait.class);
if (hasHostPrefix) {
HttpProtocolGeneratorUtils.writeHostPrefix(context, operation);
writer.write("b.hn(resolvedHostname);");
}
writer.openBlock("return new $T({", "});", requestType, () -> {
writer.write("protocol,");
if (hasHostPrefix) {
writer.write("hostname: resolvedHostname,");
} else {
writer.write("hostname,");
}
writer.write("port,");
writer.write("method: $S,", trait.getMethod());
writer.write("headers,");
writer.write("path: resolvedPath,");
if (hasQueryComponents) {
writer.write("query,");
}
// Always set the body,
writer.write("body,");
});
writer.write("b.m($S)", trait.getMethod());
writer.write(".h(headers)");
if (hasQueryComponents) {
writer.write(".q(query)");
}
// Always set the body,
writer.write(".b(body);");

writer.write("return b.build();");
});

writer.write("");
Expand Down Expand Up @@ -762,8 +761,7 @@ private void writeResolvedPath(
: Collections.emptyMap();

// Always write the bound path, but only the actual segments.
writer.write("let resolvedPath = `$L` + $S;",
"${basePath?.endsWith('/') ? basePath.slice(0, -1) : (basePath || '')}",
writer.write("b.bp(\"$L\");",
"/" + trait.getUri().getSegments().stream()
.filter(segment -> {
if (!useEndpointsV2) {
Expand Down Expand Up @@ -804,7 +802,7 @@ private void writeResolvedPath(

// Get the correct label to use.
Segment uriLabel = uriLabels.stream().filter(s -> s.getContent().equals(memberName)).findFirst().get();
writer.write("resolvedPath = __resolvedPath(resolvedPath, input, '$L', $L, '$L', $L)",
writer.write("b.p('$L', $L, '$L', $L)",
memberName,
labelValueProvider,
uriLabel.toString(),
Expand All @@ -830,7 +828,7 @@ private boolean writeRequestQueryString(
writer.openBlock("const query: any = map({", "});", () -> {
if (!queryLiterals.isEmpty()) {
// Write any query literals present in the uri.
queryLiterals.forEach((k, v) -> writer.write("$S: [, $S],", k, v));
queryLiterals.forEach((k, v) -> writer.write("[$L]: [, $S],", context.getStringStore().var(k), v));
}
// Handle any additional query params bindings.
// If query string parameter is also present in httpQuery, it would be overwritten.
Expand Down Expand Up @@ -879,36 +877,41 @@ private void writeRequestQueryParam(
String queryValue = getInputValue(
context,
binding.getLocation(),
"input." + memberName + memberAssertionComponent,
"input[" + context.getStringStore().var(memberName) + "]" + memberAssertionComponent,
binding.getMember(),
target
);

writer.addImport("expectNonNull", "__expectNonNull", TypeScriptDependency.AWS_SMITHY_CLIENT);

if (Objects.equals("input." + memberName + memberAssertionComponent, queryValue)) {
if (
Objects.equals(
"input[" + context.getStringStore().var(memberName) + "]" + memberAssertionComponent,
queryValue
)
) {
String value = isRequired ? "__expectNonNull($L, `" + memberName + "`)" : "$L";
// simple undefined check
writer.write(
"$S: [," + value + idempotencyComponent + "],",
binding.getLocationName(),
"[$L]: [," + value + idempotencyComponent + "],",
context.getStringStore().var(binding.getLocationName()),
queryValue
);
} else {
if (isRequired) {
// __expectNonNull is immediately invoked and not inside a function.
writer.write(
"$S: [__expectNonNull(input.$L, `$L`) != null, () => $L],",
binding.getLocationName(),
"[$L]: [__expectNonNull(input.$L, `$L`) != null, () => $L],",
context.getStringStore().var(binding.getLocationName()),
memberName,
memberName,
queryValue // no idempotency token default for required members
);
} else {
// undefined check with lazy eval
writer.write(
"$S: [() => input.$L !== void 0, () => ($L)$L],",
binding.getLocationName(),
"[$L]: [() => input.$L !== void 0, () => ($L)$L],",
context.getStringStore().var(binding.getLocationName()),
memberName,
queryValue,
idempotencyComponent
Expand Down Expand Up @@ -976,7 +979,9 @@ private void writeRequestHeaders(
}

private void writeNormalHeader(GenerationContext context, HttpBinding binding) {
String memberLocation = "input." + context.getSymbolProvider().toMemberName(binding.getMember());
String memberLocation = "input["
+ context.getStringStore().var(context.getSymbolProvider().toMemberName(binding.getMember()))
+ "]";
Shape target = context.getModel().expectShape(binding.getMember().getTarget());

String headerKey = binding.getLocationName().toLowerCase(Locale.US);
Expand All @@ -996,8 +1001,8 @@ private void writeNormalHeader(GenerationContext context, HttpBinding binding) {
}
// evaluated value has a function or method call attached
headerBuffer.put(headerKey, String.format(
"'%s': [() => isSerializableHeaderValue(%s), () => %s],",
headerKey,
"[%s]: [() => isSerializableHeaderValue(%s), () => %s],",
context.getStringStore().var(headerKey),
memberLocation + defaultValue,
headerValue + defaultValue
));
Expand All @@ -1008,8 +1013,8 @@ private void writeNormalHeader(GenerationContext context, HttpBinding binding) {
value = headerValue + " || " + s.substring(s.indexOf(": ") + 2, s.length() - 1);
}
headerBuffer.put(headerKey, String.format(
"'%s': %s,",
headerKey,
"[%s]: %s,",
context.getStringStore().var(headerKey),
value
));
}
Expand Down Expand Up @@ -2231,14 +2236,20 @@ private void readNormalHeaders(
String memberName = context.getSymbolProvider().toMemberName(binding.getMember());
String headerName = binding.getLocationName().toLowerCase(Locale.US);
Shape target = context.getModel().expectShape(binding.getMember().getTarget());
String headerValue = getOutputValue(context, binding.getLocation(),
outputName + ".headers['" + headerName + "']", binding.getMember(), target);
String checkedValue = outputName + ".headers['" + headerName + "']";
String headerValue = getOutputValue(
context, binding.getLocation(),
outputName + ".headers[" + context.getStringStore().var(headerName) + "]",
binding.getMember(), target
);
String checkedValue = outputName + ".headers[" + context.getStringStore().var(headerName) + "]";

if (checkedValue.equals(headerValue)) {
writer.write("$L: [, $L],", memberName, headerValue);
writer.write("[$L]: [, $L],", context.getStringStore().var(memberName), headerValue);
} else {
writer.write("$L: [() => void 0 !== $L, () => $L],", memberName, checkedValue, headerValue);
writer.write(
"[$L]: [() => void 0 !== $L, () => $L],",
context.getStringStore().var(memberName), checkedValue, headerValue
);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ public void generateSharedComponents(GenerationContext context) {
// Write common request header to be shared by all requests
writeSharedRequestHeaders(context);
writer.write("");

writer.write(
context.getStringStore().getIncremental()
);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import software.amazon.smithy.typescript.codegen.TypeScriptDelegator;
import software.amazon.smithy.typescript.codegen.TypeScriptSettings;
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
import software.amazon.smithy.typescript.codegen.util.StringStore;
import software.amazon.smithy.utils.CaseUtils;
import software.amazon.smithy.utils.SmithyUnstableApi;

Expand Down Expand Up @@ -313,6 +314,7 @@ class GenerationContext {
private TypeScriptDelegator writerDelegator;
private TypeScriptWriter writer;
private String protocolName;
private StringStore stringStore = new StringStore();

public TypeScriptSettings getSettings() {
return settings;
Expand Down Expand Up @@ -400,5 +402,9 @@ public GenerationContext withWriter(TypeScriptWriter newWriter) {
copyContext.setWriter(newWriter);
return copyContext;
}

public StringStore getStringStore() {
return stringStore;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.typescript.codegen.util;

import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;

/**
* Intended for use at the
* {@link software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext}
* level, this class allocates and tracks variables assigned to string literals, allowing a
* form of compression on long protocol serde files.
*/
public class StringStore {
private final ConcurrentHashMap<String, String> literalToVariable = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, String> variableToLiteral = new ConcurrentHashMap<>();
private final Set<String> writelog = new HashSet<>();

/**
* @param literal - a literal string value.
* @return the variable name assigned for that string, which may have been encountered before.
*/
public String var(String literal) {
if (literal == null) {
throw new RuntimeException("Literal must not be null.");
}
if (literalToVariable.containsKey(literal)) {
return literalToVariable.get(literal);
}
literalToVariable.put(literal, this.assignKey(literal));
return literalToVariable.get(literal);
}

/**
* Outputs the generated code for any constants that have been
* allocated but not yet retrieved.
*/
public String getIncremental() {
TreeMap<String, String> map = new TreeMap<>(variableToLiteral);

StringBuilder sourceCode = new StringBuilder();
Set<String> incrementalKeys = new HashSet<>();

for (Map.Entry<String, String> entry : map.entrySet()) {
String v = entry.getKey();
String l = entry.getValue();
if (writelog.contains(v)) {
// sourceCode.append(String.format("// const %s = \"%s\";%n", v, l));
} else {
incrementalKeys.add(v);
sourceCode.append(String.format("const %s = \"%s\";%n", v, l));
}
}

writelog.addAll(incrementalKeys);

return sourceCode.toString();
}

/**
* Assigns a new variable or returns the existing variable for a given string literal.
*/
private String assignKey(String literal) {
if (literalToVariable.containsKey(literal)) {
return literalToVariable.get(literal);
}
String variable = allocateVariable(literal);
variableToLiteral.put(variable, literal);
literalToVariable.put(literal, variable);
return variable;
}

/**
* Assigns a unique variable using the letters from the literal.
* Prefers the uppercase or word-starting letters.
*/
private String allocateVariable(String literal) {
String[] sections = literal.split("[-_\\s]");
StringBuilder v = new StringBuilder("_");
Queue<Character> deconfliction = new LinkedList<>();
if (sections.length > 1) {
Arrays.stream(sections)
.map(s -> s.charAt(0))
.filter(this::isAllowedChar)
.forEach(v::append);
} else {
for (int i = 0; i < literal.length(); ++i) {
char c = literal.charAt(i);
if ((c >= 'A' && c <= 'Z') || (isNeutral(v.toString()) && isAllowedChar(c))) {
v.append(c);
} else if (isAllowedChar(c)) {
deconfliction.add(c);
}
}
}
if (v.isEmpty()) {
v.append("v");
}
while (variableToLiteral.containsKey(v.toString())) {
if (!deconfliction.isEmpty()) {
v.append(deconfliction.poll());
} else {
v.append('_');
}
}
return v.toString();
}

/**
* char is in A-Za-z.
*/
private boolean isAllowedChar(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}

/**
* @return true if the variable has only underscores.
*/
private boolean isNeutral(String variable) {
return variable.chars().allMatch(c -> c == '_');
}
}

0 comments on commit 90c39ed

Please sign in to comment.