mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-06-30 10:01:42 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 580302cbd2 | |||
| 3b9c311dc5 | |||
| 016fcba1c6 |
@@ -1070,7 +1070,7 @@ impl PluginManager {
|
|||||||
&InternalEventPayload::ImportRequest(ImportRequest {
|
&InternalEventPayload::ImportRequest(ImportRequest {
|
||||||
content: content.to_string(),
|
content: content.to_string(),
|
||||||
}),
|
}),
|
||||||
Duration::from_secs(5),
|
Duration::from_secs(60),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
Generated
+303
-446
File diff suppressed because it is too large
Load Diff
@@ -10,10 +10,10 @@
|
|||||||
"test": "vp test --run tests"
|
"test": "vp test --run tests"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"openapi-to-postmanv2": "^5.8.0",
|
|
||||||
"yaml": "^2.8.3"
|
"yaml": "^2.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/openapi-to-postmanv2": "^5.0.0"
|
"@types/openapi-to-postmanv2": "^5.0.0",
|
||||||
|
"openapi-to-postmanv2": "^5.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,37 @@
|
|||||||
import { convertPostman } from "@yaak/importer-postman/src";
|
import type {
|
||||||
import type { Context, PluginDefinition } from "@yaakapp/api";
|
Context,
|
||||||
|
Environment,
|
||||||
|
Folder,
|
||||||
|
HttpRequest,
|
||||||
|
HttpRequestHeader,
|
||||||
|
HttpUrlParameter,
|
||||||
|
PartialImportResources,
|
||||||
|
PluginDefinition,
|
||||||
|
Workspace,
|
||||||
|
} from "@yaakapp/api";
|
||||||
import type { ImportPluginResponse } from "@yaakapp/api/lib/plugins/ImporterPlugin";
|
import type { ImportPluginResponse } from "@yaakapp/api/lib/plugins/ImporterPlugin";
|
||||||
import { convert } from "openapi-to-postmanv2";
|
import YAML from "yaml";
|
||||||
|
|
||||||
|
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||||
|
type UnknownRecord = Record<string, unknown>;
|
||||||
|
type ImportResources = {
|
||||||
|
workspaces: AtLeast<Workspace, "name" | "id" | "model">[];
|
||||||
|
environments: AtLeast<Environment, "name" | "id" | "model" | "workspaceId" | "variables">[];
|
||||||
|
folders: AtLeast<Folder, "name" | "id" | "model" | "workspaceId">[];
|
||||||
|
httpRequests: AtLeast<HttpRequest, "name" | "id" | "model" | "workspaceId">[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const HTTP_METHODS = ["delete", "get", "head", "options", "patch", "post", "put", "trace"];
|
||||||
|
const BODY_CONTENT_TYPE_PREFERENCE = [
|
||||||
|
"application/json",
|
||||||
|
"application/x-www-form-urlencoded",
|
||||||
|
"multipart/form-data",
|
||||||
|
"application/xml",
|
||||||
|
"text/plain",
|
||||||
|
];
|
||||||
|
const MAX_EXAMPLE_DEPTH = 8;
|
||||||
|
const MAX_EXAMPLE_PROPERTIES = 25;
|
||||||
|
const MAX_DESCRIPTION_ITEMS = 40;
|
||||||
|
|
||||||
export const plugin: PluginDefinition = {
|
export const plugin: PluginDefinition = {
|
||||||
importer: {
|
importer: {
|
||||||
@@ -14,23 +44,785 @@ export const plugin: PluginDefinition = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function convertOpenApi(contents: string): Promise<ImportPluginResponse | undefined> {
|
export async function convertOpenApi(contents: string): Promise<ImportPluginResponse | undefined> {
|
||||||
// oxlint-disable-next-line no-explicit-any
|
const spec = parseSpec(contents);
|
||||||
let postmanCollection: any;
|
if (!isOpenApiSpec(spec)) return undefined;
|
||||||
|
|
||||||
|
const importState = new ImportState(spec);
|
||||||
|
const workspace: ImportResources["workspaces"][0] = {
|
||||||
|
model: "workspace",
|
||||||
|
id: importState.generateId("workspace"),
|
||||||
|
name: stringAt(spec.info, "title") ?? "OpenAPI Import",
|
||||||
|
description: importInfoDescription(toRecord(spec.info)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const resources: ImportResources = {
|
||||||
|
workspaces: [workspace],
|
||||||
|
environments: [],
|
||||||
|
folders: [],
|
||||||
|
httpRequests: [],
|
||||||
|
};
|
||||||
|
const baseUrl = importBaseUrl(spec);
|
||||||
|
const requestBaseUrl = baseUrl.length > 0 ? "${[baseUrl]}" : "";
|
||||||
|
|
||||||
|
if (baseUrl.length > 0) {
|
||||||
|
resources.environments.push({
|
||||||
|
model: "environment",
|
||||||
|
id: importState.generateId("environment"),
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
name: "Global Variables",
|
||||||
|
variables: [{ name: "baseUrl", value: baseUrl }],
|
||||||
|
parentModel: "workspace",
|
||||||
|
parentId: null,
|
||||||
|
sortPriority: importState.nextSortPriority(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderIdsByTag = new Map<string, string>();
|
||||||
|
for (const tag of toArray(spec.tags)) {
|
||||||
|
const tagRecord = toRecord(tag);
|
||||||
|
const name = stringAt(tagRecord, "name");
|
||||||
|
if (name == null || folderIdsByTag.has(name)) continue;
|
||||||
|
|
||||||
|
const folder: ImportResources["folders"][0] = {
|
||||||
|
model: "folder",
|
||||||
|
id: importState.generateId("folder"),
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
name,
|
||||||
|
description: importTagDescription(tagRecord),
|
||||||
|
folderId: null,
|
||||||
|
sortPriority: importState.nextSortPriority(),
|
||||||
|
};
|
||||||
|
resources.folders.push(folder);
|
||||||
|
folderIdsByTag.set(name, folder.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [rawPath, rawPathItem] of Object.entries(toRecord(spec.paths))) {
|
||||||
|
const pathItem = importState.resolve(rawPathItem);
|
||||||
|
if (!isRecord(pathItem)) continue;
|
||||||
|
|
||||||
|
const pathParameters = toArray(pathItem.parameters);
|
||||||
|
for (const method of HTTP_METHODS) {
|
||||||
|
const operation = importState.resolve(pathItem[method]);
|
||||||
|
if (!isRecord(operation)) continue;
|
||||||
|
|
||||||
|
const folderId = findOrCreateFolderId({
|
||||||
|
folderIdsByTag,
|
||||||
|
importState,
|
||||||
|
operation,
|
||||||
|
resources,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
resources.httpRequests.push(
|
||||||
|
importOperation({
|
||||||
|
importState,
|
||||||
|
method,
|
||||||
|
operation,
|
||||||
|
path: rawPath,
|
||||||
|
pathParameters,
|
||||||
|
requestBaseUrl,
|
||||||
|
spec,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
folderId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resources.httpRequests.length === 0) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
resources: deleteUndefinedAttrs(
|
||||||
|
convertTemplateSyntax({
|
||||||
|
environments: resources.environments,
|
||||||
|
folders: resources.folders,
|
||||||
|
grpcRequests: [],
|
||||||
|
httpRequests: resources.httpRequests,
|
||||||
|
websocketRequests: [],
|
||||||
|
workspaces: resources.workspaces,
|
||||||
|
}),
|
||||||
|
) as PartialImportResources,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function importOperation({
|
||||||
|
importState,
|
||||||
|
method,
|
||||||
|
operation,
|
||||||
|
path,
|
||||||
|
pathParameters,
|
||||||
|
requestBaseUrl,
|
||||||
|
spec,
|
||||||
|
workspaceId,
|
||||||
|
folderId,
|
||||||
|
}: {
|
||||||
|
importState: ImportState;
|
||||||
|
method: string;
|
||||||
|
operation: UnknownRecord;
|
||||||
|
path: string;
|
||||||
|
pathParameters: unknown[];
|
||||||
|
requestBaseUrl: string;
|
||||||
|
spec: UnknownRecord;
|
||||||
|
workspaceId: string;
|
||||||
|
folderId: string | null;
|
||||||
|
}): ImportResources["httpRequests"][0] {
|
||||||
|
const parameters = [...pathParameters, ...toArray(operation.parameters)].map((p) =>
|
||||||
|
importState.resolve(p),
|
||||||
|
);
|
||||||
|
const body = importBody({ importState, operation, parameters, spec });
|
||||||
|
const urlParameters = importUrlParameters({ importState, parameters });
|
||||||
|
const headers = mergeHeaders(importHeaderParameters({ importState, parameters }), body.headers);
|
||||||
|
|
||||||
|
return {
|
||||||
|
model: "http_request",
|
||||||
|
id: importState.generateId("http_request"),
|
||||||
|
workspaceId,
|
||||||
|
folderId,
|
||||||
|
name: importOperationName(operation, method, path),
|
||||||
|
description: importOperationDescription({
|
||||||
|
importState,
|
||||||
|
operation,
|
||||||
|
parameters,
|
||||||
|
bodyContentType: body.bodyType,
|
||||||
|
}),
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
url: buildOperationUrl(requestBaseUrl, path),
|
||||||
|
urlParameters,
|
||||||
|
headers,
|
||||||
|
body: body.body,
|
||||||
|
bodyType: body.bodyType,
|
||||||
|
sortPriority: importState.nextSortPriority(),
|
||||||
|
...importAuthentication({ importState, operation, spec }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSpec(contents: string): unknown {
|
||||||
try {
|
try {
|
||||||
postmanCollection = await new Promise((resolve, reject) => {
|
return JSON.parse(contents);
|
||||||
// oxlint-disable-next-line no-explicit-any
|
|
||||||
convert({ type: "string", data: contents }, {}, (err, result: any) => {
|
|
||||||
if (err != null) reject(err);
|
|
||||||
|
|
||||||
if (Array.isArray(result.output) && result.output.length > 0) {
|
|
||||||
resolve(result.output[0].data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
// Probably not an OpenAPI file, so skip it
|
// Fall through to YAML.
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertPostman(JSON.stringify(postmanCollection));
|
try {
|
||||||
|
return YAML.parse(contents);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOpenApiSpec(value: unknown): value is UnknownRecord {
|
||||||
|
const spec = toRecord(value);
|
||||||
|
const openapi = stringAt(spec, "openapi");
|
||||||
|
const swagger = stringAt(spec, "swagger");
|
||||||
|
return isRecord(spec.paths) && (openapi?.startsWith("3.") === true || swagger === "2.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function importInfoDescription(info: UnknownRecord): string | undefined {
|
||||||
|
const parts = [
|
||||||
|
stringAt(info, "description"),
|
||||||
|
stringAt(info, "termsOfService")
|
||||||
|
? `Terms of service: ${stringAt(info, "termsOfService")}`
|
||||||
|
: null,
|
||||||
|
isRecord(info.contact) && stringAt(info.contact, "email")
|
||||||
|
? `Contact: ${stringAt(info.contact, "email")}`
|
||||||
|
: null,
|
||||||
|
isRecord(info.license) && stringAt(info.license, "name")
|
||||||
|
? `License: ${stringAt(info.license, "name")}${
|
||||||
|
stringAt(info.license, "url") ? ` (${stringAt(info.license, "url")})` : ""
|
||||||
|
}`
|
||||||
|
: null,
|
||||||
|
].filter(isPresent);
|
||||||
|
return parts.length > 0 ? parts.join("\n\n") : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function importTagDescription(tag: UnknownRecord): string | undefined {
|
||||||
|
const externalDocs = toRecord(tag.externalDocs);
|
||||||
|
const parts = [
|
||||||
|
stringAt(tag, "description"),
|
||||||
|
stringAt(externalDocs, "url")
|
||||||
|
? `${stringAt(externalDocs, "description") ?? "External docs"}: ${stringAt(externalDocs, "url")}`
|
||||||
|
: null,
|
||||||
|
].filter(isPresent);
|
||||||
|
return parts.length > 0 ? parts.join("\n\n") : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function importOperationName(operation: UnknownRecord, method: string, path: string): string {
|
||||||
|
return (
|
||||||
|
stringAt(operation, "summary") ??
|
||||||
|
stringAt(operation, "operationId") ??
|
||||||
|
`${method.toUpperCase()} ${path}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importOperationDescription({
|
||||||
|
importState,
|
||||||
|
operation,
|
||||||
|
parameters,
|
||||||
|
bodyContentType,
|
||||||
|
}: {
|
||||||
|
importState: ImportState;
|
||||||
|
operation: UnknownRecord;
|
||||||
|
parameters: unknown[];
|
||||||
|
bodyContentType: string | null;
|
||||||
|
}): string | undefined {
|
||||||
|
const parts: string[] = [];
|
||||||
|
const summary = stringAt(operation, "summary");
|
||||||
|
const description = stringAt(operation, "description");
|
||||||
|
const operationId = stringAt(operation, "operationId");
|
||||||
|
|
||||||
|
if (description != null) {
|
||||||
|
parts.push(description);
|
||||||
|
} else if (summary != null) {
|
||||||
|
parts.push(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operationId != null) {
|
||||||
|
parts.push(`Operation ID: ${operationId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameterDescriptions = parameters
|
||||||
|
.map((p) => importState.resolve(p))
|
||||||
|
.filter(isRecord)
|
||||||
|
.slice(0, MAX_DESCRIPTION_ITEMS)
|
||||||
|
.map((p) => {
|
||||||
|
const name = stringAt(p, "name") ?? "parameter";
|
||||||
|
const location = stringAt(p, "in") ?? "unknown";
|
||||||
|
const required = p.required === true ? ", required" : "";
|
||||||
|
const description = stringAt(p, "description");
|
||||||
|
return `- ${name} (${location}${required})${description ? `: ${description}` : ""}`;
|
||||||
|
});
|
||||||
|
if (parameterDescriptions.length > 0) {
|
||||||
|
parts.push(["Parameters:", ...parameterDescriptions].join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestBody = importState.resolve(operation.requestBody);
|
||||||
|
if (isRecord(requestBody)) {
|
||||||
|
const content = toRecord(requestBody.content);
|
||||||
|
const contentTypes = Object.keys(content);
|
||||||
|
const bodyLines = [
|
||||||
|
stringAt(requestBody, "description"),
|
||||||
|
bodyContentType ? `Selected content type: ${bodyContentType}` : null,
|
||||||
|
contentTypes.length > 0 ? `Available content types: ${contentTypes.join(", ")}` : null,
|
||||||
|
].filter(isPresent);
|
||||||
|
if (bodyLines.length > 0) {
|
||||||
|
parts.push(["Request body:", ...bodyLines].join("\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseDescriptions = Object.entries(toRecord(operation.responses))
|
||||||
|
.slice(0, MAX_DESCRIPTION_ITEMS)
|
||||||
|
.map(([status, response]) => {
|
||||||
|
const responseRecord = toRecord(importState.resolve(response));
|
||||||
|
return `- ${status}: ${stringAt(responseRecord, "description") ?? ""}`.trimEnd();
|
||||||
|
});
|
||||||
|
if (responseDescriptions.length > 0) {
|
||||||
|
parts.push(["Responses:", ...responseDescriptions].join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const externalDocs = toRecord(operation.externalDocs);
|
||||||
|
if (stringAt(externalDocs, "url")) {
|
||||||
|
parts.push(
|
||||||
|
`${stringAt(externalDocs, "description") ?? "External docs"}: ${stringAt(externalDocs, "url")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join("\n\n") : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findOrCreateFolderId({
|
||||||
|
folderIdsByTag,
|
||||||
|
importState,
|
||||||
|
operation,
|
||||||
|
resources,
|
||||||
|
workspaceId,
|
||||||
|
}: {
|
||||||
|
folderIdsByTag: Map<string, string>;
|
||||||
|
importState: ImportState;
|
||||||
|
operation: UnknownRecord;
|
||||||
|
resources: ImportResources;
|
||||||
|
workspaceId: string;
|
||||||
|
}): string | null {
|
||||||
|
const tag = toArray(operation.tags).find((t): t is string => typeof t === "string");
|
||||||
|
if (tag == null) return null;
|
||||||
|
|
||||||
|
const existingFolderId = folderIdsByTag.get(tag);
|
||||||
|
if (existingFolderId != null) return existingFolderId;
|
||||||
|
|
||||||
|
const folder: ImportResources["folders"][0] = {
|
||||||
|
model: "folder",
|
||||||
|
id: importState.generateId("folder"),
|
||||||
|
workspaceId,
|
||||||
|
name: tag,
|
||||||
|
folderId: null,
|
||||||
|
sortPriority: importState.nextSortPriority(),
|
||||||
|
};
|
||||||
|
resources.folders.push(folder);
|
||||||
|
folderIdsByTag.set(tag, folder.id);
|
||||||
|
return folder.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOperationUrl(baseUrl: string, path: string): string {
|
||||||
|
return joinUrlParts(baseUrl, path.replaceAll(/{([^}/]+)}/g, ":$1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function importBaseUrl(spec: UnknownRecord): string {
|
||||||
|
const openApiServer = toArray(spec.servers)
|
||||||
|
.map((s) => toRecord(s))
|
||||||
|
.map((s) => interpolateServerUrl(s))
|
||||||
|
.find((url) => url.length > 0);
|
||||||
|
if (openApiServer != null) return openApiServer;
|
||||||
|
|
||||||
|
const host = stringAt(spec, "host");
|
||||||
|
if (host == null) return stringAt(spec, "basePath") ?? "";
|
||||||
|
|
||||||
|
const scheme = toArray(spec.schemes).find((s): s is string => typeof s === "string") ?? "https";
|
||||||
|
return joinUrlParts(`${scheme}://${host}`, stringAt(spec, "basePath") ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpolateServerUrl(server: UnknownRecord): string {
|
||||||
|
let url = stringAt(server, "url") ?? "";
|
||||||
|
for (const [name, variable] of Object.entries(toRecord(server.variables))) {
|
||||||
|
url = url.replaceAll(`{${name}}`, stringifyExampleValue(toRecord(variable).default));
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinUrlParts(baseUrl: string, path: string): string {
|
||||||
|
if (baseUrl.length === 0) return path;
|
||||||
|
return `${trimTrailingSlashes(baseUrl)}/${trimLeadingSlashes(path)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimLeadingSlashes(value: string): string {
|
||||||
|
let index = 0;
|
||||||
|
while (value[index] === "/") index++;
|
||||||
|
return value.slice(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimTrailingSlashes(value: string): string {
|
||||||
|
let index = value.length;
|
||||||
|
while (value[index - 1] === "/") index--;
|
||||||
|
return value.slice(0, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importUrlParameters({
|
||||||
|
importState,
|
||||||
|
parameters,
|
||||||
|
}: {
|
||||||
|
importState: ImportState;
|
||||||
|
parameters: unknown[];
|
||||||
|
}): HttpUrlParameter[] {
|
||||||
|
return parameters
|
||||||
|
.map((p) => importState.resolve(p))
|
||||||
|
.filter(isRecord)
|
||||||
|
.filter((p) => stringAt(p, "in") === "query" || stringAt(p, "in") === "path")
|
||||||
|
.map((p) => ({
|
||||||
|
enabled: p.required === true,
|
||||||
|
name:
|
||||||
|
stringAt(p, "in") === "path"
|
||||||
|
? `:${stringAt(p, "name") ?? ""}`
|
||||||
|
: (stringAt(p, "name") ?? ""),
|
||||||
|
value: parameterExample(p, importState),
|
||||||
|
}))
|
||||||
|
.filter(({ name }) => name.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importHeaderParameters({
|
||||||
|
importState,
|
||||||
|
parameters,
|
||||||
|
}: {
|
||||||
|
importState: ImportState;
|
||||||
|
parameters: unknown[];
|
||||||
|
}): HttpRequestHeader[] {
|
||||||
|
return parameters
|
||||||
|
.map((p) => importState.resolve(p))
|
||||||
|
.filter(isRecord)
|
||||||
|
.filter((p) => stringAt(p, "in") === "header")
|
||||||
|
.map((p) => ({
|
||||||
|
enabled: p.required === true,
|
||||||
|
name: stringAt(p, "name") ?? "",
|
||||||
|
value: parameterExample(p, importState),
|
||||||
|
}))
|
||||||
|
.filter(({ name }) => name.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parameterExample(parameter: UnknownRecord, importState: ImportState): string {
|
||||||
|
const directExample = firstPresent(parameter.example, firstExampleValue(parameter.examples));
|
||||||
|
if (directExample != null) return stringifyExampleValue(directExample);
|
||||||
|
return stringifyExampleValue(schemaToExample(importState.resolve(parameter.schema), importState));
|
||||||
|
}
|
||||||
|
|
||||||
|
function importBody({
|
||||||
|
importState,
|
||||||
|
operation,
|
||||||
|
parameters,
|
||||||
|
spec,
|
||||||
|
}: {
|
||||||
|
importState: ImportState;
|
||||||
|
operation: UnknownRecord;
|
||||||
|
parameters: unknown[];
|
||||||
|
spec: UnknownRecord;
|
||||||
|
}): {
|
||||||
|
headers: HttpRequestHeader[];
|
||||||
|
body: Record<string, unknown>;
|
||||||
|
bodyType: string | null;
|
||||||
|
} {
|
||||||
|
const openApiRequestBody = importState.resolve(operation.requestBody);
|
||||||
|
if (isRecord(openApiRequestBody)) {
|
||||||
|
return importBodyFromContent(importState, toRecord(openApiRequestBody.content));
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyParameter = parameters
|
||||||
|
.map((p) => importState.resolve(p))
|
||||||
|
.find((p) => isRecord(p) && stringAt(p, "in") === "body");
|
||||||
|
if (isRecord(bodyParameter)) {
|
||||||
|
const contentType = toArray(spec.consumes).find((c): c is string => typeof c === "string");
|
||||||
|
const bodyType = contentType ?? "application/json";
|
||||||
|
return {
|
||||||
|
headers: [{ enabled: true, name: "Content-Type", value: bodyType }],
|
||||||
|
bodyType,
|
||||||
|
body: {
|
||||||
|
text: formatBodyText(
|
||||||
|
schemaToExample(importState.resolve(bodyParameter.schema), importState),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const formParameters = parameters
|
||||||
|
.map((p) => importState.resolve(p))
|
||||||
|
.filter(isRecord)
|
||||||
|
.filter((p) => stringAt(p, "in") === "formData");
|
||||||
|
if (formParameters.length > 0) {
|
||||||
|
const contentType =
|
||||||
|
toArray(spec.consumes).find((c): c is string => typeof c === "string") ??
|
||||||
|
(formParameters.some((p) => stringAt(p, "type") === "file")
|
||||||
|
? "multipart/form-data"
|
||||||
|
: "application/x-www-form-urlencoded");
|
||||||
|
return {
|
||||||
|
headers: [{ enabled: true, name: "Content-Type", value: contentType }],
|
||||||
|
bodyType: contentType,
|
||||||
|
body: {
|
||||||
|
form: formParameters.map((p) => ({
|
||||||
|
enabled: p.required === true,
|
||||||
|
name: stringAt(p, "name") ?? "",
|
||||||
|
value: parameterExample(p, importState),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { headers: [], body: {}, bodyType: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function importBodyFromContent(importState: ImportState, content: UnknownRecord) {
|
||||||
|
const contentType = chooseContentType(Object.keys(content));
|
||||||
|
if (contentType == null) return { headers: [], body: {}, bodyType: null };
|
||||||
|
|
||||||
|
const mediaType = toRecord(content[contentType]);
|
||||||
|
const example = mediaTypeExample(mediaType, importState);
|
||||||
|
|
||||||
|
if (
|
||||||
|
contentType === "application/x-www-form-urlencoded" ||
|
||||||
|
contentType === "multipart/form-data"
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
headers: [{ enabled: true, name: "Content-Type", value: contentType }],
|
||||||
|
bodyType: contentType,
|
||||||
|
body: {
|
||||||
|
form: schemaToFormParameters(importState.resolve(mediaType.schema), importState),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: [{ enabled: true, name: "Content-Type", value: contentType }],
|
||||||
|
bodyType: contentType === "application/octet-stream" ? "binary" : contentType,
|
||||||
|
body: contentType === "application/octet-stream" ? {} : { text: formatBodyText(example) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseContentType(contentTypes: string[]): string | null {
|
||||||
|
for (const preference of BODY_CONTENT_TYPE_PREFERENCE) {
|
||||||
|
const exact = contentTypes.find((c) => c.toLowerCase() === preference);
|
||||||
|
if (exact != null) return exact;
|
||||||
|
}
|
||||||
|
return contentTypes[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mediaTypeExample(mediaType: UnknownRecord, importState: ImportState): unknown {
|
||||||
|
const directExample = firstPresent(mediaType.example, firstExampleValue(mediaType.examples));
|
||||||
|
if (directExample != null) return directExample;
|
||||||
|
return schemaToExample(importState.resolve(mediaType.schema), importState);
|
||||||
|
}
|
||||||
|
|
||||||
|
function schemaToFormParameters(schema: unknown, importState: ImportState) {
|
||||||
|
const resolvedSchema = toRecord(importState.resolve(schema));
|
||||||
|
const required = toArray(resolvedSchema.required).filter(
|
||||||
|
(name): name is string => typeof name === "string",
|
||||||
|
);
|
||||||
|
const properties = Object.entries(toRecord(resolvedSchema.properties)).slice(
|
||||||
|
0,
|
||||||
|
MAX_EXAMPLE_PROPERTIES,
|
||||||
|
);
|
||||||
|
|
||||||
|
return properties.map(([name, property]) => {
|
||||||
|
const resolvedProperty = toRecord(importState.resolve(property));
|
||||||
|
const example = schemaToExample(resolvedProperty, importState);
|
||||||
|
const base = {
|
||||||
|
enabled: required.includes(name),
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
if (stringAt(resolvedProperty, "format") === "binary") {
|
||||||
|
return { ...base, file: "" };
|
||||||
|
}
|
||||||
|
return { ...base, value: stringifyExampleValue(example) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function schemaToExample(
|
||||||
|
schema: unknown,
|
||||||
|
importState: ImportState,
|
||||||
|
depth = 0,
|
||||||
|
visitedRefs = new Set<string>(),
|
||||||
|
): unknown {
|
||||||
|
if (depth > MAX_EXAMPLE_DEPTH) return {};
|
||||||
|
|
||||||
|
const resolved = importState.resolve(schema, visitedRefs);
|
||||||
|
if (!isRecord(resolved)) return "";
|
||||||
|
|
||||||
|
const explicitExample = firstPresent(
|
||||||
|
resolved.example,
|
||||||
|
firstExampleValue(resolved.examples),
|
||||||
|
resolved.default,
|
||||||
|
);
|
||||||
|
if (explicitExample != null) return explicitExample;
|
||||||
|
|
||||||
|
const enumValues = toArray(resolved.enum);
|
||||||
|
if (enumValues.length > 0) return enumValues[0];
|
||||||
|
|
||||||
|
const allOf = toArray(resolved.allOf);
|
||||||
|
if (allOf.length > 0) {
|
||||||
|
return allOf.reduce<UnknownRecord>((merged, childSchema) => {
|
||||||
|
const childExample = schemaToExample(childSchema, importState, depth + 1, visitedRefs);
|
||||||
|
return isRecord(childExample) ? { ...merged, ...childExample } : merged;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const oneOf = toArray(resolved.oneOf);
|
||||||
|
const anyOf = toArray(resolved.anyOf);
|
||||||
|
if (oneOf.length > 0 || anyOf.length > 0) {
|
||||||
|
return schemaToExample(oneOf[0] ?? anyOf[0], importState, depth + 1, visitedRefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = inferSchemaType(resolved);
|
||||||
|
if (type === "array") {
|
||||||
|
return [schemaToExample(resolved.items, importState, depth + 1, visitedRefs)];
|
||||||
|
}
|
||||||
|
if (type === "object") {
|
||||||
|
const required = toArray(resolved.required).filter(
|
||||||
|
(name): name is string => typeof name === "string",
|
||||||
|
);
|
||||||
|
const properties = Object.entries(toRecord(resolved.properties)).sort(([a], [b]) => {
|
||||||
|
const aRequired = required.includes(a);
|
||||||
|
const bRequired = required.includes(b);
|
||||||
|
return aRequired === bRequired ? 0 : aRequired ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
properties
|
||||||
|
.slice(0, MAX_EXAMPLE_PROPERTIES)
|
||||||
|
.map(([name, property]) => [
|
||||||
|
name,
|
||||||
|
schemaToExample(property, importState, depth + 1, visitedRefs),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === "integer" || type === "number") return 0;
|
||||||
|
if (type === "boolean") return false;
|
||||||
|
if (stringAt(resolved, "format") === "date-time") return "2026-01-01T00:00:00Z";
|
||||||
|
if (stringAt(resolved, "format") === "date") return "2026-01-01";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferSchemaType(schema: UnknownRecord): string {
|
||||||
|
const rawType = schema.type;
|
||||||
|
if (typeof rawType === "string") return rawType;
|
||||||
|
if (Array.isArray(rawType)) {
|
||||||
|
const nonNullType = rawType.find((t) => t !== "null");
|
||||||
|
if (typeof nonNullType === "string") return nonNullType;
|
||||||
|
}
|
||||||
|
if (isRecord(schema.properties) || isRecord(schema.additionalProperties)) return "object";
|
||||||
|
if (schema.items != null) return "array";
|
||||||
|
return "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
function importAuthentication({
|
||||||
|
importState,
|
||||||
|
operation,
|
||||||
|
spec,
|
||||||
|
}: {
|
||||||
|
importState: ImportState;
|
||||||
|
operation: UnknownRecord;
|
||||||
|
spec: UnknownRecord;
|
||||||
|
}): Pick<HttpRequest, "authentication" | "authenticationType"> {
|
||||||
|
const security = operation.security ?? spec.security;
|
||||||
|
if (!Array.isArray(security) || security.length === 0) {
|
||||||
|
return { authenticationType: null, authentication: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemes = {
|
||||||
|
...toRecord(toRecord(spec.components).securitySchemes),
|
||||||
|
...toRecord(spec.securityDefinitions),
|
||||||
|
};
|
||||||
|
for (const requirement of security) {
|
||||||
|
for (const schemeName of Object.keys(toRecord(requirement))) {
|
||||||
|
const scheme = toRecord(importState.resolve(schemes[schemeName]));
|
||||||
|
const type = stringAt(scheme, "type");
|
||||||
|
if (type === "apiKey") {
|
||||||
|
return {
|
||||||
|
authenticationType: "apikey",
|
||||||
|
authentication: {
|
||||||
|
location: stringAt(scheme, "in") === "query" ? "query" : "header",
|
||||||
|
key: stringAt(scheme, "name") ?? schemeName,
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === "http" && stringAt(scheme, "scheme")?.toLowerCase() === "basic") {
|
||||||
|
return {
|
||||||
|
authenticationType: "basic",
|
||||||
|
authentication: { username: "", password: "" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === "http" && stringAt(scheme, "scheme")?.toLowerCase() === "bearer") {
|
||||||
|
return {
|
||||||
|
authenticationType: "bearer",
|
||||||
|
authentication: { token: "", prefix: "Bearer" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { authenticationType: null, authentication: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeHeaders(...headerGroups: HttpRequestHeader[][]): HttpRequestHeader[] {
|
||||||
|
const headers: HttpRequestHeader[] = [];
|
||||||
|
for (const header of headerGroups.flat()) {
|
||||||
|
const existing = headers.find((h) => h.name.toLowerCase() === header.name.toLowerCase());
|
||||||
|
if (existing == null) {
|
||||||
|
headers.push(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBodyText(example: unknown): string {
|
||||||
|
return typeof example === "string" ? example : JSON.stringify(example, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyExampleValue(value: unknown): string {
|
||||||
|
if (value == null) return "";
|
||||||
|
if (typeof value === "string") return value;
|
||||||
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstExampleValue(examples: unknown): unknown {
|
||||||
|
const firstExample = Object.values(toRecord(examples))[0];
|
||||||
|
if (isRecord(firstExample) && "value" in firstExample) return firstExample.value;
|
||||||
|
return firstExample;
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstPresent(...values: unknown[]): unknown {
|
||||||
|
return values.find((value) => value !== undefined && value !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringAt(record: unknown, key: string): string | undefined {
|
||||||
|
const value = toRecord(record)[key];
|
||||||
|
return typeof value === "string" ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toArray(value: unknown): unknown[] {
|
||||||
|
return Array.isArray(value) ? value : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRecord(value: unknown): UnknownRecord {
|
||||||
|
return isRecord(value) ? value : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is UnknownRecord {
|
||||||
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPresent<T>(value: T | null | undefined): value is T {
|
||||||
|
return value != null && value !== "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recursively render all nested object properties */
|
||||||
|
function convertTemplateSyntax<T>(obj: T): T {
|
||||||
|
if (typeof obj === "string") {
|
||||||
|
// oxlint-disable-next-line no-template-curly-in-string -- Yaak template syntax
|
||||||
|
return obj.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") as T;
|
||||||
|
}
|
||||||
|
if (Array.isArray(obj) && obj != null) {
|
||||||
|
return obj.map(convertTemplateSyntax) as T;
|
||||||
|
}
|
||||||
|
if (typeof obj === "object" && obj != null) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)]),
|
||||||
|
) as T;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteUndefinedAttrs<T>(obj: T): T {
|
||||||
|
if (Array.isArray(obj) && obj != null) {
|
||||||
|
return obj.map(deleteUndefinedAttrs) as T;
|
||||||
|
}
|
||||||
|
if (typeof obj === "object" && obj != null) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(obj)
|
||||||
|
.filter(([, v]) => v !== undefined)
|
||||||
|
.map(([k, v]) => [k, deleteUndefinedAttrs(v)]),
|
||||||
|
) as T;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImportState {
|
||||||
|
readonly #spec: UnknownRecord;
|
||||||
|
readonly #idCount: Partial<Record<string, number>> = {};
|
||||||
|
#sortPriority = 0;
|
||||||
|
|
||||||
|
constructor(spec: UnknownRecord) {
|
||||||
|
this.#spec = spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateId(model: string): string {
|
||||||
|
this.#idCount[model] = (this.#idCount[model] ?? -1) + 1;
|
||||||
|
return `GENERATE_ID::${model.toUpperCase()}_${this.#idCount[model]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSortPriority(): number {
|
||||||
|
return this.#sortPriority++;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(value: unknown, visitedRefs = new Set<string>()): unknown {
|
||||||
|
if (!isRecord(value) || typeof value.$ref !== "string") return value;
|
||||||
|
if (visitedRefs.has(value.$ref)) return {};
|
||||||
|
|
||||||
|
const nextVisitedRefs = new Set(visitedRefs);
|
||||||
|
nextVisitedRefs.add(value.$ref);
|
||||||
|
|
||||||
|
if (!value.$ref.startsWith("#/")) return value;
|
||||||
|
|
||||||
|
const resolved = value.$ref
|
||||||
|
.slice(2)
|
||||||
|
.split("/")
|
||||||
|
.map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"))
|
||||||
|
.reduce<unknown>((current, part) => toRecord(current)[part], this.#spec);
|
||||||
|
|
||||||
|
return this.resolve(resolved, nextVisitedRefs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { convertPostman } from "@yaak/importer-postman/src";
|
||||||
|
import type { ImportPluginResponse } from "@yaakapp/api/lib/plugins/ImporterPlugin";
|
||||||
|
import { convert } from "openapi-to-postmanv2";
|
||||||
|
|
||||||
|
export async function convertOpenApiWithPostman(
|
||||||
|
contents: string,
|
||||||
|
): Promise<ImportPluginResponse | undefined> {
|
||||||
|
// oxlint-disable-next-line no-explicit-any
|
||||||
|
let postmanCollection: any;
|
||||||
|
try {
|
||||||
|
postmanCollection = await new Promise((resolve, reject) => {
|
||||||
|
// oxlint-disable-next-line no-explicit-any
|
||||||
|
convert({ type: "string", data: contents }, {}, (err, result: any) => {
|
||||||
|
if (err != null) reject(err);
|
||||||
|
|
||||||
|
if (Array.isArray(result.output) && result.output.length > 0) {
|
||||||
|
resolve(result.output[0].data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertPostman(JSON.stringify(postmanCollection));
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
|||||||
|
# Real-World OpenAPI Fixtures
|
||||||
|
|
||||||
|
These fixtures were copied from the public APIs.guru OpenAPI directory:
|
||||||
|
|
||||||
|
- `apis-guru.yaml`: https://api.apis.guru/v2/specs/apis.guru/2.2.0/openapi.yaml
|
||||||
|
- `httpbin.yaml`: https://api.apis.guru/v2/specs/httpbin.org/0.9.2/openapi.yaml
|
||||||
|
- `nasa-apod.yaml`: https://api.apis.guru/v2/specs/nasa.gov/apod/1.0.0/openapi.yaml
|
||||||
|
- `xkcd.yaml`: https://api.apis.guru/v2/specs/xkcd.com/1.0.0/openapi.yaml
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
openapi: 3.0.0
|
||||||
|
servers:
|
||||||
|
- url: https://api.apis.guru/v2
|
||||||
|
info:
|
||||||
|
contact:
|
||||||
|
email: mike.ralphson@gmail.com
|
||||||
|
name: APIs.guru
|
||||||
|
url: https://APIs.guru
|
||||||
|
description: |
|
||||||
|
Wikipedia for Web APIs. Repository of API definitions in OpenAPI format.
|
||||||
|
**Warning**: If you want to be notified about changes in advance please join our [Slack channel](https://join.slack.com/t/mermade/shared_invite/zt-g78g7xir-MLE_CTCcXCdfJfG3CJe9qA).
|
||||||
|
Client sample: [[Demo]](https://apis.guru/simple-ui) [[Repo]](https://github.com/APIs-guru/simple-ui)
|
||||||
|
license:
|
||||||
|
name: CC0 1.0
|
||||||
|
url: https://github.com/APIs-guru/openapi-directory#licenses
|
||||||
|
title: APIs.guru
|
||||||
|
version: 2.2.0
|
||||||
|
x-apisguru-categories:
|
||||||
|
- open_data
|
||||||
|
- developer_tools
|
||||||
|
x-logo:
|
||||||
|
url: https://api.apis.guru/v2/cache/logo/https_apis.guru_branding_logo_vertical.svg
|
||||||
|
x-origin:
|
||||||
|
- format: openapi
|
||||||
|
url: https://api.apis.guru/v2/openapi.yaml
|
||||||
|
version: "3.0"
|
||||||
|
x-providerName: apis.guru
|
||||||
|
x-tags:
|
||||||
|
- API
|
||||||
|
- Catalog
|
||||||
|
- Directory
|
||||||
|
- REST
|
||||||
|
- Swagger
|
||||||
|
- OpenAPI
|
||||||
|
externalDocs:
|
||||||
|
url: https://github.com/APIs-guru/openapi-directory/blob/master/API.md
|
||||||
|
security: []
|
||||||
|
tags:
|
||||||
|
- description: Actions relating to APIs in the collection
|
||||||
|
name: APIs
|
||||||
|
paths:
|
||||||
|
/list.json:
|
||||||
|
get:
|
||||||
|
description: |
|
||||||
|
List all APIs in the directory.
|
||||||
|
Returns links to the OpenAPI definitions for each API in the directory.
|
||||||
|
If API exist in multiple versions `preferred` one is explicitly marked.
|
||||||
|
Some basic info from the OpenAPI definition is cached inside each object.
|
||||||
|
This allows you to generate some simple views without needing to fetch the OpenAPI definition for each API.
|
||||||
|
operationId: listAPIs
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/APIs"
|
||||||
|
description: OK
|
||||||
|
summary: List all APIs
|
||||||
|
tags:
|
||||||
|
- APIs
|
||||||
|
/metrics.json:
|
||||||
|
get:
|
||||||
|
description: |
|
||||||
|
Some basic metrics for the entire directory.
|
||||||
|
Just stunning numbers to put on a front page and are intended purely for WoW effect :)
|
||||||
|
operationId: getMetrics
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Metrics"
|
||||||
|
description: OK
|
||||||
|
summary: Get basic metrics
|
||||||
|
tags:
|
||||||
|
- APIs
|
||||||
|
/providers.json:
|
||||||
|
get:
|
||||||
|
description: |
|
||||||
|
List all the providers in the directory
|
||||||
|
operationId: getProviders
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
items:
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
minItems: 1
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
description: OK
|
||||||
|
summary: List all providers
|
||||||
|
tags:
|
||||||
|
- APIs
|
||||||
|
"/specs/{provider}/{api}.json":
|
||||||
|
get:
|
||||||
|
description: Returns the API entry for one specific version of an API where there is no serviceName.
|
||||||
|
operationId: getAPI
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/provider"
|
||||||
|
- $ref: "#/components/parameters/api"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/API"
|
||||||
|
description: OK
|
||||||
|
summary: Retrieve one version of a particular API
|
||||||
|
tags:
|
||||||
|
- APIs
|
||||||
|
"/specs/{provider}/{service}/{api}.json":
|
||||||
|
get:
|
||||||
|
description: Returns the API entry for one specific version of an API where there is a serviceName.
|
||||||
|
operationId: getServiceAPI
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/provider"
|
||||||
|
- in: path
|
||||||
|
name: service
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
example: graph
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
- $ref: "#/components/parameters/api"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/API"
|
||||||
|
description: OK
|
||||||
|
summary: Retrieve one version of a particular API with a serviceName.
|
||||||
|
tags:
|
||||||
|
- APIs
|
||||||
|
"/{provider}.json":
|
||||||
|
get:
|
||||||
|
description: |
|
||||||
|
List all APIs in the directory for a particular providerName
|
||||||
|
Returns links to the individual API entry for each API.
|
||||||
|
operationId: getProvider
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/provider"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/APIs"
|
||||||
|
description: OK
|
||||||
|
summary: List all APIs for a particular provider
|
||||||
|
tags:
|
||||||
|
- APIs
|
||||||
|
"/{provider}/services.json":
|
||||||
|
get:
|
||||||
|
description: |
|
||||||
|
List all serviceNames in the directory for a particular providerName
|
||||||
|
operationId: getServices
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/provider"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
items:
|
||||||
|
minLength: 0
|
||||||
|
type: string
|
||||||
|
minItems: 1
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
description: OK
|
||||||
|
summary: List all serviceNames for a particular provider
|
||||||
|
tags:
|
||||||
|
- APIs
|
||||||
|
components:
|
||||||
|
parameters:
|
||||||
|
api:
|
||||||
|
in: path
|
||||||
|
name: api
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
example: 2.1.0
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
provider:
|
||||||
|
in: path
|
||||||
|
name: provider
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
example: apis.guru
|
||||||
|
maxLength: 255
|
||||||
|
minLength: 1
|
||||||
|
type: string
|
||||||
|
schemas:
|
||||||
|
API:
|
||||||
|
additionalProperties: false
|
||||||
|
description: Meta information about API
|
||||||
|
properties:
|
||||||
|
added:
|
||||||
|
description: Timestamp when the API was first added to the directory
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
preferred:
|
||||||
|
description: Recommended version
|
||||||
|
type: string
|
||||||
|
versions:
|
||||||
|
additionalProperties:
|
||||||
|
$ref: "#/components/schemas/ApiVersion"
|
||||||
|
description: List of supported versions of the API
|
||||||
|
minProperties: 1
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- added
|
||||||
|
- preferred
|
||||||
|
- versions
|
||||||
|
type: object
|
||||||
|
APIs:
|
||||||
|
additionalProperties:
|
||||||
|
$ref: "#/components/schemas/API"
|
||||||
|
description: |
|
||||||
|
List of API details.
|
||||||
|
It is a JSON object with API IDs(`<provider>[:<service>]`) as keys.
|
||||||
|
example:
|
||||||
|
googleapis.com:drive:
|
||||||
|
added: 2015-02-22T20:00:45.000Z
|
||||||
|
preferred: v3
|
||||||
|
versions:
|
||||||
|
v2:
|
||||||
|
added: 2015-02-22T20:00:45.000Z
|
||||||
|
info:
|
||||||
|
title: Drive
|
||||||
|
version: v2
|
||||||
|
x-apiClientRegistration:
|
||||||
|
url: https://console.developers.google.com
|
||||||
|
x-logo:
|
||||||
|
url: https://api.apis.guru/v2/cache/logo/https_www.gstatic.com_images_icons_material_product_2x_drive_32dp.png
|
||||||
|
x-origin:
|
||||||
|
format: google
|
||||||
|
url: https://www.googleapis.com/discovery/v1/apis/drive/v2/rest
|
||||||
|
version: v1
|
||||||
|
x-preferred: false
|
||||||
|
x-providerName: googleapis.com
|
||||||
|
x-serviceName: drive
|
||||||
|
swaggerUrl: https://api.apis.guru/v2/specs/googleapis.com/drive/v2/swagger.json
|
||||||
|
swaggerYamlUrl: https://api.apis.guru/v2/specs/googleapis.com/drive/v2/swagger.yaml
|
||||||
|
updated: 2016-06-17T00:21:44.000Z
|
||||||
|
v3:
|
||||||
|
added: 2015-12-12T00:25:13.000Z
|
||||||
|
info:
|
||||||
|
title: Drive
|
||||||
|
version: v3
|
||||||
|
x-apiClientRegistration:
|
||||||
|
url: https://console.developers.google.com
|
||||||
|
x-logo:
|
||||||
|
url: https://api.apis.guru/v2/cache/logo/https_www.gstatic.com_images_icons_material_product_2x_drive_32dp.png
|
||||||
|
x-origin:
|
||||||
|
format: google
|
||||||
|
url: https://www.googleapis.com/discovery/v1/apis/drive/v3/rest
|
||||||
|
version: v1
|
||||||
|
x-preferred: true
|
||||||
|
x-providerName: googleapis.com
|
||||||
|
x-serviceName: drive
|
||||||
|
swaggerUrl: https://api.apis.guru/v2/specs/googleapis.com/drive/v3/swagger.json
|
||||||
|
swaggerYamlUrl: https://api.apis.guru/v2/specs/googleapis.com/drive/v3/swagger.yaml
|
||||||
|
updated: 2016-06-17T00:21:44.000Z
|
||||||
|
minProperties: 1
|
||||||
|
type: object
|
||||||
|
ApiVersion:
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
added:
|
||||||
|
description: Timestamp when the version was added
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
externalDocs:
|
||||||
|
description: Copy of `externalDocs` section from OpenAPI definition
|
||||||
|
minProperties: 1
|
||||||
|
type: object
|
||||||
|
info:
|
||||||
|
description: Copy of `info` section from OpenAPI definition
|
||||||
|
minProperties: 1
|
||||||
|
type: object
|
||||||
|
link:
|
||||||
|
description: Link to the individual API entry for this API
|
||||||
|
format: url
|
||||||
|
type: string
|
||||||
|
openapiVer:
|
||||||
|
description: The value of the `openapi` or `swagger` property of the source definition
|
||||||
|
type: string
|
||||||
|
swaggerUrl:
|
||||||
|
description: URL to OpenAPI definition in JSON format
|
||||||
|
format: url
|
||||||
|
type: string
|
||||||
|
swaggerYamlUrl:
|
||||||
|
description: URL to OpenAPI definition in YAML format
|
||||||
|
format: url
|
||||||
|
type: string
|
||||||
|
updated:
|
||||||
|
description: Timestamp when the version was updated
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- added
|
||||||
|
- updated
|
||||||
|
- swaggerUrl
|
||||||
|
- swaggerYamlUrl
|
||||||
|
- info
|
||||||
|
- openapiVer
|
||||||
|
type: object
|
||||||
|
Metrics:
|
||||||
|
additionalProperties: false
|
||||||
|
description: List of basic metrics
|
||||||
|
example:
|
||||||
|
datasets: []
|
||||||
|
fixedPct: 22
|
||||||
|
fixes: 81119
|
||||||
|
invalid: 598
|
||||||
|
issues: 28
|
||||||
|
numAPIs: 2501
|
||||||
|
numDrivers: 10
|
||||||
|
numEndpoints: 106448
|
||||||
|
numProviders: 659
|
||||||
|
numSpecs: 3329
|
||||||
|
stars: 2429
|
||||||
|
thisWeek:
|
||||||
|
added: 45
|
||||||
|
updated: 171
|
||||||
|
unofficial: 25
|
||||||
|
unreachable: 123
|
||||||
|
properties:
|
||||||
|
datasets:
|
||||||
|
description: Data used for charting etc
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
fixedPct:
|
||||||
|
description: Percentage of all APIs where auto fixes have been applied
|
||||||
|
type: integer
|
||||||
|
fixes:
|
||||||
|
description: Total number of fixes applied across all APIs
|
||||||
|
type: integer
|
||||||
|
invalid:
|
||||||
|
description: Number of newly invalid APIs
|
||||||
|
type: integer
|
||||||
|
issues:
|
||||||
|
description: Open GitHub issues on our main repo
|
||||||
|
type: integer
|
||||||
|
numAPIs:
|
||||||
|
description: Number of unique APIs
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
numDrivers:
|
||||||
|
description: Number of methods of API retrieval
|
||||||
|
type: integer
|
||||||
|
numEndpoints:
|
||||||
|
description: Total number of endpoints inside all definitions
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
numProviders:
|
||||||
|
description: Number of API providers in directory
|
||||||
|
type: integer
|
||||||
|
numSpecs:
|
||||||
|
description: Number of API definitions including different versions of the same API
|
||||||
|
minimum: 1
|
||||||
|
type: integer
|
||||||
|
stars:
|
||||||
|
description: GitHub stars for our main repo
|
||||||
|
type: integer
|
||||||
|
thisWeek:
|
||||||
|
description: Summary totals for the last 7 days
|
||||||
|
properties:
|
||||||
|
added:
|
||||||
|
description: APIs added in the last week
|
||||||
|
type: integer
|
||||||
|
updated:
|
||||||
|
description: APIs updated in the last week
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
unofficial:
|
||||||
|
description: Number of unofficial APIs
|
||||||
|
type: integer
|
||||||
|
unreachable:
|
||||||
|
description: Number of unreachable (4XX,5XX status) APIs
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- numSpecs
|
||||||
|
- numAPIs
|
||||||
|
- numEndpoints
|
||||||
|
type: object
|
||||||
|
x-optic-standard: "@febf8ac6-ee67-4565-b45a-5c85a469dca7/Fz6KU3_wMIO5iJ6_VUZ30"
|
||||||
|
x-optic-url: https://app.useoptic.com/organizations/febf8ac6-ee67-4565-b45a-5c85a469dca7/apis/_0fKWqUvhs9ssYNkq1k-c
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
|||||||
|
openapi: 3.0.0
|
||||||
|
servers:
|
||||||
|
- url: https://api.nasa.gov/planetary
|
||||||
|
- url: http://api.nasa.gov/planetary
|
||||||
|
info:
|
||||||
|
contact:
|
||||||
|
email: evan.t.yates@nasa.gov
|
||||||
|
description: This endpoint structures the APOD imagery and associated metadata so that it can be repurposed for other applications. In addition, if the concept_tags parameter is set to True, then keywords derived from the image explanation are returned. These keywords could be used as auto-generated hashtags for twitter or instagram feeds; but generally help with discoverability of relevant imagery
|
||||||
|
license:
|
||||||
|
name: Apache 2.0
|
||||||
|
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
title: APOD
|
||||||
|
version: 1.0.0
|
||||||
|
x-apisguru-categories:
|
||||||
|
- media
|
||||||
|
- open_data
|
||||||
|
x-origin:
|
||||||
|
- format: swagger
|
||||||
|
url: https://raw.githubusercontent.com/nasa/api-docs/gh-pages/assets/json/APOD
|
||||||
|
version: "2.0"
|
||||||
|
x-providerName: nasa.gov
|
||||||
|
x-serviceName: apod
|
||||||
|
x-logo:
|
||||||
|
url: https://api.apis.guru/v2/cache/logo/https_apis.guru_assets_images_no-logo.svg
|
||||||
|
tags:
|
||||||
|
- description: An example tag
|
||||||
|
externalDocs:
|
||||||
|
description: Here's a link
|
||||||
|
url: https://example.com
|
||||||
|
name: request tag
|
||||||
|
paths:
|
||||||
|
/apod:
|
||||||
|
get:
|
||||||
|
description: Returns the picture of the day
|
||||||
|
parameters:
|
||||||
|
- description: The date of the APOD image to retrieve
|
||||||
|
in: query
|
||||||
|
name: date
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- description: Retrieve the URL for the high resolution image
|
||||||
|
in: query
|
||||||
|
name: hd
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
x-thing: ok
|
||||||
|
type: array
|
||||||
|
description: successful operation
|
||||||
|
"400":
|
||||||
|
description: Date must be between Jun 16, 1995 and Mar 28, 2019.
|
||||||
|
security:
|
||||||
|
- api_key: []
|
||||||
|
summary: Returns images
|
||||||
|
tags:
|
||||||
|
- request tag
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
api_key:
|
||||||
|
in: query
|
||||||
|
name: api_key
|
||||||
|
type: apiKey
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
openapi: 3.0.0
|
||||||
|
servers:
|
||||||
|
- url: http://xkcd.com/
|
||||||
|
info:
|
||||||
|
description: Webcomic of romance, sarcasm, math, and language.
|
||||||
|
title: XKCD
|
||||||
|
version: 1.0.0
|
||||||
|
x-apisguru-categories:
|
||||||
|
- media
|
||||||
|
x-logo:
|
||||||
|
url: https://api.apis.guru/v2/cache/logo/http_imgs.xkcd.com_static_terrible_small_logo.png
|
||||||
|
x-origin:
|
||||||
|
- format: openapi
|
||||||
|
url: https://raw.githubusercontent.com/APIs-guru/unofficial_openapi_specs/master/xkcd.com/1.0.0/openapi.yaml
|
||||||
|
version: "3.0"
|
||||||
|
x-providerName: xkcd.com
|
||||||
|
x-tags:
|
||||||
|
- humor
|
||||||
|
- comics
|
||||||
|
x-unofficialSpec: true
|
||||||
|
externalDocs:
|
||||||
|
url: https://xkcd.com/json.html
|
||||||
|
paths:
|
||||||
|
/info.0.json:
|
||||||
|
get:
|
||||||
|
description: |
|
||||||
|
Fetch current comic and metadata.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
"*/*":
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/comic"
|
||||||
|
description: OK
|
||||||
|
"/{comicId}/info.0.json":
|
||||||
|
get:
|
||||||
|
description: |
|
||||||
|
Fetch comics and metadata by comic id.
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: comicId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
"*/*":
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/comic"
|
||||||
|
description: OK
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
comic:
|
||||||
|
properties:
|
||||||
|
alt:
|
||||||
|
type: string
|
||||||
|
day:
|
||||||
|
type: string
|
||||||
|
img:
|
||||||
|
type: string
|
||||||
|
link:
|
||||||
|
type: string
|
||||||
|
month:
|
||||||
|
type: string
|
||||||
|
news:
|
||||||
|
type: string
|
||||||
|
num:
|
||||||
|
type: number
|
||||||
|
safe_title:
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
transcript:
|
||||||
|
type: string
|
||||||
|
year:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
@@ -5,7 +5,13 @@ import { convertOpenApi } from "../src";
|
|||||||
|
|
||||||
describe("importer-openapi", () => {
|
describe("importer-openapi", () => {
|
||||||
const p = path.join(__dirname, "fixtures");
|
const p = path.join(__dirname, "fixtures");
|
||||||
const fixtures = fs.readdirSync(p);
|
const fixtures = fs.readdirSync(p).filter((fixture) => {
|
||||||
|
return fs.statSync(path.join(p, fixture)).isFile();
|
||||||
|
});
|
||||||
|
const realWorldFixturesPath = path.join(p, "real-world");
|
||||||
|
const realWorldFixtures = fs
|
||||||
|
.readdirSync(realWorldFixturesPath)
|
||||||
|
.filter((fixture) => fixture.endsWith(".yaml"));
|
||||||
|
|
||||||
test("Maps operation description to request description", async () => {
|
test("Maps operation description to request description", async () => {
|
||||||
const imported = await convertOpenApi(
|
const imported = await convertOpenApi(
|
||||||
@@ -25,7 +31,195 @@ describe("importer-openapi", () => {
|
|||||||
|
|
||||||
expect(imported?.resources.httpRequests).toEqual([
|
expect(imported?.resources.httpRequests).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
description: "Lijst van klanten",
|
description: expect.stringContaining("Lijst van klanten"),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Imports requests directly from OpenAPI details", async () => {
|
||||||
|
const imported = await convertOpenApi(
|
||||||
|
JSON.stringify({
|
||||||
|
openapi: "3.0.0",
|
||||||
|
info: { title: "Native Import Test", version: "1.0.0" },
|
||||||
|
servers: [
|
||||||
|
{ url: "https://api.example.com/{version}", variables: { version: { default: "v1" } } },
|
||||||
|
],
|
||||||
|
tags: [{ name: "accounts", description: "Account operations" }],
|
||||||
|
paths: {
|
||||||
|
"/accounts/{accountId}/members": {
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "accountId",
|
||||||
|
in: "path",
|
||||||
|
required: true,
|
||||||
|
description: "Account identifier",
|
||||||
|
schema: { type: "string", example: "acct_123" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
post: {
|
||||||
|
tags: ["accounts"],
|
||||||
|
summary: "Create member",
|
||||||
|
operationId: "createMember",
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "include",
|
||||||
|
in: "query",
|
||||||
|
description: "Related resources to include",
|
||||||
|
schema: { type: "string", enum: ["roles"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X-Trace-Id",
|
||||||
|
in: "header",
|
||||||
|
schema: { type: "string", example: "trace-123" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
security: [{ tokenAuth: [] }],
|
||||||
|
requestBody: {
|
||||||
|
description: "Member payload",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: { $ref: "#/components/schemas/MemberInput" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
"201": { description: "Created" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
tokenAuth: { type: "http", scheme: "bearer" },
|
||||||
|
},
|
||||||
|
schemas: {
|
||||||
|
MemberInput: {
|
||||||
|
type: "object",
|
||||||
|
required: ["email"],
|
||||||
|
properties: {
|
||||||
|
email: { type: "string", example: "me@example.com" },
|
||||||
|
admin: { type: "boolean", default: false },
|
||||||
|
primaryContact: { $ref: "#/components/schemas/Contact" },
|
||||||
|
secondaryContact: { $ref: "#/components/schemas/Contact" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Contact: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string", example: "Taylor" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(imported?.resources.folders).toEqual([
|
||||||
|
expect.objectContaining({ name: "accounts", description: "Account operations" }),
|
||||||
|
]);
|
||||||
|
expect(imported?.resources.environments).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "Global Variables",
|
||||||
|
variables: [{ name: "baseUrl", value: "https://api.example.com/v1" }],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(imported?.resources.httpRequests).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "Create member",
|
||||||
|
method: "POST",
|
||||||
|
url: "${[baseUrl]}/accounts/:accountId/members",
|
||||||
|
authenticationType: "bearer",
|
||||||
|
authentication: { token: "", prefix: "Bearer" },
|
||||||
|
bodyType: "application/json",
|
||||||
|
body: {
|
||||||
|
text: JSON.stringify(
|
||||||
|
{
|
||||||
|
email: "me@example.com",
|
||||||
|
admin: false,
|
||||||
|
primaryContact: { name: "Taylor" },
|
||||||
|
secondaryContact: { name: "Taylor" },
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
headers: expect.arrayContaining([
|
||||||
|
{ enabled: false, name: "X-Trace-Id", value: "trace-123" },
|
||||||
|
{ enabled: true, name: "Content-Type", value: "application/json" },
|
||||||
|
]),
|
||||||
|
urlParameters: [
|
||||||
|
{ enabled: true, name: ":accountId", value: "acct_123" },
|
||||||
|
{ enabled: false, name: "include", value: "roles" },
|
||||||
|
],
|
||||||
|
description: expect.stringContaining("Operation ID: createMember"),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(imported?.resources.httpRequests[0]?.description).toContain("Member payload");
|
||||||
|
expect(imported?.resources.httpRequests[0]?.description).toContain("201: Created");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Handles large schemas without the Postman converter path", async () => {
|
||||||
|
const paths: Record<string, unknown> = {};
|
||||||
|
for (let i = 0; i < 500; i++) {
|
||||||
|
paths[`/zones/{zoneId}/resources/${i}`] = {
|
||||||
|
get: {
|
||||||
|
tags: ["zones"],
|
||||||
|
summary: `Read resource ${i}`,
|
||||||
|
parameters: [
|
||||||
|
{ name: "zoneId", in: "path", required: true, schema: { type: "string" } },
|
||||||
|
{ name: "page", in: "query", schema: { type: "integer", default: 1 } },
|
||||||
|
],
|
||||||
|
responses: {
|
||||||
|
"200": {
|
||||||
|
description: "OK",
|
||||||
|
content: {
|
||||||
|
"application/json": { schema: { $ref: "#/components/schemas/Resource" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const imported = await convertOpenApi(
|
||||||
|
JSON.stringify({
|
||||||
|
openapi: "3.1.0",
|
||||||
|
info: { title: "Large API", version: "1.0.0" },
|
||||||
|
servers: [{ url: "https://api.example.com/client/v4" }],
|
||||||
|
tags: [{ name: "zones" }],
|
||||||
|
paths,
|
||||||
|
components: {
|
||||||
|
schemas: {
|
||||||
|
Resource: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
metadata: { $ref: "#/components/schemas/Metadata" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
createdOn: { type: "string", format: "date-time" },
|
||||||
|
tags: { type: "array", items: { type: "string" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(imported?.resources.httpRequests.length).toBe(500);
|
||||||
|
expect(imported?.resources.httpRequests[499]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "Read resource 499",
|
||||||
|
url: "${[baseUrl]}/zones/:zoneId/resources/499",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(imported?.resources.environments).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: [{ name: "baseUrl", value: "https://api.example.com/client/v4" }],
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -46,7 +240,15 @@ describe("importer-openapi", () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
expect(imported?.resources.httpRequests.length).toBe(19);
|
expect(imported?.resources.httpRequests.length).toBe(19);
|
||||||
expect(imported?.resources.folders.length).toBe(7);
|
expect(imported?.resources.folders.map((f) => f.name)).toEqual(["pet", "store", "user"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fixture of realWorldFixtures) {
|
||||||
|
test(`Snapshots real-world fixture ${fixture}`, async () => {
|
||||||
|
const contents = fs.readFileSync(path.join(realWorldFixturesPath, fixture), "utf-8");
|
||||||
|
const imported = await convertOpenApi(contents);
|
||||||
|
expect(imported).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { describe, expect, test } from "vite-plus/test";
|
||||||
|
import { convertOpenApiWithPostman } from "../src/legacy";
|
||||||
|
|
||||||
|
describe("importer-openapi legacy converter", () => {
|
||||||
|
const realWorldFixturesPath = path.join(__dirname, "fixtures", "real-world");
|
||||||
|
const realWorldFixtures = fs
|
||||||
|
.readdirSync(realWorldFixturesPath)
|
||||||
|
.filter((fixture) => fixture.endsWith(".yaml"));
|
||||||
|
|
||||||
|
for (const fixture of realWorldFixtures) {
|
||||||
|
test(`Snapshots legacy Postman-converter output for ${fixture}`, async () => {
|
||||||
|
const contents = fs.readFileSync(path.join(realWorldFixturesPath, fixture), "utf-8");
|
||||||
|
const imported = await convertOpenApiWithPostman(contents);
|
||||||
|
expect(imported).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user