Skip to content
Open
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
74 changes: 35 additions & 39 deletions src/actions/portal/quickstart.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import { getAuthInfo } from "../../client-utils/auth-manager.js";
import { FileService } from "../../infrastructure/file-service.js";
import { withDirPath } from "../../infrastructure/tmp-extensions.js";
import { ZipService } from "../../infrastructure/zip-service.js";
import { PortalQuickstartPrompts } from "../../prompts/portal/quickstart.js";
import { DirectoryPath } from "../../types/file/directoryPath.js";
import { UrlPath } from "../../types/file/urlPath.js";
import { LoginAction } from "../auth/login.js";
import { ActionResult } from "../action-result.js";
import { PortalServeAction } from "./serve.js";
import { CommandMetadata } from "../../types/common/command-metadata.js";
import { ValidateAction } from "../api/validate.js";
import { BuildContext } from "../../types/build-context.js";
import { TempContext } from "../../types/temp-context.js";
import { FileDownloadService } from "../../infrastructure/services/file-download-service.js";
import { getLanguagesConfig } from "../../types/build/build.js";
import { FilePath } from "../../types/file/filePath.js";
import { SpecContext } from "../../types/spec-context.js";
import { FeaturesToRemove, ValidationService } from "../../infrastructure/services/validation-service.js";
import { FileName } from "../../types/file/fileName.js";
import { getAuthInfo } from '../../client-utils/auth-manager.js';
import { FileService } from '../../infrastructure/file-service.js';
import { withDirPath } from '../../infrastructure/tmp-extensions.js';
import { ZipService } from '../../infrastructure/zip-service.js';
import { PortalQuickstartPrompts } from '../../prompts/portal/quickstart.js';
import { DirectoryPath } from '../../types/file/directoryPath.js';
import { UrlPath } from '../../types/file/urlPath.js';
import { LoginAction } from '../auth/login.js';
import { ActionResult } from '../action-result.js';
import { PortalServeAction } from './serve.js';
import { CommandMetadata } from '../../types/common/command-metadata.js';
import { ValidateAction } from '../api/validate.js';
import { BuildContext } from '../../types/build-context.js';
import { TempContext } from '../../types/temp-context.js';
import { FileDownloadService } from '../../infrastructure/services/file-download-service.js';
import { getLanguagesConfig } from '../../types/build/build.js';
import { FilePath } from '../../types/file/filePath.js';
import { SpecContext } from '../../types/spec-context.js';
import { ValidationService } from '../../infrastructure/services/validation-service.js';
import { stripPrompts } from '../../prompts/strip/strip.js';

const defaultPort: number = 3000 as const;

export class PortalQuickstartAction {
private readonly prompts: PortalQuickstartPrompts = new PortalQuickstartPrompts();
private readonly zipService: ZipService = new ZipService();
private readonly stripSpecPrompts = new stripPrompts();
private readonly fileService: FileService = new FileService();
private readonly configDir: DirectoryPath;
private readonly commandMetadata: CommandMetadata;
Expand All @@ -34,7 +35,7 @@ export class PortalQuickstartAction {
private readonly defaultSpecUrl = new UrlPath(
`https://raw.githubusercontent.com/apimatic/sample-docs-as-code-portal/refs/heads/master/src/spec/openapi.json`
);
private readonly repositoryFolderName = "sample-docs-as-code-portal-master/src" as const;
private readonly repositoryFolderName = 'sample-docs-as-code-portal-master/src' as const;
private readonly validationService: ValidationService;

constructor(configDir: DirectoryPath, commandMetadata: CommandMetadata) {
Expand Down Expand Up @@ -107,22 +108,17 @@ export class PortalQuickstartAction {
}

const unallowed = validationResult.getValue();
if (unallowed && (unallowed.Features?.length > 0 || unallowed.EndpointCount > unallowed.EndpointLimit)) {
const config: FeaturesToRemove = {
features: unallowed.Features.filter((name) => !!name),
endpointsToKeep: unallowed.EndpointLimit
};

const stripUnallowedFeaturesResult = await this.validationService.stripUnallowedFeatures(specPath, config);
if (stripUnallowedFeaturesResult.isErr()) {
this.prompts.splitSpecDetected(unallowed);
return ActionResult.failed();
} else {
this.prompts.stripUnallowedFeaturesStep(unallowed);
const specContext = new SpecContext(tempDirectory);
specPath = await specContext.save(stripUnallowedFeaturesResult.value, new FileName("pruned-spec.zip"));
}
const featureResult = await this.validationService.processUnallowedFeatures(specPath, unallowed, tempDirectory);

if (!featureResult.success && featureResult.unallowedInfo) {
this.stripSpecPrompts.splitSpecDetected(featureResult.unallowedInfo);
return ActionResult.failed();
}

if (featureResult.featuresWereStripped && featureResult.unallowedInfo) {
this.stripSpecPrompts.stripUnallowedFeaturesStep(featureResult.unallowedInfo);
}
specPath = featureResult.updatedSpecPath ?? specPath;

// Step 3/4
this.prompts.selectLanguagesStep();
Expand Down Expand Up @@ -174,17 +170,17 @@ export class PortalQuickstartAction {
buildFile.generatePortal!.languageConfig = getLanguagesConfig(languages);
await tempBuildContext.updateBuildFileContents(buildFile);

const sourceDirectory = inputDirectory.join("src");
const sourceDirectory = inputDirectory.join('src');
await this.fileService.copyDirectoryContents(extractedFolder, sourceDirectory);

const specDirectory = sourceDirectory.join("spec");
const specDirectory = sourceDirectory.join('spec');
const specContext = new SpecContext(specDirectory);
await specContext.replaceDefaultSpec(specPath);

const buildDirectoryStructure = await this.fileService.getDirectory(sourceDirectory);
this.prompts.printDirectoryStructure(inputDirectory, buildDirectoryStructure);

const portalDirectory = inputDirectory.join("portal");
const portalDirectory = inputDirectory.join('portal');
const portalServeAction = new PortalServeAction(this.configDir, this.commandMetadata, null);
const result = await portalServeAction.execute(sourceDirectory, portalDirectory, defaultPort, true, false, () => {
this.prompts.nextSteps();
Expand Down
75 changes: 36 additions & 39 deletions src/actions/sdk/quickstart.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { SdkQuickstartPrompts } from "../../prompts/sdk/quickstart.js";
import { ActionResult } from "../action-result.js";
import { UrlPath } from "../../types/file/urlPath.js";
import { LoginAction } from "../auth/login.js";
import { CommandMetadata } from "../../types/common/command-metadata.js";
import { DirectoryPath } from "../../types/file/directoryPath.js";
import { getAuthInfo } from "../../client-utils/auth-manager.js";
import { withDirPath } from "../../infrastructure/tmp-extensions.js";
import { FilePath } from "../../types/file/filePath.js";
import { SpecContext } from "../../types/spec-context.js";
import { ValidateAction } from "../api/validate.js";
import { FileDownloadService } from "../../infrastructure/services/file-download-service.js";
import { FileService } from "../../infrastructure/file-service.js";
import { GenerateAction } from "./generate.js";
import { Language } from "../../types/sdk/generate.js";
import { LauncherService } from "../../infrastructure/launcher-service.js";
import { ZipService } from "../../infrastructure/zip-service.js";
import { FileName } from "../../types/file/fileName.js";
import { FeaturesToRemove, ValidationService } from "../../infrastructure/services/validation-service.js";
import { SdkQuickstartPrompts } from '../../prompts/sdk/quickstart.js';
import { ActionResult } from '../action-result.js';
import { UrlPath } from '../../types/file/urlPath.js';
import { LoginAction } from '../auth/login.js';
import { CommandMetadata } from '../../types/common/command-metadata.js';
import { DirectoryPath } from '../../types/file/directoryPath.js';
import { getAuthInfo } from '../../client-utils/auth-manager.js';
import { withDirPath } from '../../infrastructure/tmp-extensions.js';
import { FilePath } from '../../types/file/filePath.js';
import { SpecContext } from '../../types/spec-context.js';
import { ValidateAction } from '../api/validate.js';
import { FileDownloadService } from '../../infrastructure/services/file-download-service.js';
import { FileService } from '../../infrastructure/file-service.js';
import { GenerateAction } from './generate.js';
import { Language } from '../../types/sdk/generate.js';
import { LauncherService } from '../../infrastructure/launcher-service.js';
import { ZipService } from '../../infrastructure/zip-service.js';
import { FileName } from '../../types/file/fileName.js';
import { ValidationService } from '../../infrastructure/services/validation-service.js';
import { stripPrompts } from '../../prompts/strip/strip.js';

const defaultSpecUrl = new UrlPath(
`https://raw.githubusercontent.com/apimatic/sample-docs-as-code-portal/refs/heads/master/src/spec/openapi.json`
Expand All @@ -27,6 +28,7 @@ const metadataFileUrl = new UrlPath(

export class SdkQuickstartAction {
private readonly prompts = new SdkQuickstartPrompts();
private readonly stripSpecPrompts = new stripPrompts();
private readonly fileDownloadService = new FileDownloadService();
private readonly fileService = new FileService();
private readonly launcherService = new LauncherService();
Expand Down Expand Up @@ -100,22 +102,17 @@ export class SdkQuickstartAction {
}

const unallowed = validationResult.getValue();
if (unallowed && (unallowed.Features?.length > 0 || unallowed.EndpointCount > unallowed.EndpointLimit)) {
const config: FeaturesToRemove = {
features: unallowed.Features.filter((name) => !!name),
endpointsToKeep: unallowed.EndpointLimit
};

const stripUnallowedFeaturesResult = await this.validationService.stripUnallowedFeatures(specPath, config);
if (stripUnallowedFeaturesResult.isErr()) {
this.prompts.splitSpecDetected(unallowed);
return ActionResult.failed();
} else {
this.prompts.stripUnallowedFeaturesStep(unallowed);
const specContext = new SpecContext(tempDirectory);
specPath = await specContext.save(stripUnallowedFeaturesResult.value, new FileName("pruned-spec.zip"));
}
const featureResult = await this.validationService.processUnallowedFeatures(specPath, unallowed, tempDirectory);

if (!featureResult.success && featureResult.unallowedInfo) {
this.stripSpecPrompts.splitSpecDetected(featureResult.unallowedInfo);
return ActionResult.failed();
}

if (featureResult.featuresWereStripped && featureResult.unallowedInfo) {
this.stripSpecPrompts.stripUnallowedFeaturesStep(featureResult.unallowedInfo);
}
specPath = featureResult.updatedSpecPath ?? specPath;

// Step 3/4
this.prompts.selectLanguageStep();
Expand Down Expand Up @@ -158,7 +155,7 @@ export class SdkQuickstartAction {
this.prompts.serviceError(apimaticMetaFile.error);
return ActionResult.failed();
}
const tempSpecDirectory = tempDirectory.join("spec");
const tempSpecDirectory = tempDirectory.join('spec');
await this.fileService.createDirectoryIfNotExists(tempSpecDirectory);
const metadataFilePath = new FilePath(tempSpecDirectory, apimaticMetaFile.value.filename);
await this.fileService.writeFile(metadataFilePath, apimaticMetaFile.value.stream);
Expand All @@ -169,22 +166,22 @@ export class SdkQuickstartAction {
await this.fileService.copyToDir(specPath, tempSpecDirectory);
}

const sourceDirectory = inputDirectory.join("src");
const specDirectory = sourceDirectory.join("spec");
const sourceDirectory = inputDirectory.join('src');
const specDirectory = sourceDirectory.join('spec');
await this.fileService.copyDirectoryContents(tempSpecDirectory, specDirectory);

const srcDirectoryStructure = await this.fileService.getDirectory(sourceDirectory);
this.prompts.printDirectoryStructure(inputDirectory, srcDirectoryStructure);

const sdkDirectory = inputDirectory.join("sdk");
const sdkDirectory = inputDirectory.join('sdk');
const sdkGenerateAction = new GenerateAction(this.configDir, this.commandMetadata);
const result = await sdkGenerateAction.execute(specDirectory, sdkDirectory, language as Language, true, false);
if (result.isFailed()) {
return ActionResult.failed();
}

const languageDirectory = sdkDirectory.join(language);
const readmeFilePath = new FilePath(languageDirectory, new FileName("README.md"));
const readmeFilePath = new FilePath(languageDirectory, new FileName('README.md'));
if (await this.launcherService.openFolderInIde(languageDirectory, readmeFilePath)) {
this.prompts.sdkOpenedInEditor();
}
Expand Down
63 changes: 63 additions & 0 deletions src/infrastructure/services/validation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { handleServiceError, ServiceError } from "../service-error.js";
import axios from "axios";
import { envInfo } from "../env-info.js";
import { Buffer } from "node:buffer";
import { FileName } from "../../types/file/fileName.js";
import { SpecContext } from "../../types/spec-context.js";

export enum RemovableFeature {
Merging = 'Merging',
Expand Down Expand Up @@ -53,6 +55,15 @@ export interface ValidateApiResponse {
unallowedFeatures: UnallowedFeaturesResponse | null;
}

export interface ProcessUnallowedFeaturesResult {
success: boolean;
updatedSpecPath?: FilePath;
splitSpecDetected?: boolean;
featuresWereStripped?: boolean;
unallowedInfo?: UnallowedFeaturesResponse;
}


export class ValidationService {
constructor(private readonly configDir: DirectoryPath) {}

Expand Down Expand Up @@ -130,6 +141,58 @@ export class ValidationService {
}
}

public async processUnallowedFeatures(
specPath: FilePath,
unallowed: UnallowedFeaturesResponse | null,
tempDirectory: DirectoryPath,
authKey?: string | null
): Promise<ProcessUnallowedFeaturesResult> {
if (!unallowed || (unallowed.Features?.length === 0 && unallowed.EndpointCount <= unallowed.EndpointLimit)) {
return {
success: true,
updatedSpecPath: specPath,
featuresWereStripped: false
};
}

if (unallowed.IsSplitSpec) {
return {
success: false,
splitSpecDetected: true,
unallowedInfo: unallowed
};
}

const config: FeaturesToRemove = {
features: unallowed.Features.filter((name) => !!name),
endpointsToKeep: unallowed.EndpointLimit
};

const stripResult = await this.stripUnallowedFeatures(specPath, config, authKey);

if (stripResult.isErr()) {
return {
success: false,
splitSpecDetected: true,
unallowedInfo: unallowed
};
}

const specContext = new SpecContext(tempDirectory);
const updatedSpecPath = await specContext.save(
stripResult.value,
new FileName("pruned-spec.zip")
);

return {
success: true,
updatedSpecPath,
featuresWereStripped: true,
unallowedInfo: unallowed
};
}


private createAuthorizationHeader(authInfo: AuthInfo | null, overrideAuthKey: string | null): string {
const key = overrideAuthKey || authInfo?.authKey;
return `X-Auth-Key ${key ?? ""}`;
Expand Down
44 changes: 0 additions & 44 deletions src/prompts/portal/quickstart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,50 +72,6 @@ export class PortalQuickstartPrompts {
log.info(`Step 2 of 4: Validate and Lint your OpenAPI Definition`);
}

public splitSpecDetected(unallowed: UnallowedFeaturesResponse): void {
const featuresList = unallowed.Features.map((f) => ` • ${f}`).join("\n");

let endpointMessage = "";
if (unallowed.EndpointLimit < unallowed.EndpointCount) {
endpointMessage = `\nEndpoint limit exceeded: ${unallowed.EndpointCount} endpoints found, but your plan allows ${unallowed.EndpointLimit}\n`;
}

const message = [
"Your API Specification includes components not available on your current subscription plan:",
"",
featuresList,
endpointMessage,
"To continue:",
"- Remove these components from your API Specification and re-run this command.",
"- Combine your split API Specification files into a single file. We can automatically remove unsupported components from single-file specs.",
"- Upgrade your subscription to unlock additional features: https://www.apimatic.io/pricing"
].join("\n");

log.info(message);
}

public stripUnallowedFeaturesStep(unallowed: UnallowedFeaturesResponse): void {
const featuresList = unallowed.Features.map((f) => ` • ${f}`).join("\n");

let endpointMessage = "";
if (unallowed.EndpointLimit < unallowed.EndpointCount) {
const endpointsToRemove = unallowed.EndpointCount - unallowed.EndpointLimit;
endpointMessage = `\n${endpointsToRemove} endpoint(s) will be removed from your spec\n`;
}

const message = [
"Your API Specification includes components not available on your current subscription plan.",
"We'll automatically remove these components before proceeding:",
featuresList,
endpointMessage,
"",
"You won't see these components in the generated SDKs or documentation.",
"Want to keep them? Upgrade your subscription to unlock additional features: https://www.apimatic.io/pricing"
].join("\n");

log.info(message);
}

public selectLanguagesStep() {
log.info(`Step 3 of 4: Select programming languages`);
}
Expand Down
Loading