Skip to content

Commit ea8e088

Browse files
feat: update sdk generate to accept build input instead of a spec file (#243)
What was Added? - Uses the new SDK generation endpoint that accepts a build as input rather than a spec file. - Updates the SDK Generate command to accept a build as parameter. - Modifies the SDK Quickstart command's functionality to create the build in the src directory before generating the SDK. --------- Co-authored-by: Ayesha <88117894+Ayeshas09@users.noreply.github.com>
1 parent 49386d4 commit ea8e088

12 files changed

Lines changed: 4225 additions & 3018 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ $ npm install -g @apimatic/cli
2424
$ apimatic COMMAND
2525
running command...
2626
$ apimatic (--version)
27-
@apimatic/cli/1.1.0-beta.6 win32-x64 node-v23.4.0
27+
@apimatic/cli/1.1.0-beta.7 win32-x64 node-v23.4.0
2828
$ apimatic --help [COMMAND]
2929
USAGE
3030
$ apimatic COMMAND
@@ -411,16 +411,17 @@ Generate an SDK for your API
411411

412412
```
413413
USAGE
414-
$ apimatic sdk generate -l csharp|java|php|python|ruby|typescript|go [--spec <value>] [-d <value>] [-f] [--zip]
415-
[-k <value>]
414+
$ apimatic sdk generate -l csharp|java|php|python|ruby|typescript|go [-i <value>] [-d <value>] [-f] [--zip] [-k
415+
<value>]
416416
417417
FLAGS
418418
-d, --destination=<value> directory where the SDK will be generated
419419
-f, --force overwrite changes without asking for user consent.
420+
-i, --input=<value> [default: ./] path to the parent directory containing the 'src' directory, which includes
421+
API specifications and configuration files.
420422
-k, --auth-key=<value> override current authentication state with an authentication key.
421423
-l, --language=<option> (required) programming language for SDK generation
422424
<options: csharp|java|php|python|ruby|typescript|go>
423-
--spec=<value> [default: ./src/spec] path to the folder containing the API specification file
424425
--zip download the generated SDK as a .zip archive
425426
426427
DESCRIPTION
@@ -432,7 +433,7 @@ DESCRIPTION
432433
EXAMPLES
433434
apimatic sdk generate --language=java
434435
435-
apimatic sdk generate --language=csharp --spec=./src/spec
436+
apimatic sdk generate --language=csharp --input=./
436437
437438
apimatic sdk generate --language=python --destination=./sdk --zip
438439
```

package-lock.json

Lines changed: 3989 additions & 2892 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"test": "tsx node_modules/mocha/bin/_mocha --forbid-only \"test/**/*.test.ts\" --timeout 99999"
4848
},
4949
"dependencies": {
50-
"@apimatic/sdk": "^0.2.0-alpha.6",
50+
"@apimatic/sdk": "^0.2.0-alpha.7",
5151
"@clack/prompts": "1.0.0-alpha.1",
5252
"@oclif/core": "^4.2.8",
5353
"@oclif/plugin-autocomplete": "^3.2.24",

src/actions/sdk/generate.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { DirectoryPath } from "../../types/file/directoryPath.js";
33
import { ActionResult } from "../action-result.js";
44
import { withDirPath } from "../../infrastructure/tmp-extensions.js";
55
import { SdkContext } from "../../types/sdk-context.js";
6-
import { SpecContext } from "../../types/spec-context.js";
6+
import { VersionedBuildContext } from "../../types/versioned-build-context.js";
77
import { SdkGeneratePrompts } from "../../prompts/sdk/generate.js";
88
import { CommandMetadata } from "../../types/common/command-metadata.js";
99
import { TempContext } from "../../types/temp-context.js";
1010
import { Language } from "../../types/sdk/generate.js";
11+
import { BuildContext } from "../../types/build-context.js";
1112

1213
export class GenerateAction {
1314
private readonly prompts: SdkGeneratePrompts = new SdkGeneratePrompts();
@@ -23,20 +24,31 @@ export class GenerateAction {
2324
}
2425

2526
public readonly execute = async (
26-
specDirectory: DirectoryPath,
27+
buildDirectory: DirectoryPath,
2728
sdkDirectory: DirectoryPath,
2829
language: Language,
2930
force: boolean,
3031
zipSdk: boolean
3132
): Promise<ActionResult> => {
32-
if (specDirectory.isEqual(sdkDirectory)) {
33-
this.prompts.sameSpecAndSdkDir(specDirectory);
33+
if (buildDirectory.isEqual(sdkDirectory)) {
34+
this.prompts.sameBuildAndSdkDir(buildDirectory);
3435
return ActionResult.failed();
3536
}
3637

37-
const specContext = new SpecContext(specDirectory);
38-
if (!(await specContext.validate())) {
39-
this.prompts.invalidSpecDirectory(specDirectory);
38+
const versionedBuildContext = new VersionedBuildContext(buildDirectory);
39+
if (await versionedBuildContext.exists()) {
40+
const resolvedDirectory = await versionedBuildContext.getResolvedBuildDirectory();
41+
if (!resolvedDirectory) {
42+
this.prompts.versionedBuildEmpty();
43+
return ActionResult.failed();
44+
}
45+
buildDirectory = resolvedDirectory;
46+
this.prompts.versionedBuild(versionedBuildContext.getRelativePath(resolvedDirectory));
47+
}
48+
49+
const buildContext = new BuildContext(buildDirectory);
50+
if (!(await buildContext.validate())) {
51+
this.prompts.srcDirectoryEmpty(buildDirectory);
4052
return ActionResult.failed();
4153
}
4254

@@ -48,15 +60,15 @@ export class GenerateAction {
4860

4961
return await withDirPath(async (tempDirectory) => {
5062
const tempContext = new TempContext(tempDirectory);
51-
const specZipPath = await tempContext.zip(specDirectory);
63+
const buildZipPath = await tempContext.zip(buildDirectory);
5264

5365
const response = await this.prompts.generateSDK(
54-
this.portalService.generateSdk(specZipPath, language, this.configDir, this.commandMetadata, this.authKey)
66+
this.portalService.generateSdk(buildZipPath, language, this.configDir, this.commandMetadata, this.authKey)
5567
);
5668

5769
// TODO: this should be service error
5870
if (response.isErr()) {
59-
this.prompts.logGenerationError(response.error);
71+
this.prompts.sdkGenerationServiceError(response.error);
6072
return ActionResult.failed();
6173
}
6274

src/actions/sdk/quickstart.ts

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,9 @@ import { LauncherService } from '../../infrastructure/launcher-service.js';
1717
import { ZipService } from '../../infrastructure/zip-service.js';
1818
import { FileName } from '../../types/file/fileName.js';
1919
import { FeaturesToRemove, ValidationService } from '../../infrastructure/services/validation-service.js';
20-
21-
const defaultSpecUrl = new UrlPath(
22-
`https://raw.githubusercontent.com/apimatic/sample-docs-as-code-portal/refs/heads/master/src/spec/openapi.json`
23-
);
24-
const metadataFileUrl = new UrlPath(
25-
`https://raw.githubusercontent.com/apimatic/sample-docs-as-code-portal/refs/heads/master/src/spec/APIMATIC-META.json`
26-
);
20+
import { BuildContext } from '../../types/build-context.js';
21+
import { TempContext } from '../../types/temp-context.js';
22+
import { getLanguagesConfig } from '../../types/build/build.js';
2723

2824
export class SdkQuickstartAction {
2925
private readonly prompts = new SdkQuickstartPrompts();
@@ -32,6 +28,13 @@ export class SdkQuickstartAction {
3228
private readonly launcherService = new LauncherService();
3329
private readonly zipService = new ZipService();
3430
private readonly validationService = new ValidationService(this.configDir);
31+
private readonly buildFileUrl = new UrlPath(
32+
`https://github.com/apimatic/sample-docs-as-code-portal/archive/refs/heads/master.zip`
33+
);
34+
private readonly defaultSpecUrl = new UrlPath(
35+
`https://raw.githubusercontent.com/apimatic/sample-docs-as-code-portal/refs/heads/master/src/spec/openapi.json`
36+
);
37+
private readonly repositoryFolderName = 'sample-docs-as-code-portal-master/src' as const;
3538

3639
constructor(private readonly configDir: DirectoryPath, private readonly commandMetadata: CommandMetadata) {}
3740

@@ -50,7 +53,7 @@ export class SdkQuickstartAction {
5053

5154
let specPath: FilePath | undefined;
5255
while (!specPath) {
53-
const inputPath = await this.prompts.specPathPrompt(defaultSpecUrl);
56+
const inputPath = await this.prompts.specPathPrompt(this.defaultSpecUrl);
5457
if (!inputPath) {
5558
this.prompts.noSpecSpecified();
5659
return ActionResult.cancelled();
@@ -89,7 +92,7 @@ export class SdkQuickstartAction {
8992
return ActionResult.cancelled();
9093
}
9194
const downloadFileResult = await this.prompts.downloadSpecFile(
92-
this.fileDownloadService.downloadFile(defaultSpecUrl)
95+
this.fileDownloadService.downloadFile(this.defaultSpecUrl)
9396
);
9497
if (downloadFileResult.isErr()) {
9598
this.prompts.serviceError(downloadFileResult.error);
@@ -152,35 +155,39 @@ export class SdkQuickstartAction {
152155
break;
153156
}
154157

155-
// Setup source directory with the spec folder
156-
const apimaticMetaFile = await this.prompts.downloadMetadataFile(
157-
this.fileDownloadService.downloadFile(metadataFileUrl)
158+
// Setup source directory with the build structure
159+
const masterBuildFile = await this.prompts.downloadBuildDirectory(
160+
this.fileDownloadService.downloadFile(this.buildFileUrl)
158161
);
159-
if (apimaticMetaFile.isErr()) {
160-
this.prompts.serviceError(apimaticMetaFile.error);
162+
if (masterBuildFile.isErr()) {
163+
this.prompts.serviceError(masterBuildFile.error);
161164
return ActionResult.failed();
162165
}
163-
const tempSpecDirectory = tempDirectory.join('spec');
164-
await this.fileService.createDirectoryIfNotExists(tempSpecDirectory);
165-
const metadataFilePath = new FilePath(tempSpecDirectory, apimaticMetaFile.value.filename);
166-
await this.fileService.writeFile(metadataFilePath, apimaticMetaFile.value.stream);
167-
168-
if (await this.fileService.isZipFile(specPath)) {
169-
await this.zipService.unArchive(specPath, tempSpecDirectory);
170-
} else {
171-
await this.fileService.copyToDir(specPath, tempSpecDirectory);
172-
}
166+
const tempContext = new TempContext(tempDirectory);
167+
const masterBuildFilePath = await tempContext.save(masterBuildFile.value.stream);
168+
await this.zipService.unArchive(masterBuildFilePath, tempDirectory);
169+
const extractedFolder = tempDirectory.join(this.repositoryFolderName);
170+
171+
const tempBuildContext = new BuildContext(extractedFolder);
172+
await tempBuildContext.deleteWorkflowDir();
173+
174+
const buildFile = await tempBuildContext.getBuildFileContents();
175+
buildFile.generatePortal!.languageConfig = getLanguagesConfig([language]);
176+
await tempBuildContext.updateBuildFileContents(buildFile);
173177

174178
const sourceDirectory = inputDirectory.join('src');
179+
await this.fileService.copyDirectoryContents(extractedFolder, sourceDirectory);
180+
175181
const specDirectory = sourceDirectory.join('spec');
176-
await this.fileService.copyDirectoryContents(tempSpecDirectory, specDirectory);
182+
const specContext = new SpecContext(specDirectory);
183+
await specContext.replaceDefaultSpec(specPath);
177184

178-
const srcDirectoryStructure = await this.fileService.getDirectory(sourceDirectory);
179-
this.prompts.printDirectoryStructure(inputDirectory, srcDirectoryStructure);
185+
const buildDirectoryStructure = await this.fileService.getDirectory(sourceDirectory);
186+
this.prompts.printDirectoryStructure(inputDirectory, buildDirectoryStructure);
180187

181188
const sdkDirectory = inputDirectory.join('sdk');
182189
const sdkGenerateAction = new GenerateAction(this.configDir, this.commandMetadata);
183-
const result = await sdkGenerateAction.execute(specDirectory, sdkDirectory, language as Language, true, false);
190+
const result = await sdkGenerateAction.execute(sourceDirectory, sdkDirectory, language as Language, true, false);
184191
if (result.isFailed()) {
185192
return ActionResult.failed();
186193
}

src/commands/sdk/generate.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ Supports multiple programming languages including Java, C#, Python, JavaScript,
2121
description: "Programming language for SDK generation",
2222
options: Object.values(Language).map((p) => p.valueOf()),
2323
}),
24-
spec: Flags.string({
25-
description: "Path to the folder containing the API specification file",
26-
default: "./src/spec"
27-
}),
24+
...FlagsProvider.input,
2825
destination: Flags.string({
2926
char: "d",
3027
description: "Directory where the SDK will be generated"
@@ -39,18 +36,19 @@ Supports multiple programming languages including Java, C#, Python, JavaScript,
3936

4037
static examples = [
4138
`${SdkGenerate.cmdTxt} ${format.flag("language", "java")}`,
42-
`${SdkGenerate.cmdTxt} ${format.flag("language", "csharp")} ${format.flag("spec", "./src/spec")}`,
39+
`${SdkGenerate.cmdTxt} ${format.flag("language", "csharp")} ${format.flag("input", "./")}`,
4340
`${SdkGenerate.cmdTxt} ${format.flag("language", "python")} ${format.flag("destination", "./sdk")} ${format.flag(
4441
"zip"
4542
)}`
4643
];
4744

4845
async run() {
4946
const {
50-
flags: { language, spec, destination, force, zip: zipSdk, "auth-key": authKey }
47+
flags: { language, input, destination, force, zip: zipSdk, "auth-key": authKey }
5148
} = await this.parse(SdkGenerate);
5249

53-
const specDirectory = new DirectoryPath(spec);
50+
const workingDirectory = DirectoryPath.createInput(input);
51+
const buildDirectory = input ? new DirectoryPath(input, "src") : workingDirectory.join("src");
5452
const sdkDirectory = destination ? new DirectoryPath(destination) : DirectoryPath.default.join("sdk");
5553

5654
const commandMetadata: CommandMetadata = {
@@ -60,7 +58,7 @@ Supports multiple programming languages including Java, C#, Python, JavaScript,
6058

6159
intro("Generate SDK");
6260
const action = new GenerateAction(this.getConfigDir(), commandMetadata, authKey);
63-
const result = await action.execute(specDirectory, sdkDirectory, language as Language, force, zipSdk);
61+
const result = await action.execute(buildDirectory, sdkDirectory, language as Language, force, zipSdk);
6462
outro(result);
6563
}
6664

src/infrastructure/file-service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ export class FileService {
5757
return new Directory(directoryPath, results);
5858
}
5959

60+
public async getSubDirectoriesPaths(dir: DirectoryPath): Promise<DirectoryPath[]> {
61+
try {
62+
const entries = await fsExtra.readdir(dir.toString(), { withFileTypes: true });
63+
return entries
64+
.filter((entry) => entry.isDirectory())
65+
.map((entry) => dir.join(entry.name));
66+
} catch {
67+
return [];
68+
}
69+
}
70+
6071
public async copyDirectoryContents(source: DirectoryPath, destination: DirectoryPath) {
6172
const entries = await fsExtra.readdir(source.toString());
6273
await Promise.all(

src/infrastructure/services/api-service.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { SubscriptionInfo } from "../../types/api/account.js";
55
import { envInfo } from "../env-info.js";
66
import { err, ok, Result } from "neverthrow";
77
import { handleServiceError, ServiceError } from "../service-error.js";
8-
import { PortalGenerationStatusResponse } from "@apimatic/sdk";
8+
import { PortalGenerationStatusResponse, SdkGenerationStatusResponse } from "@apimatic/sdk";
99

1010
export class ApiService {
1111
private readonly apiBaseUrl = "https://api.apimatic.io" as const;
@@ -66,6 +66,39 @@ export class ApiService {
6666
}
6767
}
6868

69+
public async getSdkGenerationStatus(
70+
requestId: string,
71+
configDir: DirectoryPath,
72+
shell: string,
73+
authKey: string | null
74+
): Promise<Result<SdkGenerationStatusResponse, ServiceError>> {
75+
const authInfo: AuthInfo | null = await getAuthInfo(configDir.toString());
76+
if (authInfo === null && !authKey) {
77+
return err(ServiceError.UnAuthorized);
78+
}
79+
80+
try {
81+
const token = authKey || authInfo?.authKey;
82+
const response = await this.axiosInstance(shell, token).get(`/sdk/${requestId}/status`, {
83+
headers: { Accept: "application/json" },
84+
maxRedirects: 0,
85+
validateStatus: () => true
86+
});
87+
88+
if (response.status === 200) {
89+
return ok(response.data as SdkGenerationStatusResponse);
90+
}
91+
92+
if (response.status === 302) {
93+
return ok({ status: "Completed" } as SdkGenerationStatusResponse);
94+
}
95+
96+
return err(ServiceError.InvalidResponse);
97+
} catch (error: unknown) {
98+
return err(handleServiceError(error));
99+
}
100+
}
101+
69102
public async sendTelemetry(payload: string, authKey: string, shell: string): Promise<Result<string, string | ServiceError>> {
70103
try {
71104
const response = await this.axiosInstance(shell, authKey).post("/telemetry/track", payload, {

0 commit comments

Comments
 (0)