Skip to content

Commit 03c0e41

Browse files
authored
feat: implement prompt framework validate/transform (#183)
1 parent 9de5e7e commit 03c0e41

File tree

20 files changed

+309
-423
lines changed

20 files changed

+309
-423
lines changed

src/actions/api/transform.ts

Lines changed: 29 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,20 @@
1-
import fsExtra from "fs-extra";
21
import { DirectoryPath } from "../../types/file/directoryPath.js";
32
import { ActionResult } from "../action-result.js";
43
import { ApiTransformPrompts } from "../../prompts/api/transform.js";
5-
import { DestinationFormats } from "../../types/api/transform.js";
6-
import { getFileNameFromPath } from "../../utils/utils.js";
7-
import { FileService } from "../../infrastructure/file-service.js";
8-
import { validateFileInputParams } from "../../infrastructure/api-utils.js";
94
import { withDirPath } from "../../infrastructure/tmp-extensions.js";
10-
import { TransformationService } from "../../infrastructure/services/transform-service.js";
11-
import { FilePath } from "../../types/file/filePath.js";
12-
import { FileName } from "../../types/file/fileName.js";
13-
import { Result } from "../../types/common/result.js";
14-
import { ApiValidationSummary } from "@apimatic/sdk";
5+
import { TransformationService } from "../../infrastructure/services/transformation-service.js";
6+
import { ExportFormats } from "@apimatic/sdk";
157
import { ApiValidatePrompts } from "../../prompts/api/validate.js";
168
import { CommandMetadata } from "../../types/common/command-metadata.js";
17-
import { getValidFormat } from "../../controllers/api/transform.js";
9+
import { TransformContext } from "../../types/transform-context.js";
10+
import { ResourceInput } from "../../types/file/resource-input.js";
11+
import { ResourceContext } from "../../types/resource-context.js";
1812

19-
export interface TransformationResultData {
20-
stream: NodeJS.ReadableStream;
21-
apiValidationSummary: ApiValidationSummary;
22-
}
2313

2414
export class TransformAction {
2515
private readonly prompts: ApiTransformPrompts = new ApiTransformPrompts();
2616
private readonly validatePrompts: ApiValidatePrompts = new ApiValidatePrompts();
2717
private readonly transformationService: TransformationService = new TransformationService();
28-
private readonly fileService: FileService = new FileService();
2918
private readonly configDir: DirectoryPath;
3019
private readonly commandMetadata: CommandMetadata;
3120
private readonly authKey: string | null;
@@ -37,80 +26,41 @@ export class TransformAction {
3726
}
3827

3928
public readonly execute = async (
40-
format: string,
29+
resourcePath: ResourceInput,
30+
format: ExportFormats,
4131
destination: DirectoryPath,
42-
force: boolean,
43-
file?: FilePath,
44-
url?: string
32+
force: boolean
4533
): Promise<ActionResult> => {
46-
const validationResult = await validateFileInputParams(file, url);
47-
48-
if (!validationResult.isSuccess()) {
49-
// TODO: Render the message here: validationResult.error!
50-
return ActionResult.failed();
51-
}
52-
53-
this.prompts.displayApiTransformationMessage();
54-
55-
const parsedFormat = getValidFormat(format);
56-
57-
const destinationFileExt: string = DestinationFormats[format as keyof typeof DestinationFormats];
58-
const destinationFilePrefix = file ? getFileNameFromPath(file.toString()) : getFileNameFromPath(url || "");
59-
60-
const destinationFileName = `${destinationFilePrefix}_${format}.${destinationFileExt}`;
61-
const destinationFilePath = new FilePath(destination, new FileName(destinationFileName));
62-
63-
if ((await fsExtra.pathExists(destinationFilePath.toString())) && !force) {
64-
// TODO: Render the error message here
65-
// return ActionResult.error(
66-
// `Can't download transformed file to path ${destinationFilePath.toString()}, because it already exists`
67-
// );
68-
69-
return ActionResult.failed();
70-
}
71-
72-
if (!(await fsExtra.pathExists(destination.toString()))) {
73-
await fsExtra.ensureDir(destination.toString());
74-
}
75-
7634
return await withDirPath(async (tempDirectory) => {
77-
let result: Result<TransformationResultData, string>;
35+
const resourceContext = new ResourceContext(tempDirectory);
36+
const specFileDirResult = await resourceContext.resolveTo(resourcePath);
37+
if (specFileDirResult.isErr()){
38+
this.prompts.networkError(specFileDirResult.error);
39+
return ActionResult.failed();
40+
}
41+
const transformContext = new TransformContext(specFileDirResult.value, format, destination);
42+
if (!force && (await transformContext.exists()) && !(await this.prompts.overwriteApi(destination))) {
43+
this.prompts.transformedApiAlreadyExists();
44+
return ActionResult.cancelled();
45+
}
7846

79-
if (file) {
80-
result = await this.transformationService.transformViaFile({
81-
file,
82-
format: parsedFormat,
83-
configDir: this.configDir,
84-
commandMetadata: this.commandMetadata,
85-
authKey: this.authKey
86-
});
87-
} else {
88-
result = await this.transformationService.transformViaUrl({
89-
url: url!,
90-
format: parsedFormat,
47+
const result = await this.prompts.transformApi(
48+
this.transformationService.transformViaFile({
49+
file: specFileDirResult.value,
50+
format: format,
9151
configDir: this.configDir,
9252
commandMetadata: this.commandMetadata,
9353
authKey: this.authKey
94-
});
95-
}
96-
97-
const tempTransformedFilePath = new FilePath(tempDirectory, new FileName(`transformed_${destinationFileName}`));
98-
await this.fileService.writeFile(tempTransformedFilePath, result.value?.stream as NodeJS.ReadableStream);
54+
})
55+
);
9956

100-
if (!result.isSuccess()) {
101-
this.validatePrompts.displayValidationMessages(
102-
result.value?.apiValidationSummary || { warnings: [], errors: [], messages: [] }
103-
);
104-
this.prompts.displayApiTransformationFailureMessage();
105-
106-
// TODO: Render the error message here
107-
//return ActionResult.error(result.error || "An unknown error occurred");
57+
if (result.isErr()) {
58+
this.prompts.logTransformationError(result.error);
10859
return ActionResult.failed();
10960
}
11061

111-
this.prompts.displayApiTransformationSuccessMessage();
112-
this.validatePrompts.displayValidationMessages(result.value!.apiValidationSummary);
113-
62+
await transformContext.save(result.value.stream);
63+
this.validatePrompts.displayValidationMessages(result.value.apiValidationSummary);
11464
return ActionResult.success();
11565
});
11666
};

src/actions/api/validate.ts

Lines changed: 34 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,55 @@ import { DirectoryPath } from "../../types/file/directoryPath.js";
22
import { ActionResult } from "../action-result.js";
33
import { ApiValidatePrompts } from "../../prompts/api/validate.js";
44
import { ValidationService } from "../../infrastructure/services/validation-service.js";
5-
import { FilePath } from "../../types/file/filePath.js";
65
import { ApiValidationSummary } from "@apimatic/sdk";
7-
import { Result } from "../../types/common/result.js";
8-
import { validateFileInputParams } from "../../infrastructure/api-utils.js";
6+
import { Result } from "neverthrow";
97
import { CommandMetadata } from "../../types/common/command-metadata.js";
8+
import { ResourceInput } from "../../types/file/resource-input.js";
9+
import { withDirPath } from "../../infrastructure/tmp-extensions.js";
10+
import { ResourceContext } from "../../types/resource-context.js";
1011

1112
export class ValidateAction {
1213
private readonly prompts: ApiValidatePrompts = new ApiValidatePrompts();
1314
private readonly validationService: ValidationService;
1415
private readonly authKey: string | null;
16+
private readonly commandMetadata: CommandMetadata;
1517

1618
constructor(configDir: DirectoryPath, commandMetadata: CommandMetadata, authKey: string | null = null) {
1719
this.authKey = authKey;
18-
this.validationService = new ValidationService(configDir, commandMetadata);
20+
this.validationService = new ValidationService(configDir);
21+
this.commandMetadata = commandMetadata;
1922
}
2023

21-
public readonly execute = async (file?: FilePath, url?: string): Promise<ActionResult> => {
22-
const validationResult = await validateFileInputParams(file, url);
24+
public readonly execute = async (resourcePath: ResourceInput): Promise<ActionResult> => {
25+
return await withDirPath(async (tempDirectory) => {
2326

24-
if (!validationResult.isSuccess()) {
25-
// TODO: Render the error message here
26-
//return ActionResult.error(validationResult.error!);
27-
return ActionResult.failed();
28-
}
2927

30-
this.prompts.displayValidationStartMessage();
31-
32-
let validationSummaryResult: Result<ApiValidationSummary, string>;
33-
34-
if (file) {
35-
validationSummaryResult = await this.validationService.validateViaFile({
36-
file,
37-
authKey: this.authKey
38-
});
39-
} else {
40-
validationSummaryResult = await this.validationService.validateViaUrl({
41-
url: url!,
42-
authKey: this.authKey
43-
});
44-
}
45-
46-
if (!validationSummaryResult.isSuccess()) {
47-
// TODO: Render the error message here
48-
//return ActionResult.error(validationSummaryResult.error! || "Validation failed with an unknown error");
49-
return ActionResult.failed();
50-
}
51-
52-
const validationSummary = validationSummaryResult.value;
53-
if (!validationSummary?.success) {
54-
this.prompts.displayValidationFailureMessage();
55-
if (validationSummary) {
28+
const resourceContext = new ResourceContext(tempDirectory);
29+
const specFileDirResult = await resourceContext.resolveTo(resourcePath);
30+
if (specFileDirResult.isErr()){
31+
this.prompts.networkError(specFileDirResult.error);
32+
return ActionResult.failed();
33+
}
34+
const validationSummaryResult: Result<ApiValidationSummary, string>= await this.prompts.validateApi(
35+
this.validationService.validateViaFile({
36+
file: specFileDirResult.value,
37+
commandMetadata: this.commandMetadata,
38+
authKey: this.authKey
39+
})
40+
);
41+
42+
if (validationSummaryResult.isErr()) {
43+
this.prompts.logValidationError(validationSummaryResult.error);
44+
return ActionResult.failed();
45+
}
46+
const validationSummary = validationSummaryResult.value;
47+
if (validationSummary?.success) {
5648
this.prompts.displayValidationMessages(validationSummary);
49+
return ActionResult.success();
50+
} else {
51+
this.prompts.displayValidationMessages(validationSummary);
52+
return ActionResult.failed();
5753
}
58-
// TODO: Render the error message here
59-
//return ActionResult.error("Specification file provided is invalid");
60-
return ActionResult.failed();
61-
}
62-
63-
this.prompts.displayValidationSuccessMessage();
64-
this.prompts.displayValidationMessages(validationSummary);
65-
return ActionResult.success();
54+
});
6655
};
6756
}

src/actions/portal/quickstart.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class PortalQuickstartAction {
3939
constructor(configDir: DirectoryPath, commandMetadata: CommandMetadata) {
4040
this.configDir = configDir;
4141
this.commandMetadata = commandMetadata;
42-
this.validationService = new ValidationService(configDir, commandMetadata);
42+
this.validationService = new ValidationService(configDir);
4343
}
4444

4545
public readonly execute = async (): Promise<ActionResult> => {
@@ -127,11 +127,11 @@ export class PortalQuickstartAction {
127127
const inputPath = await this.prompts.specPathPrompt(this.defaultSpecUrl);
128128

129129
const resourceContext = new ResourceContext(tempDirectory);
130-
const result = await resourceContext.resolveTo(inputPath, "spec");
130+
const result = await resourceContext.resolveTo(new UrlPath(inputPath));
131131
if (result.isErr()) {
132132
return err(result.error);
133133
}
134-
return ok(result.value);
134+
return ok(new DirectoryPath(result.value.toString(), "spec"));
135135
}
136136

137137
private async validateSpec(
@@ -149,9 +149,9 @@ export class PortalQuickstartAction {
149149
const specZipFilePath = new FilePath(tempDirectory, new FileName("spec.zip"));
150150
await this.zipService.archive(specDirectory, specZipFilePath);
151151

152-
const validationResult = await this.validationService.validateViaFile({ file: specZipFilePath });
152+
const validationResult = await this.validationService.validateViaFile({ file: specZipFilePath, commandMetadata: this.commandMetadata });
153153
// TODO: Add spinner when refactoring
154-
if (validationResult.isFailed()) {
154+
if (validationResult.isErr()) {
155155
this.prompts.stopProgressIndicator(`Something went wrong while validating your API Definition.`, 1);
156156
return Result.failure(validationResult.error!);
157157
}
@@ -169,11 +169,11 @@ export class PortalQuickstartAction {
169169

170170
// Use default spec...
171171
const resourceContext = new ResourceContext(tempDirectory);
172-
const result = await resourceContext.resolveTo(this.defaultSpecUrl.toString(), "spec");
172+
const result = await resourceContext.resolveTo(this.defaultSpecUrl);
173173
if (result.isErr()) {
174174
return Result.failure(result.error);
175175
}
176-
return Result.success(result.value);
176+
return Result.success(new DirectoryPath(result.value.toString(), "spec"));
177177
}
178178

179179
private async selectLanguages(): Promise<string[]> {

src/commands/api/transform.ts

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,33 @@
11
import { Command, Flags } from "@oclif/core";
22
import { DirectoryPath } from "../../types/file/directoryPath.js";
33
import { FlagsProvider } from "../../types/flags-provider.js";
4-
import { ApiTransformPrompts } from "../../prompts/api/transform.js";
54
import { TransformAction } from "../../actions/api/transform.js";
6-
import { FilePath } from "../../types/file/filePath.js";
7-
import path from "path/win32";
8-
import { FileName } from "../../types/file/fileName.js";
95
import { CommandMetadata } from "../../types/common/command-metadata.js";
10-
import { outro } from "../../prompts/format.js";
6+
import { format, intro, outro } from "../../prompts/format.js";
7+
import { createResourceInput } from "../../types/file/resource-input.js";
8+
import { TransformationFormats } from "../../types/api/transform.js";
9+
import { ExportFormats } from "@apimatic/sdk";
10+
import { err, ok, Result } from "neverthrow";
1111

1212
const DEFAULT_WORKING_DIRECTORY = "./";
1313

1414
export default class Transform extends Command {
15+
static readonly summary = "Transform API specifications between different formats";
16+
1517
static readonly description = `Transform API specifications from one format to another.
1618
Supports multiple formats including OpenAPI/Swagger, RAML, WSDL, and Postman Collections.`;
1719

20+
static readonly cmdTxt = format.cmd("apimatic", "api", "transform");
21+
1822
static examples = [
19-
`apimatic api transform --format=OPENAPI3YAML --file="./specs/sample.json" --destination="D:/"`,
20-
`apimatic api transform --format=RAML --url="https://petstore.swagger.io/v2/swagger.json" --destination="D:/"`
23+
`${Transform.cmdTxt} ${format.flag("format", "OPENAPI3YAML")} ${format.flag(
24+
"file",
25+
"./specs/sample.json"
26+
)} ${format.flag("destination", "./")}`,
27+
`${Transform.cmdTxt} ${format.flag("format", "RAML")} ${format.flag(
28+
"url",
29+
'"https://petstore.swagger.io/v2/swagger.json"'
30+
)} ${format.flag("destination", "./")}`
2131
];
2232

2333
static flags = {
@@ -33,39 +43,53 @@ Supports multiple formats including OpenAPI/Swagger, RAML, WSDL, and Postman Col
3343
}),
3444
destination: Flags.string({
3545
char: "d",
36-
description: "Directory to download the transformed file to",
46+
description: "Directory to save the transformed file to",
3747
default: DEFAULT_WORKING_DIRECTORY
3848
}),
3949
...FlagsProvider.force,
4050
...FlagsProvider.authKey
4151
};
4252

43-
private readonly prompts: ApiTransformPrompts = new ApiTransformPrompts();
44-
4553
async run() {
4654
const {
4755
flags: { format, file, url, destination, force, "auth-key": authKey }
4856
} = await this.parse(Transform);
4957

50-
const destinationDir = new DirectoryPath(destination);
58+
const workingDirectory = new DirectoryPath(destination ?? DEFAULT_WORKING_DIRECTORY);
59+
const transformedApiDirectory = workingDirectory.join("transformations");
60+
const specFile = createResourceInput(file, url);
61+
const parsedFormatResult = this.getValidFormat(format);
5162

52-
let filePath: FilePath | undefined = undefined;
53-
if (file) {
54-
filePath = new FilePath(new DirectoryPath(path.dirname(file)), new FileName(path.basename(file)));
63+
if (parsedFormatResult.isErr()) {
64+
this.error(parsedFormatResult.error);
5565
}
66+
const parsedFormat = parsedFormatResult.value;
67+
5668
const commandMetadata: CommandMetadata = {
5769
commandName: Transform.id,
5870
shell: this.config.shell
59-
}
71+
};
6072

73+
intro("Transform API");
6174
const action = new TransformAction(this.getConfigDir(), commandMetadata, authKey);
62-
63-
const result = await action.execute(format, destinationDir, force, filePath, url);
64-
75+
const result = await action.execute(specFile, parsedFormat, transformedApiDirectory, force);
6576
outro(result);
6677
}
6778

6879
private readonly getConfigDir = () => {
6980
return new DirectoryPath(this.config.configDir);
7081
};
82+
83+
private readonly getValidFormat = (format: string): Result<ExportFormats, string> => {
84+
const key = Object.keys(TransformationFormats).find((value) => value === format) as
85+
| keyof typeof TransformationFormats
86+
| undefined;
87+
if (key) {
88+
const transformationFormat = TransformationFormats[key] as keyof typeof ExportFormats;
89+
return ok(ExportFormats[transformationFormat]);
90+
} else {
91+
const formats = Object.keys(TransformationFormats).join("|");
92+
return err(`Please provide a valid platform, e.g. ${formats}`);
93+
}
94+
};
7195
}

0 commit comments

Comments
 (0)