Skip to content

Commit e67773a

Browse files
alexforsythsrchase
authored andcommitted
Add pagination generation (smithy-lang#200)
This commit adds support for generating a paginator abstraction for service operations with the `@paginated` trait applied.
1 parent 15e6f02 commit e67773a

File tree

3 files changed

+235
-1
lines changed

3 files changed

+235
-1
lines changed

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Set;
2828
import java.util.TreeSet;
2929
import java.util.logging.Logger;
30+
3031
import software.amazon.smithy.build.FileManifest;
3132
import software.amazon.smithy.build.PluginContext;
3233
import software.amazon.smithy.codegen.core.CodegenException;
@@ -47,6 +48,7 @@
4748
import software.amazon.smithy.model.shapes.UnionShape;
4849
import software.amazon.smithy.model.traits.BoxTrait;
4950
import software.amazon.smithy.model.traits.EnumTrait;
51+
import software.amazon.smithy.model.traits.PaginatedTrait;
5052
import software.amazon.smithy.model.traits.Trait;
5153
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator;
5254
import software.amazon.smithy.typescript.codegen.integration.RuntimeClientPlugin;
@@ -57,7 +59,9 @@ class CodegenVisitor extends ShapeVisitor.Default<Void> {
5759

5860
private static final Logger LOGGER = Logger.getLogger(CodegenVisitor.class.getName());
5961

60-
/** A mapping of static resource files to copy over to a new filename. */
62+
/**
63+
* A mapping of static resource files to copy over to a new filename.
64+
*/
6165
private static final Map<String, String> STATIC_FILE_COPIES = MapUtils.of(
6266
"jest.config.js", "jest.config.js",
6367
"tsconfig.es.json", "tsconfig.es.json",
@@ -277,10 +281,27 @@ public Void serviceShape(ServiceShape shape) {
277281
// Generate each operation for the service.
278282
TopDownIndex topDownIndex = model.getKnowledge(TopDownIndex.class);
279283
Set<OperationShape> containedOperations = new TreeSet<>(topDownIndex.getContainedOperations(service));
284+
boolean hasPaginatedOperation = false;
285+
280286
for (OperationShape operation : containedOperations) {
281287
writers.useShapeWriter(operation, commandWriter -> new CommandGenerator(
282288
settings, model, operation, symbolProvider, commandWriter,
283289
runtimePlugins, protocolGenerator, applicationProtocol).run());
290+
if (operation.hasTrait(PaginatedTrait.ID)) {
291+
hasPaginatedOperation = true;
292+
String outputFilename = PaginationGenerator.getOutputFilelocation(operation);
293+
writers.useFileWriter(outputFilename, paginationWriter ->
294+
new PaginationGenerator(model, service, operation, symbolProvider, paginationWriter,
295+
nonModularName).run());
296+
}
297+
}
298+
299+
if (hasPaginatedOperation) {
300+
writers.useFileWriter(PaginationGenerator.PAGINATION_INTERFACE_FILE, paginationWriter ->
301+
PaginationGenerator.generateServicePaginationInterfaces(
302+
nonModularName,
303+
serviceSymbol,
304+
paginationWriter));
284305
}
285306

286307
if (protocolGenerator != null) {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717

1818
import java.util.Set;
1919
import java.util.TreeSet;
20+
2021
import software.amazon.smithy.build.FileManifest;
2122
import software.amazon.smithy.codegen.core.Symbol;
2223
import software.amazon.smithy.codegen.core.SymbolProvider;
2324
import software.amazon.smithy.model.Model;
2425
import software.amazon.smithy.model.knowledge.TopDownIndex;
2526
import software.amazon.smithy.model.shapes.OperationShape;
2627
import software.amazon.smithy.model.shapes.ServiceShape;
28+
import software.amazon.smithy.model.traits.PaginatedTrait;
2729

2830
/**
2931
* Generates an index to export the service client and each command.
@@ -52,8 +54,18 @@ static void writeIndex(
5254
// write export statements for each command in /commands directory
5355
TopDownIndex topDownIndex = model.getKnowledge(TopDownIndex.class);
5456
Set<OperationShape> containedOperations = new TreeSet<>(topDownIndex.getContainedOperations(service));
57+
boolean hasPaginatedOperation = false;
5558
for (OperationShape operation : containedOperations) {
5659
writer.write("export * from \"./commands/" + symbolProvider.toSymbol(operation).getName() + "\";");
60+
if (operation.hasTrait(PaginatedTrait.ID)) {
61+
hasPaginatedOperation = true;
62+
String modulePath = PaginationGenerator.getOutputFilelocation(operation);
63+
writer.write("export * from \"./$L\"", modulePath.replace(".ts", ""));
64+
}
65+
}
66+
if (hasPaginatedOperation) {
67+
String modulePath = PaginationGenerator.PAGINATION_INTERFACE_FILE;
68+
writer.write("export * from \"./$L\"", modulePath.replace(".ts", ""));
5769
}
5870

5971
// write export statement for models
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
17+
package software.amazon.smithy.typescript.codegen;
18+
19+
import java.util.Optional;
20+
21+
import software.amazon.smithy.codegen.core.CodegenException;
22+
import software.amazon.smithy.codegen.core.Symbol;
23+
import software.amazon.smithy.codegen.core.SymbolProvider;
24+
import software.amazon.smithy.model.Model;
25+
import software.amazon.smithy.model.knowledge.PaginatedIndex;
26+
import software.amazon.smithy.model.knowledge.PaginationInfo;
27+
import software.amazon.smithy.model.shapes.OperationShape;
28+
import software.amazon.smithy.model.shapes.ServiceShape;
29+
30+
final class PaginationGenerator implements Runnable {
31+
32+
static final String PAGINATION_INTERFACE_FILE = "pagination/Interfaces.ts";
33+
34+
private final TypeScriptWriter writer;
35+
private final PaginationInfo paginatedInfo;
36+
37+
private final Symbol serviceSymbol;
38+
private final Symbol operationSymbol;
39+
private final Symbol inputSymbol;
40+
private final Symbol outputSymbol;
41+
42+
private final String methodName;
43+
private final String nonModularServiceName;
44+
private final String paginationType;
45+
46+
PaginationGenerator(
47+
Model model,
48+
ServiceShape service,
49+
OperationShape operation,
50+
SymbolProvider symbolProvider,
51+
TypeScriptWriter writer,
52+
String nonModularServiceName
53+
) {
54+
55+
this.writer = writer;
56+
57+
this.serviceSymbol = symbolProvider.toSymbol(service);
58+
this.operationSymbol = symbolProvider.toSymbol(operation);
59+
this.inputSymbol = symbolProvider.toSymbol(operation).expectProperty("inputType", Symbol.class);
60+
this.outputSymbol = symbolProvider.toSymbol(operation).expectProperty("outputType", Symbol.class);
61+
62+
String operationName = operation.getId().getName();
63+
this.nonModularServiceName = nonModularServiceName;
64+
65+
// e.g. listObjects
66+
this.methodName = Character.toLowerCase(operationName.charAt(0)) + operationName.substring(1);
67+
this.paginationType = this.nonModularServiceName + "PaginationConfiguration";
68+
69+
PaginatedIndex paginatedIndex = model.getKnowledge(PaginatedIndex.class);
70+
Optional<PaginationInfo> paginationInfo = paginatedIndex.getPaginationInfo(service, operation);
71+
this.paginatedInfo = paginationInfo.orElseThrow(() -> {
72+
return new CodegenException("Expected Paginator to have pagination information.");
73+
});
74+
}
75+
76+
@Override
77+
public void run() {
78+
// Import Service Types
79+
writer.addImport(operationSymbol.getName(),
80+
operationSymbol.getName(),
81+
operationSymbol.getNamespace());
82+
writer.addImport(inputSymbol.getName(),
83+
inputSymbol.getName(),
84+
inputSymbol.getNamespace());
85+
writer.addImport(outputSymbol.getName(),
86+
outputSymbol.getName(),
87+
outputSymbol.getNamespace());
88+
String nonModularLocation = serviceSymbol.getNamespace()
89+
.replace(serviceSymbol.getName(), nonModularServiceName);
90+
writer.addImport(nonModularServiceName,
91+
nonModularServiceName,
92+
nonModularLocation);
93+
writer.addImport(serviceSymbol.getName(), serviceSymbol.getName(), serviceSymbol.getNamespace());
94+
95+
// Import Pagination types
96+
writer.addImport("Paginator", "Paginator", "@aws-sdk/types");
97+
writer.addImport(paginationType, paginationType, "./" + PAGINATION_INTERFACE_FILE.replace(".ts", ""));
98+
99+
writeCommandRequest();
100+
writeMethodRequest();
101+
writePager();
102+
}
103+
104+
static String getOutputFilelocation(OperationShape operation) {
105+
return "pagination/" + operation.getId().getName() + "Paginator.ts";
106+
}
107+
108+
static void generateServicePaginationInterfaces(
109+
String nonModularServiceName,
110+
Symbol service,
111+
TypeScriptWriter writer
112+
) {
113+
writer.addImport("PaginationConfiguration", "PaginationConfiguration", "@aws-sdk/types");
114+
String nonModularLocation = service.getNamespace().replace(service.getName(), nonModularServiceName);
115+
writer.addImport(nonModularServiceName, nonModularServiceName, nonModularLocation);
116+
writer.addImport(service.getName(), service.getName(), service.getNamespace());
117+
118+
writer.openBlock("export interface $LPaginationConfiguration extends PaginationConfiguration {",
119+
"}", nonModularServiceName, () -> {
120+
writer.write("client: $L | $L;", nonModularServiceName, service.getName());
121+
});
122+
}
123+
124+
private void writePager() {
125+
String serviceTypeName = serviceSymbol.getName();
126+
String inputTypeName = inputSymbol.getName();
127+
String outputTypeName = outputSymbol.getName();
128+
String inputTokenName = paginatedInfo.getInputTokenMember().getMemberName();
129+
String outputTokenName = paginatedInfo.getOutputTokenMember().getMemberName();
130+
131+
writer.openBlock(
132+
"export async function* $LPaginate(config: $L, input: $L, ...additionalArguments: any): Paginator<$L>{",
133+
"}", methodName, paginationType, inputTypeName, outputTypeName, () -> {
134+
writer.write("let token: string | undefined = config.startingToken || '';");
135+
136+
writer.write("let hasNext = true;");
137+
writer.write("let page: $L;", outputTypeName);
138+
writer.openBlock("while (hasNext) {", "}", () -> {
139+
writer.write("input[$S] = token;", inputTokenName);
140+
if (paginatedInfo.getPageSizeMember().isPresent()) {
141+
String pageSize = paginatedInfo.getPageSizeMember().get().getMemberName();
142+
writer.write("input[$S] = config.pageSize;", pageSize);
143+
}
144+
145+
writer.openBlock("if (config.client instanceof $L) {", "}", nonModularServiceName, () -> {
146+
writer.write("page = await makePagedRequest(config.client, input, ...additionalArguments);");
147+
});
148+
writer.openBlock("else if (config.client instanceof $L) {", "}", serviceTypeName, () -> {
149+
writer.write("page = await makePagedClientRequest(config.client, input, ...additionalArguments);");
150+
});
151+
writer.openBlock("else {", "}", () -> {
152+
writer.write("throw new Error(\"Invalid client, expected $L | $L\");",
153+
nonModularServiceName, serviceTypeName);
154+
});
155+
156+
writer.write("yield page;");
157+
if (outputTokenName.contains(".")) {
158+
// Smithy allows one level indexing (ex. 'bucket.outputToken').
159+
String[] outputIndex = outputTokenName.split("\\.");
160+
writer.write("token = page[$S][$S];", outputIndex[0], outputIndex[1]);
161+
} else {
162+
writer.write("token = page[$S];", outputTokenName);
163+
}
164+
165+
writer.write("hasNext = !!(token);");
166+
});
167+
168+
writer.write("// @ts-ignore");
169+
writer.write("return undefined;");
170+
});
171+
}
172+
173+
174+
/**
175+
* Paginated command that calls client.method({...}) under the hood. This is meant for server side environments and
176+
* exposes the entire service.
177+
*/
178+
private void writeMethodRequest() {
179+
writer.openBlock(
180+
"const makePagedRequest = async (client: $L, input: $L, ...args: any): Promise<$L> => {",
181+
"}", nonModularServiceName, inputSymbol.getName(),
182+
outputSymbol.getName(), () -> {
183+
writer.write("// @ts-ignore");
184+
writer.write("return await client.$L(input, ...args);", methodName);
185+
});
186+
}
187+
188+
/**
189+
* Paginated command that calls CommandClient().send({...}) under the hood. This is meant for client side (browser)
190+
* environments and does not generally expose the entire service.
191+
*/
192+
private void writeCommandRequest() {
193+
writer.openBlock(
194+
"const makePagedClientRequest = async (client: $L, input: $L, ...args: any): Promise<$L> => {",
195+
"}", serviceSymbol.getName(), inputSymbol.getName(),
196+
outputSymbol.getName(), () -> {
197+
writer.write("// @ts-ignore");
198+
writer.write("return await client.send(new $L(input, ...args));", operationSymbol.getName());
199+
});
200+
}
201+
}

0 commit comments

Comments
 (0)