Skip to content
Merged
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
30 changes: 29 additions & 1 deletion __tests__/__snapshots__/nullables.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,34 @@ export type UploadDataCommandInput = UploadDataCommandParams;
`;

exports[`header parameters 2`] = `
"import { Command } from "@block65/rest-client";
import type { UploadDataCommandHeader, UploadDataCommandInput, UploadStatus } from "./types.js";

/**
* Tagged template literal that applies encodeURIComponent to all interpolated
* values, protecting path integrity from characters like \`/\` and \`#\`.
* @example encodePath\`/users/\${userId}\` // "/users/foo%2Fbar"
*/
function encodePath(strings: TemplateStringsArray, ...values: string[]): string {
return String.raw({ raw: strings }, ...values.map(encodeURIComponent));
}

/**
* UploadDataCommand
*
*/
export class UploadDataCommand extends Command<UploadDataCommandInput, UploadStatus, never, UploadDataCommandHeader> {
public override method = "post" as const;

constructor(input: UploadDataCommandInput, headers: UploadDataCommandHeader) {
const {uploadId } = input;
super(encodePath\`/uploads/\${uploadId}\`, undefined, undefined, headers);
}
}
"
`;

exports[`header parameters 3`] = `
"import * as v from "valibot";

export const uploadStatusSchema = v.picklist(["pending", "complete"]);
Expand All @@ -47,7 +75,7 @@ export const uploadDataCommandHeaderSchema = v.object({
"
`;

exports[`header parameters 3`] = `
exports[`header parameters 4`] = `
"import { validator } from "hono/validator";
import * as v from "valibot";
import { PublicValibotHonoError } from "@block65/rest-client";
Expand Down
1 change: 1 addition & 0 deletions __tests__/nullables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ test("header parameters", async () => {
);

expect(result.typesFile.getText()).toMatchSnapshot();
expect(result.commandsFile.getText()).toMatchSnapshot();
expect(result.valibotFile.getText()).toMatchSnapshot();
expect(result.honoValibotFile.getText()).toMatchSnapshot();
});
48 changes: 42 additions & 6 deletions lib/process-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,19 @@ export async function processOpenApiDocument(
?.addTypeArgument(queryType.getName());
}

// headers type argument (4th generic on Command)
if (headerType) {
// fill in query slot if missing
if (!queryType) {
commandClassDeclaration
.getExtends()
?.addTypeArgument(neverKeyword);
}
commandClassDeclaration
.getExtends()
?.addTypeArgument(headerType.getName());
}

const hasPathParams = path.includes("{");
const pathname = hasPathParams
? `encodePath\`${path.replaceAll(/{/g, "${")}\``
Expand All @@ -967,7 +980,9 @@ export async function processOpenApiDocument(
!isUnspecifiedKeyword(paramsType) &&
pathParameters.length > 0;

if (hasNonJsonBody || hasJsonBody || hasQuery || hasParams) {
const hasHeaders = !!headerType && headerParameters.length > 0;

if (hasNonJsonBody || hasJsonBody || hasQuery || hasParams || hasHeaders) {
const ctor = commandClassDeclaration.addConstructor();

const queryParameterNames = queryParameters
Expand All @@ -989,6 +1004,13 @@ export async function processOpenApiDocument(
type: inputType.getName(),
});

if (hasHeaders) {
ctor.addParameter({
name: "headers",
type: headerType.getName(),
});
}

ctor.addStatements([
{
kind: StructureKind.VariableStatement,
Expand Down Expand Up @@ -1034,6 +1056,8 @@ export async function processOpenApiDocument(
SyntaxKind.CallExpression,
);

const headersArg = hasHeaders ? "headers" : undefined;

// type narrowing
if (Node.isCallExpression(callExpr)) {
if (hasJsonBody) {
Expand All @@ -1042,23 +1066,35 @@ export async function processOpenApiDocument(
`jsonStringify(${inputBodyName})`,
...(hasQuery
? [`stripUndefined({${queryParameterNames.join(", ")}})`]
: []),
: hasHeaders
? [emptyKeyword]
: []),
...(headersArg ? [headersArg] : []),
]);
} else if (hasNonJsonBody) {
callExpr.addArguments([
pathname,
nonJsonBodyPropName,
...(hasQuery
? [`stripUndefined({${queryParameterNames.join(", ")}})`]
: []),
: hasHeaders
? [emptyKeyword]
: []),
...(headersArg ? [headersArg] : []),
]);
} else if (hasQuery) {
callExpr.addArguments([
pathname,
emptyKeyword,
...(hasQuery
? [`stripUndefined({${queryParameterNames.join(", ")}})`]
: []),
`stripUndefined({${queryParameterNames.join(", ")}})`,
...(headersArg ? [headersArg] : []),
]);
} else if (hasHeaders && headersArg) {
callExpr.addArguments([
pathname,
emptyKeyword,
emptyKeyword,
headersArg,
]);
} else {
callExpr.addArguments([pathname]);
Expand Down
Loading