Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Initial take on typed headers #12

Merged
merged 2 commits into from
Sep 22, 2024
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
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@denosaurs/typefetch",
"version": "0.0.20",
"version": "0.0.21",
"exports": {
".": "./main.ts"
},
Expand Down
28 changes: 18 additions & 10 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@ if (import.meta.main) {
console.log(
`Usage: typefetch [OPTIONS] <PATH>\n\n` +
`Options:\n` +
` -h, --help Print this help message\n` +
` -V, --version Print the version of TypeFetch\n` +
` -o, --output <PATH> Output file path (default: typefetch.d.ts)\n` +
` --config <PATH> File path to the tsconfig.json file\n` +
` --import <PATH> Import path for TypeFetch (default: https://raw.githubusercontent.com/denosaurs/typefetch/main)\n` +
` --base-urls <URLS> A comma separated list of custom base urls for paths to start with\n` +
` --include-server-urls Include server URLs from the schema in the generated paths (default: true)\n` +
` --include-absolute-url Include absolute URLs in the generated paths (default: false)\n` +
` --include-relative-url Include relative URLs in the generated paths (default: false)\n` +
` --experimental-urlsearchparams Enable the experimental fully typed URLSearchParams type (default: false)\n`,
` -h, --help Print this help message\n` +
` -V, --version Print the version of TypeFetch\n` +
` -o, --output <PATH> Output file path (default: typefetch.d.ts)\n` +
` --config <PATH> File path to the tsconfig.json file\n` +
` --import <PATH> Import path for TypeFetch (default: https://raw.githubusercontent.com/denosaurs/typefetch/main)\n` +
` --base-urls <URLS> A comma separated list of custom base urls for paths to start with\n` +
` --include-server-urls Include server URLs from the schema in the generated paths (default: true)\n` +
` --include-absolute-url Include absolute URLs in the generated paths (default: false)\n` +
` --include-relative-url Include relative URLs in the generated paths (default: false)\n` +
` --experimental-urlsearchparams Enable the experimental fully typed URLSearchParams type (default: false)\n`,
);
Deno.exit(0);
}
Expand Down Expand Up @@ -117,6 +117,14 @@ if (import.meta.main) {
namedImports: ["JSONString"],
});

source.addImportDeclaration({
isTypeOnly: true,
moduleSpecifier: `${args["import"]}/types/headers${
URL.canParse(args["import"]) ? ".ts" : ""
}`,
namedImports: ["TypedHeadersInit"],
});

if (options.experimentalURLSearchParams) {
source.addImportDeclaration({
isTypeOnly: true,
Expand Down
44 changes: 37 additions & 7 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,26 @@ export function toTemplateString(
return `${patternTemplateString}${URLSearchParams}`;
}

export function toHeadersInitType(
document: OpenAPI.Document,
parameters: ParameterObjectMap,
additionalHeaders: string[] = [],
): string | undefined {
const headersInitProperties = [...additionalHeaders];

for (const parameter of parameters.values()) {
if (parameter.in !== "header") continue;
headersInitProperties.push(
`"${parameter.name}"${parameter.required ? "" : "?"}: ${
toSchemaType(document, parameter.schema) ?? "string"
}`,
);
}

if (headersInitProperties.length === 0) return undefined;
return `TypedHeadersInit<{ ${headersInitProperties.join("; ")} }>`;
}

export function addOperationObject(
global: ModuleDeclaration,
document: OpenAPI.Document,
Expand Down Expand Up @@ -561,15 +581,21 @@ export function addOperationObject(
`${server}${path}`
));
}

if (options.includeRelativeUrl) {
inputs.push(path);
}

if (inputs.length === 0) {
throw new TypeError(
`No URLs were generated for ${path} with options ${
JSON.stringify(options)
}`,
`No URLs were generated for ${path} with the options:\n${
JSON.stringify(options, null, 2)
}\n\n` +
`You may want to run TypeFetch with one of the following options:\n` +
` --base-urls <URLS> A comma separated list of custom base urls for paths to start with\n` +
` --include-server-urls Include server URLs from the schema in the generated paths\n` +
` --include-absolute-url Include absolute URLs in the generated paths\n` +
` --include-relative-url Include relative URLs in the generated paths\n`,
);
}

Expand All @@ -590,8 +616,14 @@ export function addOperationObject(
operation.requestBody === undefined,
type: (writer) => {
const omit = ["method", "body"];
const additionalHeaders = [];

if (contentType !== undefined) {
additionalHeaders.push(`"Content-Type": "${contentType}"`);
}

const headersInitType = toHeadersInitType(document, parameters);
if (headersInitType !== undefined) {
omit.push("headers");
}

Expand Down Expand Up @@ -619,10 +651,8 @@ export function addOperationObject(
writer.newLine();
}

if (contentType !== undefined) {
writer.write(
`headers: { "Content-Type": "${contentType}"; } & Record<string, string>;`,
);
if (headersInitType !== undefined) {
writer.write(`headers: ${headersInitType};`);
}
});
},
Expand Down
58 changes: 58 additions & 0 deletions types/headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// deno-lint-ignore-file no-var

import type { OptionalKeys, RequiredKeys } from "./utils.ts";

// TODO: Allow certain default headers
// type DefaultHeaders =
// | "accept"
// | "accept-language"
// | "content-language"
// | "content-type"
// | "content-length";

type HeadersRecord = Record<string, string>;

// TODO: Add support for tuple format of headers
export type TypedHeadersInit<T extends HeadersRecord> = T | Headers<T>;

declare interface Headers<T extends HeadersRecord = HeadersRecord> {
/**
* Appends a new value onto an existing header inside a `Headers` object, or
* adds the header if it does not already exist.
*/
append<K extends keyof T>(name: K, value: T[K]): void;

/**
* Deletes a header from a `Headers` object.
*/
delete<K extends OptionalKeys<T>>(name: K): void;

/**
* Returns a `ByteString` sequence of all the values of a header within a
* `Headers` object with a given name.
*/
get<K extends keyof T>(name: K): T[K];

/**
* Returns a boolean stating whether a `Headers` object contains a certain
* header.
*/
has<K extends RequiredKeys<T>>(name: K): true;
has<K extends OptionalKeys<T>>(name: K): boolean;

/**
* Sets a new value for an existing header inside a Headers object, or adds
* the header if it does not already exist.
*/
set<K extends keyof T>(name: K, value: NonNullable<T[K]>): void;

/** Returns an array containing the values of all `Set-Cookie` headers
* associated with a response.
*/
getSetCookie(): string[];
}

declare var Headers: {
readonly prototype: Headers;
new <T extends HeadersRecord>(init?: T): Headers<T>;
};
9 changes: 9 additions & 0 deletions types/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
// deno-lint-ignore-file ban-types

/**
* A type that represents a value that can be null or undefined.
*/
export type Nullish<T> = T | null | undefined;

export type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];
export type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
Loading