Skip to content

Commit 87d65bd

Browse files
feat(sdk:generate): update flags and working directory behavior (#130)
1 parent 9ad18ab commit 87d65bd

File tree

14 files changed

+337
-252
lines changed

14 files changed

+337
-252
lines changed

src/actions/portal/generate.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ export class GenerateAction {
3232
): Promise<ActionResult> => {
3333

3434
if (buildDirectory.isEqual(portalDirectory)) {
35-
return ActionResult.error("build directory and portal directory cannot be the same.");
35+
return ActionResult.error(`The build and portal directory cannot be the same: "${portalDirectory}"`);
3636
}
3737

3838
const buildContext = new BuildContext(buildDirectory);
3939
if (!await buildContext.validate()) {
40-
return ActionResult.error("build directory is empty or not valid");
40+
return ActionResult.error(`The build directory is either empty or invalid: "${buildDirectory}"`);
4141
}
4242

4343
const portalContext = new PortalContext(portalDirectory);
@@ -60,12 +60,11 @@ export class GenerateAction {
6060
return ActionResult.error(await this.parseError(response.error!, portalDirectory, tempDirectory));
6161
}
6262

63-
this.prompts.displayPortalGenerationSuccessMessage();
64-
6563
const tempPortalFilePath = new FilePath(tempDirectory, new FileName("portal.zip"));
6664
await this.fileService.writeFile(tempPortalFilePath, <NodeJS.ReadableStream>response.value);
6765

6866
await portalContext.save(tempPortalFilePath, zipPortal);
67+
this.prompts.displayPortalGenerationSuccessMessage();
6968

7069
return ActionResult.success();
7170
});

src/actions/portal/toc/new-toc.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { TocContentParser } from "../../../application/portal/toc/toc-content-pa
99
import { TocEndpoint, TocGroup, TocModel } from "../../../types/toc/toc.js";
1010
import { PortalService } from "../../../infrastructure/services/portal-service.js";
1111
import { DirectoryPath } from "../../../types/file/directoryPath.js";
12+
import { FilePath } from "../../../types/file/filePath.js";
13+
import { FileName } from "../../../types/file/fileName.js";
14+
import { BuildContext } from "../../../types/build-context.js";
1215

1316
export class PortalNewTocAction {
1417
private readonly prompts: PortalNewTocPrompts;
@@ -35,10 +38,10 @@ export class PortalNewTocAction {
3538
): Promise<Result<string, string>> {
3639
try {
3740
const tocDir = await this.getDestinationPath(buildDirectory, tocDirectory);
38-
const tocPath = path.join(tocDir, this.DEFAULT_TOC_FILENAME);
41+
const tocPath = new FilePath(tocDir, new FileName(this.DEFAULT_TOC_FILENAME))
3942
const tocCheckResult = await this.handleExistingToc(tocPath, force);
4043
if (!tocCheckResult.isSuccess()) {
41-
return tocCheckResult;
44+
return Result.cancelled(tocCheckResult.value!);
4245
}
4346

4447
const { endpointGroups, models } = await this.extractSdlComponents(
@@ -58,10 +61,10 @@ export class PortalNewTocAction {
5861
contentGroups
5962
);
6063
const yamlString = this.tocGenerator.transformToYaml(toc);
61-
await this.writeToc(tocPath, yamlString, "utf8");
64+
await this.writeToc(tocPath.toString(), yamlString, "utf8");
6265

6366
this.prompts.displayOutroMessage(tocPath);
64-
return Result.success(tocPath);
67+
return Result.success(tocPath.toString());
6568
} catch (error) {
6669
this.prompts.logError(getMessageInRedColor(`${(error as Error).message}`));
6770
return Result.failure(`❌ An unexpected error occurred while generating the TOC file.`);
@@ -73,7 +76,7 @@ export class PortalNewTocAction {
7376
await fsExtra.writeFile(path, content, encoding);
7477
}
7578

76-
private async handleExistingToc(tocPath: string, force: boolean): Promise<Result<string, string>> {
79+
private async handleExistingToc(tocPath: FilePath, force: boolean): Promise<Result<string, string>> {
7780
const shouldContinue = await this.checkExistingToc(tocPath, force);
7881
if (!shouldContinue) {
7982
return Result.cancelled("Operation was cancelled by the user.");
@@ -117,45 +120,44 @@ export class PortalNewTocAction {
117120
private async extractContentGroups(buildDirectory: DirectoryPath): Promise<TocGroup[]> {
118121
const contentFolderPath = await this.getContentFolderPath(buildDirectory);
119122

120-
if (!(await fsExtra.pathExists(contentFolderPath))) {
123+
if (!(await fsExtra.pathExists(contentFolderPath.toString()))) {
121124
this.prompts.displayInfo(`⚠️ Could not locate the content folder at: ${contentFolderPath}`);
122125
this.prompts.displayInfo("Skipping custom content addition in TOC...");
123126
return [];
124127
}
125128

126-
return await this.contentParser.parseContentFolder(contentFolderPath, contentFolderPath);
129+
return await this.contentParser.parseContentFolder(contentFolderPath.toString(), contentFolderPath.toString());
127130
}
128131

129-
private async getDestinationPath(buildDirectory: DirectoryPath, providedTocDirectory?: DirectoryPath): Promise<string> {
132+
private async getDestinationPath(buildDirectory: DirectoryPath, providedTocDirectory?: DirectoryPath): Promise<DirectoryPath> {
130133
if (providedTocDirectory === undefined) {
131134
const inferredDestination = await this.getContentFolderPath(buildDirectory);
132135
return inferredDestination;
133136
}
134-
return providedTocDirectory.toString();
137+
return providedTocDirectory;
135138
}
136139

137-
private async checkExistingToc(tocPath: string, force: boolean): Promise<boolean> {
138-
if ((await fsExtra.pathExists(tocPath)) && !force) {
139-
return await this.prompts.overwriteExistingTocPrompt();
140+
private async checkExistingToc(tocPath: FilePath, force: boolean): Promise<boolean> {
141+
if ((await fsExtra.pathExists(tocPath.toString())) && !force) {
142+
return await this.prompts.overwriteExistingTocPrompt(tocPath);
140143
}
141144
return true;
142145
}
143146

144-
private async getContentFolderPath(buildDirectory: DirectoryPath): Promise<string> {
145-
const buildFilePath = path.join(buildDirectory.toString(), this.APIMATIC_BUILD_FILENAME);
146-
const defaultContentFolder = path.join(buildDirectory.toString(), "content");
147+
private async getContentFolderPath(buildDirectory: DirectoryPath): Promise<DirectoryPath> {
148+
const buildContext = new BuildContext(buildDirectory);
149+
const defaultContentFolder = buildDirectory.join("content");
147150

148-
if (!(await fsExtra.pathExists(buildFilePath))) {
151+
if (!(await buildContext.validate())) {
149152
return defaultContentFolder;
150153
}
151154

152155
try {
153-
const buildConfig = await fsExtra.readJson(buildFilePath, "utf8");
154-
156+
const buildConfig = await buildContext.getBuildFileContents();
155157
if (buildConfig.generatePortal?.contentFolder == null) {
156158
return defaultContentFolder;
157159
}
158-
return path.join(buildDirectory.toString(), buildConfig.generatePortal.contentFolder, "content");
160+
return buildDirectory.join(buildConfig.generatePortal.contentFolder).join("content");
159161
} catch {
160162
return defaultContentFolder;
161163
}

src/actions/sdk/generate.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { ZipService } from "../../infrastructure/zip-service.js";
2+
import { FileService } from "../../infrastructure/file-service.js";
3+
import { PortalService } from "../../infrastructure/services/portal-service.js";
4+
import { DirectoryPath } from "../../types/file/directoryPath.js";
5+
import { FilePath } from "../../types/file/filePath.js";
6+
import { FileName } from "../../types/file/fileName.js";
7+
import { ActionResult } from "../action-result.js";
8+
import { withDirPath } from "../../infrastructure/tmp-extensions.js";
9+
import { SdkContext } from "../../types/sdk-context.js";
10+
import { Platforms } from "@apimatic/sdk";
11+
import { SpecContext } from "../../types/spec-context.js";
12+
import { SdkGeneratePrompts } from "../../prompts/sdk/generate.js";
13+
14+
15+
export class GenerateAction {
16+
private readonly prompts: SdkGeneratePrompts = new SdkGeneratePrompts();
17+
private readonly zipArchiver: ZipService = new ZipService();
18+
private readonly fileService: FileService = new FileService();
19+
private readonly portalService: PortalService = new PortalService();
20+
private readonly configDir: DirectoryPath;
21+
private readonly authKey: string | null;
22+
23+
constructor(configDir: DirectoryPath, authKey: string | null = null) {
24+
this.configDir = configDir;
25+
this.authKey = authKey;
26+
}
27+
28+
public readonly execute = async (
29+
specDirectory: DirectoryPath,
30+
sdkDirectory: DirectoryPath,
31+
platform: Platforms,
32+
force: boolean,
33+
zipSdk: boolean
34+
): Promise<ActionResult> => {
35+
36+
if (specDirectory.isEqual(sdkDirectory)) {
37+
return ActionResult.error(`The spec directory and sdk directory cannot be the same: "${specDirectory}"`);
38+
}
39+
40+
const specContext = new SpecContext(specDirectory);
41+
if (!await specContext.validate()) {
42+
return ActionResult.error(`The spec directory is either empty or invalid: "${specDirectory}"`);
43+
}
44+
45+
const sdkContext = new SdkContext(sdkDirectory, platform);
46+
if (!force && (await sdkContext.exists()) && !(await this.prompts.overwriteSdk(sdkDirectory))) {
47+
return ActionResult.error(
48+
"Please enter a different destination folder or remove the existing files and try again."
49+
);
50+
}
51+
52+
return await withDirPath(async (tempDirectory) => {
53+
this.prompts.displaySdkGenerationMessage();
54+
55+
const specZipPath = new FilePath(tempDirectory, new FileName("spec.zip"));
56+
await this.zipArchiver.archive(specDirectory, specZipPath);
57+
58+
const response = await this.portalService.generateSdk(specZipPath, platform, this.configDir, this.authKey);
59+
60+
if (!response.isSuccess()) {
61+
this.prompts.displaySdkGenerationErrorMessage();
62+
return ActionResult.error(response.error!);
63+
}
64+
65+
const tempSdkFilePath = new FilePath(tempDirectory, new FileName("sdk.zip"));
66+
await this.fileService.writeFile(tempSdkFilePath, <NodeJS.ReadableStream>response.value);
67+
68+
await sdkContext.save(tempSdkFilePath, zipSdk);
69+
this.prompts.displaySdkGenerationSuccessMessage();
70+
71+
return ActionResult.success();
72+
});
73+
}
74+
}

src/commands/sdk/generate.ts

Lines changed: 55 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,82 @@
1-
import * as path from "path";
2-
import fsExtra from "fs-extra";
3-
41
import { Command, Flags } from "@oclif/core";
5-
import { SDKClient } from "../../client-utils/sdk-client.js";
6-
import { ApiError, Client, CodeGenerationExternalApisController } from "@apimatic/sdk";
2+
import { DirectoryPath } from "../../types/file/directoryPath.js";
3+
import { FlagsProvider } from "../../types/flags-provider.js";
4+
import { SdkGeneratePrompts } from "../../prompts/sdk/generate.js";
5+
import { GenerateAction } from "../../actions/sdk/generate.js";
6+
import { Platforms } from "@apimatic/sdk";
7+
import { LanguagePlatform } from "../../types/sdk/generate.js";
78

8-
import { replaceHTML, isJSONParsable, getFileNameFromPath } from "../../utils/utils.js";
9-
import { getSDKGenerationId, downloadGeneratedSDK } from "../../controllers/sdk/generate.js";
10-
import { DownloadSDKParams, SDKGenerateUnprocessableError } from "../../types/sdk/generate.js";
11-
import { AuthenticationError } from "../../types/utils.js";
9+
const DEFAULT_WORKING_DIRECTORY = "./";
1210

1311
export default class SdkGenerate extends Command {
14-
static description = "Generate SDK for your APIs";
12+
static description = "Generates SDK for your API";
1513
static flags = {
1614
platform: Flags.string({
17-
parse: async (input) => input.toUpperCase(),
1815
required: true,
19-
description: `language platform for sdk
20-
Simple: CSHARP|JAVA|PYTHON|RUBY|PHP|TYPESCRIPT|GO
21-
Legacy: CS_NET_STANDARD_LIB|JAVA_ECLIPSE_JRE_LIB|PHP_GENERIC_LIB_V2|PYTHON_GENERIC_LIB|RUBY_GENERIC_LIB|TS_GENERIC_LIB|GO_GENERIC_LIB`
22-
}),
23-
file: Flags.string({
24-
parse: async (input) => path.resolve(input),
25-
default: "",
26-
description: "path to the API specification to generate SDKs for"
16+
options: Object.values(LanguagePlatform).map(p => p.toString()),
17+
description: `language platform for sdk`
2718
}),
28-
url: Flags.string({
29-
default: "",
30-
description:
31-
"URL to the API specification to generate SDKs for. Can be used in place of the --file option if the API specification is publicly available."
19+
spec: Flags.string({
20+
description: "path to the folder containing the API specification file.",
21+
default: "./build/spec"
3222
}),
3323
destination: Flags.string({
34-
parse: async (input) => path.resolve(input),
35-
default: path.resolve("./"),
36-
description: "directory to download the generated SDK to"
24+
description: "[default: ./sdk] path where the sdk will be generated."
3725
}),
38-
force: Flags.boolean({
39-
char: "f",
26+
...FlagsProvider.force,
27+
zip: Flags.boolean({
4028
default: false,
41-
description: "overwrite if an SDK already exists in the destination"
29+
description: "download the generated SDK as a .zip archive"
4230
}),
43-
zip: Flags.boolean({ default: false, description: "download the generated SDK as a .zip archive" }),
44-
"auth-key": Flags.string({
45-
default: "",
46-
description: "override current authentication state with an authentication key"
47-
})
31+
...FlagsProvider["auth-key"]
4832
};
4933

5034
static examples = [
51-
`$ apimatic sdk:generate --platform="CSHARP" --file="./specs/sample.json"`,
52-
`$ apimatic sdk:generate --platform="CSHARP" --url=https://petstore.swagger.io/v2/swagger.json`
35+
`$ apimatic sdk:generate --platform="java"`,
36+
`$ apimatic sdk:generate --platform="csharp" --spec="./build/spec"`
5337
];
5438

39+
private readonly prompts: SdkGeneratePrompts = new SdkGeneratePrompts();
40+
5541
async run() {
56-
const { flags } = await this.parse(SdkGenerate);
57-
const zip = flags.zip;
58-
const fileName = flags.file ? getFileNameFromPath(flags.file) : getFileNameFromPath(flags.url);
59-
const sdkFolderPath: string = path.join(flags.destination, `${fileName}_sdk_${flags.platform}`.toLowerCase());
60-
const zippedSDKPath: string = path.join(flags.destination, `${fileName}_sdk_${flags.platform}.zip`.toLowerCase());
42+
const { flags: { platform, spec, destination, force, zip: zipSdk, "auth-key": authKey } } = await this.parse(SdkGenerate);
6143

62-
// Check if at destination, SDK already exists and throw error if force flag is not set for both zip and extracted
63-
if (fsExtra.existsSync(sdkFolderPath) && !flags.force && !zip) {
64-
throw new Error(`Can't download SDK to path ${sdkFolderPath}, because it already exists`);
65-
} else if (fsExtra.existsSync(zippedSDKPath) && !flags.force && zip) {
66-
throw new Error(`Can't download SDK to path ${zippedSDKPath}, because it already exists`);
67-
}
44+
const workingDirectory = new DirectoryPath(DEFAULT_WORKING_DIRECTORY);
45+
const specDirectory = new DirectoryPath(spec);
6846

69-
try {
70-
if (!(await fsExtra.pathExists(path.resolve(flags.destination)))) {
71-
throw new Error(`Destination path ${flags.destination} does not exist`);
72-
} else if (!(await fsExtra.pathExists(path.resolve(flags.file)))) {
73-
throw new Error(`Specification file ${flags.file} does not exist`);
74-
}
47+
const sdkPlatform = this.convertSimplePlatformToPlatform(platform as LanguagePlatform);
48+
const sdkDirectory = destination ? new DirectoryPath(destination) : workingDirectory.join("sdk").join(sdkPlatform);
7549

76-
const overrideAuthKey = flags["auth-key"] ? flags["auth-key"] : null;
77-
const client: Client = await SDKClient.getInstance().getClient(overrideAuthKey, this.config.configDir);
78-
const sdkGenerationController: CodeGenerationExternalApisController = new CodeGenerationExternalApisController(
79-
client
80-
);
50+
var action = new GenerateAction(this.getConfigDir(), authKey);
51+
const result = await action.execute(specDirectory, sdkDirectory, sdkPlatform, force, zipSdk);
52+
result.mapAll(
53+
() => this.prompts.displayOutroMessage(sdkDirectory),
54+
(message) => this.prompts.logError(message)
55+
);
56+
}
8157

82-
// Get generation id for the specification and platform
83-
const codeGenId: string = await getSDKGenerationId(flags, sdkGenerationController);
58+
private getConfigDir = () => {
59+
return new DirectoryPath(this.config.configDir);
60+
};
8461

85-
// If user wanted to download the SDK as well
86-
const sdkDownloadParams: DownloadSDKParams = {
87-
codeGenId,
88-
zippedSDKPath,
89-
sdkFolderPath,
90-
zip
91-
};
92-
const sdkPath: string = await downloadGeneratedSDK(sdkDownloadParams, sdkGenerationController);
93-
this.log(`Success! Your SDK is located at ${sdkPath}`);
94-
} catch (error) {
95-
if ((error as ApiError).result) {
96-
const apiError = error as ApiError;
97-
const result = apiError.result as SDKGenerateUnprocessableError;
98-
if (apiError.statusCode === 400 && isJSONParsable(result.message)) {
99-
const errors = JSON.parse(result.message);
100-
if (Array.isArray(errors.Errors) && apiError.statusCode === 400) {
101-
this.error(replaceHTML(`${JSON.parse(result.message).Errors[0]}`));
102-
}
103-
} else if (apiError.statusCode === 401 && apiError.body && typeof apiError.body === "string") {
104-
this.error("You are not authorized to perform this action");
105-
} else if (
106-
apiError.statusCode === 500 &&
107-
apiError.body &&
108-
typeof apiError.body === "string" &&
109-
isJSONParsable(apiError.body)
110-
) {
111-
this.error(JSON.parse(apiError.body).message);
112-
} else if (
113-
apiError.statusCode === 422 &&
114-
apiError.body &&
115-
typeof apiError.body === "string" &&
116-
isJSONParsable(apiError.body)
117-
) {
118-
this.error(JSON.parse(apiError.body)["dto.Url"][0]);
119-
} else {
120-
this.error(replaceHTML(result.message));
121-
}
122-
} else if ((error as AuthenticationError).statusCode === 401) {
123-
this.error("You are not authorized to perform this action");
124-
} else if (
125-
(error as AuthenticationError).statusCode === 402 &&
126-
(error as AuthenticationError).body &&
127-
typeof (error as AuthenticationError).body === "string"
128-
) {
129-
this.error(replaceHTML((error as AuthenticationError).body));
130-
} else {
131-
this.error(`${(error as Error).message}`);
132-
}
62+
private convertSimplePlatformToPlatform(languagePlatform: LanguagePlatform): Platforms {
63+
switch (languagePlatform) {
64+
case LanguagePlatform.CSHARP:
65+
return Platforms.CsNetStandardLib;
66+
case LanguagePlatform.JAVA:
67+
return Platforms.JavaEclipseJreLib;
68+
case LanguagePlatform.PHP:
69+
return Platforms.PhpGenericLibV2;
70+
case LanguagePlatform.PYTHON:
71+
return Platforms.PythonGenericLib;
72+
case LanguagePlatform.RUBY:
73+
return Platforms.RubyGenericLib;
74+
case LanguagePlatform.TYPESCRIPT:
75+
return Platforms.TsGenericLib;
76+
case LanguagePlatform.GO:
77+
return Platforms.GoGenericLib;
78+
default:
79+
throw new Error(`Unknown LanguagePlatform: ${languagePlatform}`);
13380
}
13481
}
13582
}

0 commit comments

Comments
 (0)