diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java index c0d4ba26030..9280acf2d5e 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/TypeScriptSettings.java @@ -57,6 +57,7 @@ public final class TypeScriptSettings { private static final String PROTOCOL = "protocol"; private static final String PRIVATE = "private"; private static final String PACKAGE_MANAGER = "packageManager"; + private static final String CREATE_DEFAULT_README = "createDefaultReadme"; private String packageName; private String packageDescription = ""; @@ -72,6 +73,7 @@ public final class TypeScriptSettings { private RequiredMemberMode requiredMemberMode = RequiredMemberMode.NULLABLE; private PackageManager packageManager = PackageManager.YARN; + private boolean createDefaultReadme = false; @Deprecated public static TypeScriptSettings from(Model model, ObjectNode config) { @@ -103,6 +105,8 @@ public static TypeScriptSettings from(Model model, ObjectNode config, ArtifactTy settings.packageJson = config.getObjectMember(PACKAGE_JSON).orElse(Node.objectNode()); config.getStringMember(PROTOCOL).map(StringNode::getValue).map(ShapeId::from).ifPresent(settings::setProtocol); settings.setPrivate(config.getBooleanMember(PRIVATE).map(BooleanNode::getValue).orElse(false)); + settings.setCreateDefaultReadme( + config.getBooleanMember(CREATE_DEFAULT_README).map(BooleanNode::getValue).orElse(false)); settings.setPackageManager( config.getStringMember(PACKAGE_MANAGER) .map(s -> PackageManager.fromString(s.getValue())) @@ -261,6 +265,14 @@ public void setPrivate(boolean isPrivate) { this.isPrivate = isPrivate; } + public boolean createDefaultReadme() { + return createDefaultReadme; + } + + public void setCreateDefaultReadme(boolean createDefaultReadme) { + this.createDefaultReadme = createDefaultReadme; + } + /** * Returns if the generated package will be a client. * @@ -427,11 +439,12 @@ public String getDefaultSigningName() { public enum ArtifactType { CLIENT(SymbolVisitor::new, Arrays.asList(PACKAGE, PACKAGE_DESCRIPTION, PACKAGE_JSON, PACKAGE_VERSION, PACKAGE_MANAGER, - SERVICE, PROTOCOL, TARGET_NAMESPACE, PRIVATE, REQUIRED_MEMBER_MODE)), + SERVICE, PROTOCOL, TARGET_NAMESPACE, PRIVATE, REQUIRED_MEMBER_MODE, + CREATE_DEFAULT_README)), SSDK((m, s) -> new ServerSymbolVisitor(m, new SymbolVisitor(m, s)), Arrays.asList(PACKAGE, PACKAGE_DESCRIPTION, PACKAGE_JSON, PACKAGE_VERSION, PACKAGE_MANAGER, SERVICE, PROTOCOL, TARGET_NAMESPACE, PRIVATE, REQUIRED_MEMBER_MODE, - DISABLE_DEFAULT_VALIDATION)); + DISABLE_DEFAULT_VALIDATION, CREATE_DEFAULT_README)); private final BiFunction symbolProviderFactory; private final List configProperties; diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DefaultReadmeGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DefaultReadmeGenerator.java new file mode 100644 index 00000000000..e4cabc3b1f5 --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/DefaultReadmeGenerator.java @@ -0,0 +1,78 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.typescript.codegen.integration; + +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.typescript.codegen.TypeScriptCodegenContext; +import software.amazon.smithy.typescript.codegen.TypeScriptSettings; +import software.amazon.smithy.utils.IoUtils; +import software.amazon.smithy.utils.SmithyInternalApi; +import software.amazon.smithy.utils.StringUtils; + +@SmithyInternalApi +public final class DefaultReadmeGenerator implements TypeScriptIntegration { + + public static final String README_FILENAME = "README.md"; + public static final String DEFAULT_CLIENT_README_TEMPLATE = "default_readme_client.md.template"; + public static final String DEFAULT_SERVER_README_TEMPLATE = "default_readme_server.md.template"; + + @Override + public void customize(TypeScriptCodegenContext codegenContext) { + TypeScriptSettings settings = codegenContext.settings(); + + if (!settings.createDefaultReadme()) { + return; + } + + String file = settings.generateClient() ? DEFAULT_CLIENT_README_TEMPLATE : DEFAULT_SERVER_README_TEMPLATE; + + Model model = codegenContext.model(); + + codegenContext.writerDelegator().useFileWriter(README_FILENAME, "", writer -> { + ServiceShape service = settings.getService(model); + String resource = IoUtils.readUtf8Resource(getClass(), file); + resource = resource.replaceAll(Pattern.quote("${packageName}"), settings.getPackageName()); + + String clientName = StringUtils.capitalize(service.getId().getName(service)); + + resource = resource.replaceAll(Pattern.quote("${serviceId}"), clientName); + + String rawDocumentation = service.getTrait(DocumentationTrait.class) + .map(DocumentationTrait::getValue) + .orElse(""); + String documentation = Arrays.asList(rawDocumentation.split("\n")).stream() + .map(StringUtils::trim) + .collect(Collectors.joining("\n")); + resource = resource.replaceAll(Pattern.quote("${documentation}"), Matcher.quoteReplacement(documentation)); + + TopDownIndex topDownIndex = TopDownIndex.of(model); + OperationShape firstOperation = topDownIndex.getContainedOperations(service).iterator().next(); + String operationName = firstOperation.getId().getName(service); + resource = resource.replaceAll(Pattern.quote("${commandName}"), operationName); + + // The $ character is escaped using $$ + writer.write(resource.replaceAll(Pattern.quote("$"), Matcher.quoteReplacement("$$"))); + }); + } +} diff --git a/smithy-typescript-codegen/src/main/resources/META-INF/services/software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration b/smithy-typescript-codegen/src/main/resources/META-INF/services/software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration index af4d1f12f86..d2d04b70e68 100644 --- a/smithy-typescript-codegen/src/main/resources/META-INF/services/software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration +++ b/smithy-typescript-codegen/src/main/resources/META-INF/services/software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration @@ -5,3 +5,4 @@ software.amazon.smithy.typescript.codegen.integration.AddDefaultsModeDependency software.amazon.smithy.typescript.codegen.integration.AddHttpApiKeyAuthPlugin software.amazon.smithy.typescript.codegen.integration.AddBaseServiceExceptionClass software.amazon.smithy.typescript.codegen.integration.AddSdkStreamMixinDependency +software.amazon.smithy.typescript.codegen.integration.DefaultReadmeGenerator diff --git a/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/integration/default_readme_client.md.template b/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/integration/default_readme_client.md.template new file mode 100644 index 00000000000..e1cdc2a5ee2 --- /dev/null +++ b/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/integration/default_readme_client.md.template @@ -0,0 +1,136 @@ + + +# ${packageName} + +## Description + +SDK for JavaScript ${serviceId} Client for Node.js, Browser and React Native. + +${documentation} + +## Installing +To install the this package, simply type add or install ${packageName} +using your favorite package manager: +- `npm install ${packageName}` +- `yarn add ${packageName}` +- `pnpm add ${packageName}` + +## Getting Started + +### Import + +To send a request, you only need to import the `${serviceId}Client` and +the commands you need, for example `${commandName}Command`: + +```js +// CJS example +const { ${serviceId}Client, ${commandName}Command } = require("${packageName}"); +``` + +```ts +// ES6+ example +import { ${serviceId}Client, ${commandName}Command } from "${packageName}"; +``` + +### Usage + +To send a request, you: + +- Initiate client with configuration. +- Initiate command with input parameters. +- Call `send` operation on client with command object as input. +- If you are using a custom http handler, you may call `destroy()` to close open connections. + +```js +// a client can be shared by different commands. +const client = new ${serviceId}Client(); + +const params = { /** input parameters */ }; +const command = new ${commandName}Command(params); +``` + +#### Async/await + +We recommend using [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) +operator to wait for the promise returned by send operation as follows: + +```js +// async/await. +try { + const data = await client.send(command); + // process data. +} catch (error) { + // error handling. +} finally { + // finally. +} +``` + +Async-await is clean, concise, intuitive, easy to debug and has better error handling +as compared to using Promise chains or callbacks. + +#### Promises + +You can also use [Promise chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises#chaining) +to execute send operation. + +```js +client.send(command).then( + (data) => { + // process data. + }, + (error) => { + // error handling. + } +); +``` + +Promises can also be called using `.catch()` and `.finally()` as follows: + +```js +client + .send(command) + .then((data) => { + // process data. + }) + .catch((error) => { + // error handling. + }) + .finally(() => { + // finally. + }); +``` + +#### Callbacks + +We do not recommend using callbacks because of [callback hell](http://callbackhell.com/), +but they are supported by the send operation. + +```js +// callbacks. +client.send(command, (err, data) => { + // process err and data. +}); +``` + +### Troubleshooting + +When the service returns an exception, the error will include the exception information, +as well as response metadata (e.g. request id). + +```js +try { + const data = await client.send(command); + // process data. +} catch (error) { + const { requestId, httpStatusCode } = error.$$metadata; + console.log({ requestId, httpStatusCode }); + /** + * The keys within exceptions are also parsed. + * You can access them by specifying exception names: + * if (error.name === 'SomeServiceException') { + * const value = error.specialKeyInException; + * } + */ +} +``` diff --git a/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/integration/default_readme_server.md.template b/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/integration/default_readme_server.md.template new file mode 100644 index 00000000000..c63a91a1c5f --- /dev/null +++ b/smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/integration/default_readme_server.md.template @@ -0,0 +1,57 @@ + + +# ${packageName} + +## Description + +JavaScript Server SDK for ${serviceId} + +${documentation} + +## Installing +To install this package, simply type add or install ${packageName} +using your favorite package manager: +- `npm install ${packageName}` +- `yarn add ${packageName}` +- `pnpm add ${packageName}` + +## Getting Started + +Below is an example service handler created for the ${commandName} operation. + +```ts +import { createServer, IncomingMessage, ServerResponse } from "http"; +import { HttpRequest } from "@aws-sdk/protocol-http"; +import { + ${serviceId}Service as __${serviceId}Service, + ${commandName}Input, + ${commandName}Output, + get${serviceId}ServiceHandler +} from "${packageName}"; +import { convertEvent, convertResponse } from "@aws-smithy/server-node"; + +class ${serviceId}Service implements __${serviceId}Service { + ${commandName}(input: ${commandName}Input, request: HttpRequest): ${commandName}Output { + // Populate your business logic + } +} + +const serviceHandler = get${serviceId}ServiceHandler(new ${serviceId}Service()); + +const server = createServer(async function ( + req: IncomingMessage, + res: ServerResponse & { req: IncomingMessage } +) { + // Convert NodeJS's http request to an HttpRequest. + const httpRequest = convertRequest(req); + + // Call the service handler, which will route the request to the GreetingService + // implementation and then serialize the response to an HttpResponse. + const httpResponse = await serviceHandler.handle(httpRequest); + + // Write the HttpResponse to NodeJS http's response expected format. + return writeResponse(httpResponse, res); +}); + +server.listen(3000); +``` diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/DefaultDefaultReadmeGeneratorTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/DefaultDefaultReadmeGeneratorTest.java new file mode 100644 index 00000000000..b89d9a6a965 --- /dev/null +++ b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/DefaultDefaultReadmeGeneratorTest.java @@ -0,0 +1,77 @@ +package software.amazon.smithy.typescript.codegen; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.typescript.codegen.integration.DefaultReadmeGenerator; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static software.amazon.smithy.typescript.codegen.integration.DefaultReadmeGenerator.README_FILENAME; + +class DefaultDefaultReadmeGeneratorTest { + + private TypeScriptSettings settings; + private TypeScriptCodegenContext context; + private MockManifest manifest; + private SymbolProvider symbolProvider; + private final Model model = Model.assembler() + .addImport(getClass().getResource("simple-service-with-operation.smithy")) + .assemble() + .unwrap(); + + @BeforeEach + void setup() { + settings = TypeScriptSettings.from(model, Node.objectNodeBuilder() + .withMember("service", Node.from("smithy.example#Example")) + .withMember("package", Node.from("example")) + .withMember("packageVersion", Node.from("1.0.0")) + .withMember("createDefaultReadme", Node.from(true)) + .build()); + + manifest = new MockManifest(); + symbolProvider = new SymbolVisitor(model, settings); + } + + private TypeScriptCodegenContext createContext() { + return TypeScriptCodegenContext.builder() + .model(model) + .settings(settings) + .symbolProvider(symbolProvider) + .fileManifest(manifest) + .integrations(List.of(new DefaultReadmeGenerator())) + .runtimePlugins(new ArrayList<>()) + .protocolGenerator(null) + .applicationProtocol(ApplicationProtocol.createDefaultHttpApplicationProtocol()) + .writerDelegator(new TypeScriptDelegator(manifest, symbolProvider)) + .build(); + } + + @Test + void expectDefaultFileWrittenForClientSDK() { + context = createContext(); + new DefaultReadmeGenerator().customize(context); + context.writerDelegator().flushWriters(); + Assertions.assertTrue(manifest.hasFile("/" + README_FILENAME)); + String readme = manifest.getFileString("/" + README_FILENAME).get(); + assertThat(readme, containsString("SDK for JavaScript Example Client")); + } + + @Test + void expectDefaultFileWrittenForServerSDK() { + settings.setArtifactType(TypeScriptSettings.ArtifactType.SSDK); + context = createContext(); + new DefaultReadmeGenerator().customize(context); + context.writerDelegator().flushWriters(); + Assertions.assertTrue(manifest.hasFile("/" + README_FILENAME)); + String readme = manifest.getFileString("/" + README_FILENAME).get(); + assertThat(readme, containsString("JavaScript Server SDK for Example")); + } +}