openapi-typescript
Version:
Convert OpenAPI 3.0 & 3.1 schemas to TypeScript
157 lines (147 loc) • 4.9 kB
text/typescript
import ts from "typescript";
import {
addJSDocComment,
oapiRef,
stringToAST,
tsModifiers,
tsPropertyIndex,
} from "../lib/ts.js";
import { createRef, debug, getEntries } from "../lib/utils.js";
import {
GlobalContext,
OperationObject,
ParameterObject,
PathItemObject,
PathsObject,
ReferenceObject,
} from "../types.js";
import transformPathItemObject, { Method } from "./path-item-object.js";
const PATH_PARAM_RE = /\{[^}]+\}/g;
/**
* Transform the PathsObject node (4.8.8)
* @see https://spec.openapis.org/oas/v3.1.0#operation-object
*/
export default function transformPathsObject(
pathsObject: PathsObject,
ctx: GlobalContext,
): ts.TypeNode {
const type: ts.TypeElement[] = [];
for (const [url, pathItemObject] of getEntries(pathsObject, ctx)) {
if (!pathItemObject || typeof pathItemObject !== "object") {
continue;
}
const pathT = performance.now();
// handle $ref
if ("$ref" in pathItemObject) {
const property = ts.factory.createPropertySignature(
/* modifiers */ tsModifiers({ readonly: ctx.immutable }),
/* name */ tsPropertyIndex(url),
/* questionToken */ undefined,
/* type */ oapiRef(pathItemObject.$ref),
);
addJSDocComment(pathItemObject, property);
} else {
const pathItemType = transformPathItemObject(pathItemObject, {
path: createRef(["paths", url]),
ctx,
});
// pathParamsAsTypes
if (ctx.pathParamsAsTypes && url.includes("{")) {
const pathParams = extractPathParams(pathItemObject, ctx);
const matches = url.match(PATH_PARAM_RE);
let rawPath = `\`${url}\``;
if (matches) {
/* eslint-disable @typescript-eslint/no-explicit-any */
for (const match of matches) {
const paramName = match.slice(1, -1);
const param = pathParams[paramName];
if (!param) {
rawPath = rawPath.replace(match, "${string}");
} else {
rawPath = rawPath.replace(
match,
`$\{${(param.schema as any)?.type ?? "string"}}`,
);
}
}
// note: creating a string template literal’s AST manually is hard!
// just pass an arbitrary string to TS
const pathType = (stringToAST(rawPath)[0] as any)?.expression;
if (pathType) {
type.push(
ts.factory.createIndexSignature(
/* modifiers */ tsModifiers({ readonly: ctx.immutable }),
/* parameters */ [
ts.factory.createParameterDeclaration(
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
/* name */ "path",
/* questionToken */ undefined,
/* type */ pathType,
/* initializer */ undefined,
),
],
/* type */ pathItemType,
),
);
continue;
}
/* eslint-enable @typescript-eslint/no-explicit-any */
}
}
type.push(
ts.factory.createPropertySignature(
/* modifiers */ tsModifiers({ readonly: ctx.immutable }),
/* name */ tsPropertyIndex(url),
/* questionToken */ undefined,
/* type */ pathItemType,
),
);
debug(`Transformed path "${url}"`, "ts", performance.now() - pathT);
}
}
return ts.factory.createTypeLiteralNode(type);
}
function extractPathParams(pathItemObject: PathItemObject, ctx: GlobalContext) {
const params: Record<string, ParameterObject> = {};
for (const p of pathItemObject.parameters ?? []) {
const resolved =
"$ref" in p && p.$ref
? ctx.resolve<ParameterObject>(p.$ref)
: (p as ParameterObject);
if (resolved && resolved.in === "path") {
params[resolved.name] = resolved;
}
}
for (const method of [
"get",
"put",
"post",
"delete",
"options",
"head",
"patch",
"trace",
] as Method[]) {
if (!(method in pathItemObject)) {
continue;
}
const resolvedMethod = (pathItemObject[method] as ReferenceObject).$ref
? ctx.resolve<OperationObject>(
(pathItemObject[method] as ReferenceObject).$ref,
)
: (pathItemObject[method] as OperationObject);
if (resolvedMethod?.parameters) {
for (const p of resolvedMethod.parameters) {
const resolvedParam =
"$ref" in p && p.$ref
? ctx.resolve<ParameterObject>(p.$ref)
: (p as ParameterObject);
if (resolvedParam && resolvedParam.in === "path") {
params[resolvedParam.name] = resolvedParam;
}
}
}
}
return params;
}