Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/actions/portal/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ export class GenerateAction {
): Promise<ActionResult> => {

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

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

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

this.prompts.displayPortalGenerationSuccessMessage();

const tempPortalFilePath = new FilePath(tempDirectory, new FileName("portal.zip"));
await this.fileService.writeFile(tempPortalFilePath, <NodeJS.ReadableStream>response.value);

await portalContext.save(tempPortalFilePath, zipPortal);
this.prompts.displayPortalGenerationSuccessMessage();

return ActionResult.success();
});
Expand Down
40 changes: 21 additions & 19 deletions src/actions/portal/toc/new-toc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { TocContentParser } from "../../../application/portal/toc/toc-content-pa
import { TocEndpoint, TocGroup, TocModel } from "../../../types/toc/toc.js";
import { PortalService } from "../../../infrastructure/services/portal-service.js";
import { DirectoryPath } from "../../../types/file/directoryPath.js";
import { FilePath } from "../../../types/file/filePath.js";
import { FileName } from "../../../types/file/fileName.js";
import { BuildContext } from "../../../types/build-context.js";

export class PortalNewTocAction {
private readonly prompts: PortalNewTocPrompts;
Expand All @@ -35,10 +38,10 @@ export class PortalNewTocAction {
): Promise<Result<string, string>> {
try {
const tocDir = await this.getDestinationPath(buildDirectory, tocDirectory);
const tocPath = path.join(tocDir, this.DEFAULT_TOC_FILENAME);
const tocPath = new FilePath(tocDir, new FileName(this.DEFAULT_TOC_FILENAME))
const tocCheckResult = await this.handleExistingToc(tocPath, force);
if (!tocCheckResult.isSuccess()) {
return tocCheckResult;
return Result.cancelled(tocCheckResult.value!);
}

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

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

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

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

return await this.contentParser.parseContentFolder(contentFolderPath, contentFolderPath);
return await this.contentParser.parseContentFolder(contentFolderPath.toString(), contentFolderPath.toString());
}

private async getDestinationPath(buildDirectory: DirectoryPath, providedTocDirectory?: DirectoryPath): Promise<string> {
private async getDestinationPath(buildDirectory: DirectoryPath, providedTocDirectory?: DirectoryPath): Promise<DirectoryPath> {
if (providedTocDirectory === undefined) {
const inferredDestination = await this.getContentFolderPath(buildDirectory);
return inferredDestination;
}
return providedTocDirectory.toString();
return providedTocDirectory;
}

private async checkExistingToc(tocPath: string, force: boolean): Promise<boolean> {
if ((await fsExtra.pathExists(tocPath)) && !force) {
return await this.prompts.overwriteExistingTocPrompt();
private async checkExistingToc(tocPath: FilePath, force: boolean): Promise<boolean> {
if ((await fsExtra.pathExists(tocPath.toString())) && !force) {
return await this.prompts.overwriteExistingTocPrompt(tocPath);
}
return true;
}

private async getContentFolderPath(buildDirectory: DirectoryPath): Promise<string> {
const buildFilePath = path.join(buildDirectory.toString(), this.APIMATIC_BUILD_FILENAME);
const defaultContentFolder = path.join(buildDirectory.toString(), "content");
private async getContentFolderPath(buildDirectory: DirectoryPath): Promise<DirectoryPath> {
const buildContext = new BuildContext(buildDirectory);
const defaultContentFolder = buildDirectory.join("content");

if (!(await fsExtra.pathExists(buildFilePath))) {
if (!(await buildContext.validate())) {
return defaultContentFolder;
}

try {
const buildConfig = await fsExtra.readJson(buildFilePath, "utf8");

const buildConfig = await buildContext.getBuildFileContents();
if (buildConfig.generatePortal?.contentFolder == null) {
return defaultContentFolder;
}
return path.join(buildDirectory.toString(), buildConfig.generatePortal.contentFolder, "content");
return buildDirectory.join(buildConfig.generatePortal.contentFolder).join("content");
} catch {
return defaultContentFolder;
}
Expand Down
74 changes: 74 additions & 0 deletions src/actions/sdk/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ZipService } from "../../infrastructure/zip-service.js";
import { FileService } from "../../infrastructure/file-service.js";
import { PortalService } from "../../infrastructure/services/portal-service.js";
import { DirectoryPath } from "../../types/file/directoryPath.js";
import { FilePath } from "../../types/file/filePath.js";
import { FileName } from "../../types/file/fileName.js";
import { ActionResult } from "../action-result.js";
import { withDirPath } from "../../infrastructure/tmp-extensions.js";
import { SdkContext } from "../../types/sdk-context.js";
import { Platforms } from "@apimatic/sdk";
import { SpecContext } from "../../types/spec-context.js";
import { SdkGeneratePrompts } from "../../prompts/sdk/generate.js";


export class GenerateAction {
private readonly prompts: SdkGeneratePrompts = new SdkGeneratePrompts();
private readonly zipArchiver: ZipService = new ZipService();
private readonly fileService: FileService = new FileService();
private readonly portalService: PortalService = new PortalService();
private readonly configDir: DirectoryPath;
private readonly authKey: string | null;

constructor(configDir: DirectoryPath, authKey: string | null = null) {
this.configDir = configDir;
this.authKey = authKey;
}

public readonly execute = async (
specDirectory: DirectoryPath,
sdkDirectory: DirectoryPath,
platform: Platforms,
force: boolean,
zipSdk: boolean
): Promise<ActionResult> => {

if (specDirectory.isEqual(sdkDirectory)) {
return ActionResult.error(`The spec directory and sdk directory cannot be the same: "${specDirectory}"`);
}

const specContext = new SpecContext(specDirectory);
if (!await specContext.validate()) {
return ActionResult.error(`The spec directory is either empty or invalid: "${specDirectory}"`);
}

const sdkContext = new SdkContext(sdkDirectory, platform);
if (!force && (await sdkContext.exists()) && !(await this.prompts.overwriteSdk(sdkDirectory))) {
return ActionResult.error(
"Please enter a different destination folder or remove the existing files and try again."
);
}

return await withDirPath(async (tempDirectory) => {
this.prompts.displaySdkGenerationMessage();

const specZipPath = new FilePath(tempDirectory, new FileName("spec.zip"));
await this.zipArchiver.archive(specDirectory, specZipPath);

const response = await this.portalService.generateSdk(specZipPath, platform, this.configDir, this.authKey);

if (!response.isSuccess()) {
this.prompts.displaySdkGenerationErrorMessage();
return ActionResult.error(response.error!);
}

const tempSdkFilePath = new FilePath(tempDirectory, new FileName("sdk.zip"));
await this.fileService.writeFile(tempSdkFilePath, <NodeJS.ReadableStream>response.value);

await sdkContext.save(tempSdkFilePath, zipSdk);
this.prompts.displaySdkGenerationSuccessMessage();

return ActionResult.success();
});
}
}
163 changes: 55 additions & 108 deletions src/commands/sdk/generate.ts
Original file line number Diff line number Diff line change
@@ -1,135 +1,82 @@
import * as path from "path";
import fsExtra from "fs-extra";

import { Command, Flags } from "@oclif/core";
import { SDKClient } from "../../client-utils/sdk-client.js";
import { ApiError, Client, CodeGenerationExternalApisController } from "@apimatic/sdk";
import { DirectoryPath } from "../../types/file/directoryPath.js";
import { FlagsProvider } from "../../types/flags-provider.js";
import { SdkGeneratePrompts } from "../../prompts/sdk/generate.js";
import { GenerateAction } from "../../actions/sdk/generate.js";
import { Platforms } from "@apimatic/sdk";
import { LanguagePlatform } from "../../types/sdk/generate.js";

import { replaceHTML, isJSONParsable, getFileNameFromPath } from "../../utils/utils.js";
import { getSDKGenerationId, downloadGeneratedSDK } from "../../controllers/sdk/generate.js";
import { DownloadSDKParams, SDKGenerateUnprocessableError } from "../../types/sdk/generate.js";
import { AuthenticationError } from "../../types/utils.js";
const DEFAULT_WORKING_DIRECTORY = "./";

export default class SdkGenerate extends Command {
static description = "Generate SDK for your APIs";
static description = "Generates SDK for your API";
static flags = {
platform: Flags.string({
parse: async (input) => input.toUpperCase(),
required: true,
description: `language platform for sdk
Simple: CSHARP|JAVA|PYTHON|RUBY|PHP|TYPESCRIPT|GO
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`
}),
file: Flags.string({
parse: async (input) => path.resolve(input),
default: "",
description: "path to the API specification to generate SDKs for"
options: Object.values(LanguagePlatform).map(p => p.toString()),
description: `language platform for sdk`
}),
url: Flags.string({
default: "",
description:
"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."
spec: Flags.string({
description: "path to the folder containing the API specification file.",
default: "./build/spec"
}),
destination: Flags.string({
parse: async (input) => path.resolve(input),
default: path.resolve("./"),
description: "directory to download the generated SDK to"
description: "[default: ./sdk] path where the sdk will be generated."
}),
force: Flags.boolean({
char: "f",
...FlagsProvider.force,
zip: Flags.boolean({
default: false,
description: "overwrite if an SDK already exists in the destination"
description: "download the generated SDK as a .zip archive"
}),
zip: Flags.boolean({ default: false, description: "download the generated SDK as a .zip archive" }),
"auth-key": Flags.string({
default: "",
description: "override current authentication state with an authentication key"
})
...FlagsProvider["auth-key"]
};

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

private readonly prompts: SdkGeneratePrompts = new SdkGeneratePrompts();

async run() {
const { flags } = await this.parse(SdkGenerate);
const zip = flags.zip;
const fileName = flags.file ? getFileNameFromPath(flags.file) : getFileNameFromPath(flags.url);
const sdkFolderPath: string = path.join(flags.destination, `${fileName}_sdk_${flags.platform}`.toLowerCase());
const zippedSDKPath: string = path.join(flags.destination, `${fileName}_sdk_${flags.platform}.zip`.toLowerCase());
const { flags: { platform, spec, destination, force, zip: zipSdk, "auth-key": authKey } } = await this.parse(SdkGenerate);

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

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

const overrideAuthKey = flags["auth-key"] ? flags["auth-key"] : null;
const client: Client = await SDKClient.getInstance().getClient(overrideAuthKey, this.config.configDir);
const sdkGenerationController: CodeGenerationExternalApisController = new CodeGenerationExternalApisController(
client
);
var action = new GenerateAction(this.getConfigDir(), authKey);
const result = await action.execute(specDirectory, sdkDirectory, sdkPlatform, force, zipSdk);
result.mapAll(
() => this.prompts.displayOutroMessage(sdkDirectory),
(message) => this.prompts.logError(message)
);
}

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

// If user wanted to download the SDK as well
const sdkDownloadParams: DownloadSDKParams = {
codeGenId,
zippedSDKPath,
sdkFolderPath,
zip
};
const sdkPath: string = await downloadGeneratedSDK(sdkDownloadParams, sdkGenerationController);
this.log(`Success! Your SDK is located at ${sdkPath}`);
} catch (error) {
if ((error as ApiError).result) {
const apiError = error as ApiError;
const result = apiError.result as SDKGenerateUnprocessableError;
if (apiError.statusCode === 400 && isJSONParsable(result.message)) {
const errors = JSON.parse(result.message);
if (Array.isArray(errors.Errors) && apiError.statusCode === 400) {
this.error(replaceHTML(`${JSON.parse(result.message).Errors[0]}`));
}
} else if (apiError.statusCode === 401 && apiError.body && typeof apiError.body === "string") {
this.error("You are not authorized to perform this action");
} else if (
apiError.statusCode === 500 &&
apiError.body &&
typeof apiError.body === "string" &&
isJSONParsable(apiError.body)
) {
this.error(JSON.parse(apiError.body).message);
} else if (
apiError.statusCode === 422 &&
apiError.body &&
typeof apiError.body === "string" &&
isJSONParsable(apiError.body)
) {
this.error(JSON.parse(apiError.body)["dto.Url"][0]);
} else {
this.error(replaceHTML(result.message));
}
} else if ((error as AuthenticationError).statusCode === 401) {
this.error("You are not authorized to perform this action");
} else if (
(error as AuthenticationError).statusCode === 402 &&
(error as AuthenticationError).body &&
typeof (error as AuthenticationError).body === "string"
) {
this.error(replaceHTML((error as AuthenticationError).body));
} else {
this.error(`${(error as Error).message}`);
}
private convertSimplePlatformToPlatform(languagePlatform: LanguagePlatform): Platforms {
switch (languagePlatform) {
case LanguagePlatform.CSHARP:
return Platforms.CsNetStandardLib;
case LanguagePlatform.JAVA:
return Platforms.JavaEclipseJreLib;
case LanguagePlatform.PHP:
return Platforms.PhpGenericLibV2;
case LanguagePlatform.PYTHON:
return Platforms.PythonGenericLib;
case LanguagePlatform.RUBY:
return Platforms.RubyGenericLib;
case LanguagePlatform.TYPESCRIPT:
return Platforms.TsGenericLib;
case LanguagePlatform.GO:
return Platforms.GoGenericLib;
default:
throw new Error(`Unknown LanguagePlatform: ${languagePlatform}`);
}
}
}
Loading