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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ carrying the `style` and `explode` each parameter declares:
import { listPetsQueryParams } from "./generated/hono.ts";
```

The client half is already handled: `@block65/rest-client` encodes each
parameter from the `queryStyles` the generated command carries.
The client half is already handled: a generated command names the
`@block65/rest-client` serializer for the style its document states, and
inherits `formExplodeSerializer` — form with explode, the OpenAPI default —
when it names none. One serializer covers a whole operation, so an operation
whose query parameters need two of them stops generation.

## Linting generated output

Expand Down
36 changes: 36 additions & 0 deletions __tests__/__snapshots__/query-serializer.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`sortQuery orders the generated query in the URL > findPets 1`] = `"https://example.com/api/pets?limit=10&tags=cat&tags=dog"`;

exports[`sortQuery orders the generated query in the URL > imageCreate 1`] = `"https://example.com/v1.43/images/create?changes=ENV%20a%3D1,ENV%20b%3D2&fromImage=alpine&platform=linux%2Famd64&tag=latest"`;

exports[`the generated query reaches the URL in document order > findPets 1`] = `"https://example.com/api/pets?tags=cat&tags=dog&limit=10"`;

exports[`the generated query reaches the URL in document order > imageCreate 1`] = `"https://example.com/v1.43/images/create?fromImage=alpine&tag=latest&changes=ENV%20a%3D1,ENV%20b%3D2&platform=linux%2Famd64"`;

exports[`the source of a command that names a serializer 1`] = `
"import { Command, stripUndefined, deepObjectSerializer } from "@block65/rest-client";

export class ListThingsCommand extends Command<UndefinedOnPartialDeep<ListThingsCommandInput>, ListThingsCommandOutput, ListThingsCommandQuery> {
public override method = "get" as const;
public override querySerializer = deepObjectSerializer;

constructor(input?: UndefinedOnPartialDeep<ListThingsCommandInput>) {
const {limit, filter } = input ?? {};
super("/things", undefined, stripUndefined({limit, filter}));
}
}"
`;

exports[`the source of a command that names none 1`] = `
"import { Command, stripUndefined } from "@block65/rest-client";

export class ListThingsCommand extends Command<UndefinedOnPartialDeep<ListThingsCommandInput>, ListThingsCommandOutput, ListThingsCommandQuery> {
public override method = "get" as const;

constructor(input?: UndefinedOnPartialDeep<ListThingsCommandInput>) {
const {limit, tags } = input ?? {};
super("/things", undefined, stripUndefined({limit, tags}));
}
}"
`;
32 changes: 32 additions & 0 deletions __tests__/codegen-regressions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,38 @@ test("additionalProperties chooses the object schema", async () => {
}
});

// A one-member `anyOf` or `oneOf` is that member. `v.union` of one option
// only wraps its issues, and the block65 valibot rules reject it
test("a single-member combinator emits the member alone", async () => {
const cases: [oas31.SchemaObject, string][] = [
[{ anyOf: [{ type: "string" }] }, "v.string()"],
[{ oneOf: [{ type: "string" }] }, "v.string()"],
[
{ oneOf: [{ type: "string" }, { type: "number" }] },
"v.union([v.string(), v.number()])",
],
];

const emitted = await Promise.all(
cases.map(async ([schema]) => {
const result = await processOpenApiDocument(
"/tmp/single-member-combinator",
docWithSchema("Only", schema),
);

const text = result.valibotFile.getText();

return text.slice(text.indexOf("export const inputOnlySchema"), -1);
}),
);

for (const [index, [, expected]] of cases.entries()) {
expect(emitted[index]).toContain(
`export const inputOnlySchema = ${expected};`,
);
}
});

function docWithSchema(name: string, schema: oas31.SchemaObject) {
return {
openapi: "3.1.0",
Expand Down
4 changes: 2 additions & 2 deletions __tests__/fixtures/docker/.openapi-codegen-manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"#generator": "d42d93cf60354fd3599a5861038e3bec",
"commands.ts": "26c79ac152c76497332be8d70b6d9ef7",
"#generator": "d9c25059b8f56e84cd41d51232262509",
"commands.ts": "20f63fd7ad412eb1f03ae5cfc4b81df9",
"types.ts": "72a44fac13cd4872db19c1a64b11a6ea",
"main.ts": "0f596fab7f6e9bb140fcd133caccb38d",
"valibot.ts": "822492c19be03028e145c1819bc12f83",
Expand Down
15 changes: 8 additions & 7 deletions __tests__/fixtures/docker/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
* Do not edit directly
*/

import { Command, stripUndefined, jsonStringify } from "@block65/rest-client";
import {
Command,
stripUndefined,
jsonStringify,
formJoinSerializer,
} from "@block65/rest-client";
import type { Except, UndefinedOnPartialDeep } from "type-fest";
import type {
ContainerListCommandQuery,
Expand Down Expand Up @@ -1097,9 +1102,7 @@ export class ImageCreateCommand extends Command<
ImageCreateCommandHeader
> {
public override method = "post" as const;
public override queryStyles = {
changes: { style: "form", explode: false },
} as const;
public override querySerializer = formJoinSerializer;

constructor(
input: UndefinedOnPartialDeep<Except<ImageCreateCommandInput, "body">> &
Expand Down Expand Up @@ -1520,9 +1523,7 @@ export class ImageGetAllCommand extends Command<
ImageGetAllCommandQuery
> {
public override method = "get" as const;
public override queryStyles = {
names: { style: "form", explode: false },
} as const;
public override querySerializer = formJoinSerializer;

constructor(input?: UndefinedOnPartialDeep<ImageGetAllCommandInput>) {
const { names } = input ?? {};
Expand Down
4 changes: 2 additions & 2 deletions __tests__/fixtures/openai/.openapi-codegen-manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"#generator": "d42d93cf60354fd3599a5861038e3bec",
"#generator": "d9c25059b8f56e84cd41d51232262509",
"commands.ts": "7d2187eb106fc582735b22176033ea28",
"types.ts": "e2ca3f6a2e1e1a382f4d11f187b341df",
"main.ts": "5ba91c2efb44e3e5c5bd1e1a1b90bb51",
"valibot.ts": "d43862b947bcf47a57a94d14daa0cc29",
"valibot.ts": "1ce0f05f51117be27a3719d479c82790",
"hono.ts": "f5768dde31ada252b89074452fe2f549",
"commands-validated.ts": "e4929484f6a64b784ff7b03327f58a3a",
"enums.ts": "350bddfda5b5eb357bbf6cff60f1808f"
Expand Down
37 changes: 14 additions & 23 deletions __tests__/fixtures/openai/valibot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1376,7 +1376,7 @@ export const inputCreateFineTuningJobRequestSchema = v.looseObject({
* The type of integration to enable. Currently, only "wandb" (Weights and
* Biases) is supported.
*/
type: v.union([v.picklist(["wandb"])]),
type: v.picklist(["wandb"]),
/**
* The settings for your integration with Weights and Biases. This payload
* specifies the project that
Expand Down Expand Up @@ -1524,7 +1524,7 @@ export const createFineTuningJobRequestSchema = v.looseObject({
* The type of integration to enable. Currently, only "wandb" (Weights and
* Biases) is supported.
*/
type: v.union([v.picklist(["wandb"])]),
type: v.picklist(["wandb"]),
/**
* The settings for your integration with Weights and Biases. This payload
* specifies the project that
Expand Down Expand Up @@ -6739,12 +6739,10 @@ export const threadObjectSchema = v.looseObject({
*/
metadata: v.nullable(v.record(v.string(), v.unknown())),
});
export const inputThreadStreamEventSchema = v.union([
v.looseObject({
event: v.picklist(["thread.created"]),
data: inputThreadObjectSchema,
}),
]);
export const inputThreadStreamEventSchema = v.looseObject({
event: v.picklist(["thread.created"]),
data: inputThreadObjectSchema,
});
export const threadStreamEventSchema = inputThreadStreamEventSchema;
/**
* Represents an event emitted when streaming a Run.
Expand Down Expand Up @@ -8653,7 +8651,7 @@ export const inputModifyAssistantRequestSchema = v.strictObject({
* models, or see our [Model overview](/docs/models/overview) for descriptions
* of them.
*/
model: v.optional(v.union([v.string()])),
model: v.optional(v.string()),
/**
* The name of the assistant. The maximum length is 256 characters.
*/
Expand Down Expand Up @@ -8755,7 +8753,7 @@ export const modifyAssistantRequestSchema = v.strictObject({
* models, or see our [Model overview](/docs/models/overview) for descriptions
* of them.
*/
model: v.exactOptional(v.union([v.pipe(v.string(), v.trim())])),
model: v.exactOptional(v.pipe(v.string(), v.trim())),
/**
* The name of the assistant. The maximum length is 256 characters.
*/
Expand Down Expand Up @@ -10080,10 +10078,7 @@ export const inputFineTuningJobSchema = v.looseObject({
*/
integrations: v.optional(
v.nullable(
v.pipe(
v.array(v.union([inputFineTuningIntegrationSchema])),
v.maxLength(5),
),
v.pipe(v.array(inputFineTuningIntegrationSchema), v.maxLength(5)),
),
),
/**
Expand Down Expand Up @@ -10204,9 +10199,7 @@ export const fineTuningJobSchema = v.looseObject({
* A list of integrations to enable for this fine-tuning job.
*/
integrations: v.exactOptional(
v.nullable(
v.pipe(v.array(v.union([fineTuningIntegrationSchema])), v.maxLength(5)),
),
v.nullable(v.pipe(v.array(fineTuningIntegrationSchema), v.maxLength(5))),
),
/**
* The seed used for the fine-tuning job.
Expand Down Expand Up @@ -10683,9 +10676,8 @@ export const chatCompletionRequestMessageContentPartTextSchema = v.looseObject({
*/
text: v.pipe(v.string(), v.trim()),
});
export const inputChatCompletionRequestToolMessageContentPartSchema = v.union([
inputChatCompletionRequestMessageContentPartTextSchema,
]);
export const inputChatCompletionRequestToolMessageContentPartSchema =
inputChatCompletionRequestMessageContentPartTextSchema;
export const chatCompletionRequestToolMessageContentPartSchema =
inputChatCompletionRequestToolMessageContentPartSchema;
export const inputChatCompletionRequestToolMessageSchema = v.looseObject({
Expand Down Expand Up @@ -11004,9 +10996,8 @@ export const chatCompletionRequestUserMessageSchema = v.looseObject({
*/
name: v.exactOptional(v.pipe(v.string(), v.trim())),
});
export const inputChatCompletionRequestSystemMessageContentPartSchema = v.union(
[inputChatCompletionRequestMessageContentPartTextSchema],
);
export const inputChatCompletionRequestSystemMessageContentPartSchema =
inputChatCompletionRequestMessageContentPartTextSchema;
export const chatCompletionRequestSystemMessageContentPartSchema =
inputChatCompletionRequestSystemMessageContentPartSchema;
export const inputChatCompletionRequestSystemMessageSchema = v.looseObject({
Expand Down
2 changes: 1 addition & 1 deletion __tests__/fixtures/petstore/.openapi-codegen-manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"#generator": "d42d93cf60354fd3599a5861038e3bec",
"#generator": "d9c25059b8f56e84cd41d51232262509",
"commands.ts": "608af748764e3adf1fd212532dfabd10",
"types.ts": "ea65c3e67352d4e22b97af80085727b4",
"main.ts": "64edb526dcbcbd345e631ccff959f11d",
Expand Down
2 changes: 1 addition & 1 deletion __tests__/fixtures/test1/.openapi-codegen-manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"#generator": "d42d93cf60354fd3599a5861038e3bec",
"#generator": "d9c25059b8f56e84cd41d51232262509",
"commands.ts": "19fd590dfc5cc8616a70dcf78326c027",
"types.ts": "a5a7fef55f948f68f724f7e50ba282ca",
"main.ts": "1e2091a697e1aa9d8b770d777c172bf8",
Expand Down
49 changes: 49 additions & 0 deletions __tests__/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import path from "node:path";
import type { oas31 } from "openapi3-ts";
import { processOpenApiDocument } from "../lib/process-document.ts";

// OAS 3.2 added `in: "querystring"`, which the 3.1 types predate
export type TestParameter =
| oas31.ParameterObject
| {
name: string;
in: "querystring";
content: oas31.ParameterObject["content"];
};

export function documentFor(
parameters: readonly TestParameter[],
): oas31.OpenAPIObject {
return {
openapi: "3.1.0",
info: { title: "Test", version: "1.0.0" },
paths: {
"/things": {
get: {
operationId: "listThingsCommand",
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- TestParameter widens the 3.1 union by the one 3.2 location these tests exercise, and processOpenApiDocument takes a 3.1 document
parameters: parameters as oas31.ParameterObject[],
responses: {
"200": {
description: "OK",
content: { "application/json": { schema: { type: "string" } } },
},
},
},
},
},
};
}

export async function generateFor(parameters: readonly TestParameter[]) {
// This path names the emitted files, which stay in memory
const outputDir = path.join(import.meta.dirname, ".generated");

return processOpenApiDocument(outputDir, documentFor(parameters));
}

export async function commandsFor(parameters: readonly TestParameter[]) {
const result = await generateFor(parameters);

return result.commandsFile.getText();
}
1 change: 1 addition & 0 deletions __tests__/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ test("a stale emitter revision rewrites every file", async () => {
await writeFile(target, "// edited by hand\n");
await writeFile(
manifestPath,
// oxlint-disable-next-line block65/snake-case-wire-keys -- the manifest's own key, read back by this generator
JSON.stringify({ ...manifest, "#generator": "0".repeat(32) }),
);

Expand Down
Loading
Loading