diff --git a/src/actions/portal/quickstart.ts b/src/actions/portal/quickstart.ts index 422aebc2..ea9560d5 100644 --- a/src/actions/portal/quickstart.ts +++ b/src/actions/portal/quickstart.ts @@ -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; @@ -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) { @@ -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(); @@ -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(); diff --git a/src/actions/sdk/quickstart.ts b/src/actions/sdk/quickstart.ts index 13bb3e7b..45fba478 100644 --- a/src/actions/sdk/quickstart.ts +++ b/src/actions/sdk/quickstart.ts @@ -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` @@ -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(); @@ -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(); @@ -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); @@ -169,14 +166,14 @@ 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()) { @@ -184,7 +181,7 @@ export class SdkQuickstartAction { } 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(); } diff --git a/src/infrastructure/services/validation-service.ts b/src/infrastructure/services/validation-service.ts index 3ec96ccb..95cd5c1b 100644 --- a/src/infrastructure/services/validation-service.ts +++ b/src/infrastructure/services/validation-service.ts @@ -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', @@ -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) {} @@ -130,6 +141,58 @@ export class ValidationService { } } + public async processUnallowedFeatures( + specPath: FilePath, + unallowed: UnallowedFeaturesResponse | null, + tempDirectory: DirectoryPath, + authKey?: string | null + ): Promise { + 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 ?? ""}`; diff --git a/src/prompts/portal/quickstart.ts b/src/prompts/portal/quickstart.ts index aa7e6280..66ce8401 100644 --- a/src/prompts/portal/quickstart.ts +++ b/src/prompts/portal/quickstart.ts @@ -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`); } diff --git a/src/prompts/sdk/quickstart.ts b/src/prompts/sdk/quickstart.ts index 5aaedb62..ac0a27dc 100644 --- a/src/prompts/sdk/quickstart.ts +++ b/src/prompts/sdk/quickstart.ts @@ -10,7 +10,6 @@ import { DirectoryPath } from "../../types/file/directoryPath.js"; import { removeQuotes } from "../../utils/string-utils.js"; import { Directory } from "../../types/file/directory.js"; import { Language } from "../../types/sdk/generate.js"; -import { UnallowedFeaturesResponse } from "../../infrastructure/services/validation-service.js"; const vscodeExtensionUrl = "https://marketplace.visualstudio.com/items?itemName=apimatic-developers.apimatic-for-vscode"; @@ -42,50 +41,6 @@ export class SdkQuickstartPrompts { return createResourceInputFromInput(spec); } - 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 specFileDoesNotExist() { log.error("The specified file does not exist or is not a valid file. Please enter a valid file path."); } diff --git a/src/prompts/strip/strip.ts b/src/prompts/strip/strip.ts new file mode 100644 index 00000000..4bec935b --- /dev/null +++ b/src/prompts/strip/strip.ts @@ -0,0 +1,48 @@ +import { log } from "@clack/prompts"; +import { UnallowedFeaturesResponse } from "../../infrastructure/services/validation-service.js"; + +export class stripPrompts { + 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); + } +}