From 77f2b25e53488d7c8af9bebdf0fcfa4b211a089e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Sj=C3=B6green?= Date: Wed, 18 Dec 2024 14:43:01 +0100 Subject: [PATCH] fix: Add test and export types --- main.ts | 10 ++- mod.ts | 88 +++++++++++++++++------- scripts/npm.ts | 5 ++ tests/petstore/test_url_search_params.ts | 23 +++++++ types/headers.ts | 4 +- types/url_search_params.ts | 8 +-- 6 files changed, 106 insertions(+), 32 deletions(-) create mode 100644 tests/petstore/test_url_search_params.ts diff --git a/main.ts b/main.ts index d9b4e27..e082a20 100644 --- a/main.ts +++ b/main.ts @@ -63,7 +63,7 @@ if (args.help) { ` --include-server-urls Include server URLs from the schema in the generated paths (default: ${parseOptions.default["include-server-urls"]})\n` + ` --include-absolute-url Include absolute URLs in the generated paths (default: ${parseOptions.default["include-absolute-url"]})\n` + ` --include-relative-url Include relative URLs in the generated paths (default: ${parseOptions.default["include-relative-url"]})\n` + - ` --experimental-urlsearchparams Enable the experimental fully typed URLSearchParams type (default: ${parseOptions.default["experimental-urlsearchparams"]})\n`, + ` --experimental-urlsearchparams Enable the experimental fully typed URLSearchParamsString type (default: ${parseOptions.default["experimental-urlsearchparams"]})\n`, ); Deno.exit(0); } @@ -141,6 +141,14 @@ if (options.experimentalURLSearchParams) { }`, namedImports: ["URLSearchParamsString"], }); +} else { + source.addImportDeclaration({ + isTypeOnly: true, + moduleSpecifier: `${args["import"]}/types/url_search_params${ + URL.canParse(args["import"]) ? ".ts" : "" + }`, + namedImports: ["URLSearchParamsString"], + }); } source.insertText(0, (writer) => { diff --git a/mod.ts b/mod.ts index 1d6d8f0..24ab635 100644 --- a/mod.ts +++ b/mod.ts @@ -61,19 +61,28 @@ export function toSchemaType( schema?: | OpenAPI.ReferenceObject | OpenAPI.SchemaObject, + coerceToString?: boolean, ): string | undefined { if (schema === undefined) return undefined; if ("$ref" in schema) return pascalCase(schema.$ref.split("/").pop()!); if ("nullable" in schema && schema.nullable !== undefined) { - const type = toSchemaType(document, { ...schema, nullable: undefined }); + const type = toSchemaType( + document, + { ...schema, nullable: undefined }, + coerceToString, + ); if (type !== undefined) return `${type}|null`; return "null"; } if (schema.not !== undefined) { - const type = toSchemaType(document, { ...schema, not: undefined }); - const exclude = toSchemaType(document, schema.not); + const type = toSchemaType( + document, + { ...schema, not: undefined }, + coerceToString, + ); + const exclude = toSchemaType(document, schema.not, coerceToString); if (type !== undefined && exclude !== undefined) { return `Exclude<${type}, ${exclude}>`; } @@ -85,12 +94,13 @@ export function toSchemaType( const type = toSchemaType(document, { ...schema, additionalProperties: undefined, - }); + }, coerceToString); let additionalProperties; if (schema.additionalProperties !== true) { additionalProperties = toSchemaType( document, schema.additionalProperties, + coerceToString, ); } if (type !== undefined) { @@ -101,14 +111,14 @@ export function toSchemaType( if (schema.allOf) { return schema.allOf - .map((schema) => toSchemaType(document, schema)) + .map((schema) => toSchemaType(document, schema, coerceToString)) .filter(Boolean) .join("&"); } if (schema.oneOf) { return schema.oneOf - .map((schema) => toSchemaType(document, schema)) + .map((schema) => toSchemaType(document, schema, coerceToString)) .map((type, _, types) => toSafeUnionString(type, types)) .filter(Boolean) .join("|"); @@ -129,7 +139,7 @@ export function toSchemaType( } return schema.anyOf - .map((schema) => toSchemaType(document, schema)) + .map((schema) => toSchemaType(document, schema, coerceToString)) .map((type, _, types) => toSafeUnionString(type, types)) .filter(Boolean) .join("|"); @@ -141,11 +151,13 @@ export function toSchemaType( switch (schema.type) { case "boolean": + if (coerceToString) return "`${boolean}`"; return "boolean"; case "string": return "string"; case "number": case "integer": + if (coerceToString) return "`${number}`"; return "number"; case "object": { if ("properties" in schema && schema.properties !== undefined) { @@ -157,19 +169,24 @@ export function toSchemaType( .map(([property, type]) => `${escapeObjectKey(property)}${ schema.required?.includes(property) ? "" : "?" - }:${toSchemaType(document, type)}` + }:${toSchemaType(document, type, coerceToString)}` ) .join(";") }}`; } + + if (coerceToString) return "Record"; return "Record"; } case "array": { - const items = toSchemaType(document, schema.items); + const items = toSchemaType(document, schema.items, coerceToString); if (items !== undefined) return `(${items})[]`; + + if (coerceToString) return "string[]"; return "unknown[]"; } case "null": + if (coerceToString) return "`${null}`"; return "null"; } @@ -300,25 +317,43 @@ export function createRequestBodyType( document: OpenAPI.Document, contentType: string, schema?: OpenAPI.SchemaObject | OpenAPI.ReferenceObject, + options?: Options, ): string { let type = "BodyInit"; switch (contentType) { - case "application/json": + case "application/json": { type = `JSONString<${toSchemaType(document, schema) ?? "unknown"}>`; break; - case "text/plain": + } + case "text/plain": { type = "string"; break; - case "multipart/form-data": + } + case "multipart/form-data": { type = "FormData"; break; - case "application/x-www-form-urlencoded": - type = "URLSearchParams"; + } + case "application/x-www-form-urlencoded": { + const schemaType = toSchemaType(document, schema, true); + if (schemaType !== undefined) { + const types = [`URLSearchParamsString<${schemaType}>`]; + + // TODO: We don't yet support URLSearchParams with the --experimental-urlsearchparams flag + if (!options?.experimentalURLSearchParams) { + types.push(`URLSearchParams<${schemaType}>`); + } + + return `(${types.join("|")})`; + } else { + type = `URLSearchParams`; + } break; - case "application/octet-stream": + } + case "application/octet-stream": { type = "ReadableStream | Blob | BufferSource"; break; + } } return type; @@ -385,7 +420,6 @@ export function toTemplateString( document: OpenAPI.Document, pattern: string, parameters: ParameterObjectMap, - options: Options, ): string { let patternTemplateString = pattern; let urlSearchParamsOptional = true; @@ -397,7 +431,9 @@ export function toTemplateString( urlSearchParamsOptional = false; } - const types = [toSchemaType(document, parameter.schema) ?? "string"]; + const types = [ + toSchemaType(document, parameter.schema, true) ?? "string", + ]; if (parameter.allowEmptyValue === true) types.push("true"); urlSearchParamsRecord.push( `${escapeObjectKey(parameter.name)}${!parameter.required ? "?" : ""}: ${ @@ -414,15 +450,17 @@ export function toTemplateString( ); } - const URLSearchParams = urlSearchParamsRecord.length > 0 - ? options.experimentalURLSearchParams - ? `\${URLSearchParamsString<{${urlSearchParamsRecord.join(";")}}>}` - : urlSearchParamsOptional - ? '${"" | `?${string}`}' - : "?${string}" + const urlSearchParamsType = urlSearchParamsRecord.length > 0 + ? `URLSearchParamsString<{${urlSearchParamsRecord.join(";")}}>` + : undefined; + + const urlSearchParams = urlSearchParamsType + ? urlSearchParamsOptional + ? `\${\`?\${${urlSearchParamsType}}\` | ""}` + : `?\${${urlSearchParamsType}}` : ""; - return `${patternTemplateString}${URLSearchParams}`; + return `${patternTemplateString}${urlSearchParams}`; } export function toHeadersInitType( @@ -569,7 +607,7 @@ export function addOperationObject( doc.tags.push({ tagName: "summary", text: operation.summary.trim() }); } - const path = toTemplateString(document, pattern, parameters, options); + const path = toTemplateString(document, pattern, parameters); const inputs = []; diff --git a/scripts/npm.ts b/scripts/npm.ts index 6504837..63274cc 100644 --- a/scripts/npm.ts +++ b/scripts/npm.ts @@ -22,6 +22,11 @@ await build({ name: "./types/json", path: "./types/json.ts", }, + { + kind: "export", + name: "./types/url_search_params", + path: "./types/url_search_params.ts", + }, { kind: "export", name: "./types/url_search_params_string", diff --git a/tests/petstore/test_url_search_params.ts b/tests/petstore/test_url_search_params.ts new file mode 100644 index 0000000..d9576d0 --- /dev/null +++ b/tests/petstore/test_url_search_params.ts @@ -0,0 +1,23 @@ +import type { Equal, Expect, IsUnion, NotEqual } from "npm:type-testing"; +import type { Error, Pets } from "./schemas/petstore.json.ts"; + +import { URLSearchParams } from "../../types/url_search_params.ts"; + +const urlSearchParams = new URLSearchParams<{ limit?: `${number}` }>({ + limit: "10", +}); +const response = await fetch( + `http://petstore.swagger.io/v1/pets?${urlSearchParams.toString()}`, +); + +if (response.ok) { + const json = await response.json(); + type test_IsUnion = Expect>; + type test_IsPetsOrError = Expect>; +} + +if (response.status === 200) { + const pets = await response.json(); + type test_IsPets = Expect>; + type test_IsNotError = Expect>; +} diff --git a/types/headers.ts b/types/headers.ts index 2b139ee..be79b99 100644 --- a/types/headers.ts +++ b/types/headers.ts @@ -32,7 +32,7 @@ type HeadersRecord = Record; // TODO: Add support for tuple format of headers export type TypedHeadersInit = T | Headers; -declare interface Headers { +export declare interface Headers { /** * Appends a new value onto an existing header inside a `Headers` object, or * adds the header if it does not already exist. @@ -70,7 +70,7 @@ declare interface Headers { getSetCookie(): string[]; } -declare var Headers: { +export declare var Headers: { readonly prototype: Headers; new (init?: T): Headers; }; diff --git a/types/url_search_params.ts b/types/url_search_params.ts index afd446d..f1e158c 100644 --- a/types/url_search_params.ts +++ b/types/url_search_params.ts @@ -35,7 +35,7 @@ export type URLSearchParamsInit = | T | URLSearchParamsString; -declare interface URLSearchParams< +export declare interface URLSearchParams< T extends URLSearchParamsRecord = URLSearchParamsRecord, > { /** @@ -69,8 +69,8 @@ declare interface URLSearchParams< * searchParams.getAll('name'); * ``` */ - get>(name: K): [T[K]]; - get>(name: K): [] | [T[K]]; + getAll>(name: K): [T[K]]; + getAll>(name: K): [] | [NonNullable]; /** * Returns the first value associated to the given search parameter. @@ -194,7 +194,7 @@ declare interface URLSearchParams< size: number; } -declare var URLSearchParams: { +export declare var URLSearchParams: { readonly prototype: URLSearchParams; new ( init?: URLSearchParamsInit,