@hey-api/openapi-ts
Version:
🚀 The OpenAPI to TypeScript codegen. Generate clients, SDKs, validators, and more.
359 lines (319 loc) • 9.15 kB
text/typescript
import type { ComputedRef, Ref } from 'vue';
import { isRef, toValue, unref } from 'vue';
import { getAuthToken } from '../core/auth';
import type { QuerySerializerOptions } from '../core/bodySerializer';
import { jsonBodySerializer } from '../core/bodySerializer';
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from '../core/pathSerializer';
import type {
ArraySeparatorStyle,
BuildUrlOptions,
Client,
ClientOptions,
Config,
QuerySerializer,
RequestOptions,
} from './types';
type PathSerializer = Pick<Required<BuildUrlOptions>, 'path' | 'url'>;
const PATH_PARAM_RE = /\{[^{}]+\}/g;
type MaybeArray<T> = T | T[];
const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = 'simple';
if (name.endsWith('*')) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith('.')) {
name = name.substring(1);
style = 'label';
} else if (name.startsWith(';')) {
name = name.substring(1);
style = 'matrix';
}
const value = toValue(toValue(path)[name]);
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(
match,
serializeArrayParam({ explode, name, style, value }),
);
continue;
}
if (typeof value === 'object') {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === 'matrix') {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(
style === 'label' ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
}
}
return url;
};
export const createQuerySerializer = <T = unknown>({
allowReserved,
array,
object,
}: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = [];
const qParams = toValue(queryParams);
if (qParams && typeof qParams === 'object') {
for (const name in qParams) {
const value = toValue(qParams[name]);
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({
allowReserved,
explode: true,
name,
style: 'form',
value,
...array,
});
if (serializedArray) search.push(serializedArray);
} else if (typeof value === 'object') {
const serializedObject = serializeObjectParam({
allowReserved,
explode: true,
name,
style: 'deepObject',
value: value as Record<string, unknown>,
...object,
});
if (serializedObject) search.push(serializedObject);
} else {
const serializedPrimitive = serializePrimitiveParam({
allowReserved,
name,
value: value as string,
});
if (serializedPrimitive) search.push(serializedPrimitive);
}
}
}
return search.join('&');
};
return querySerializer;
};
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, 'security'> &
Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
}) => {
for (const auth of security) {
const token = await getAuthToken(auth, options.auth);
if (!token) {
continue;
}
const name = auth.name ?? 'Authorization';
switch (auth.in) {
case 'query':
if (!options.query) {
options.query = {};
}
toValue(options.query)[name] = token;
break;
case 'cookie':
options.headers.append('Cookie', `${name}=${token}`);
break;
case 'header':
default:
options.headers.set(name, token);
break;
}
return;
}
};
export const buildUrl: Client['buildUrl'] = (options) => {
const url = getUrl({
baseUrl: options.baseURL as string,
path: options.path,
query: options.query,
querySerializer:
typeof options.querySerializer === 'function'
? options.querySerializer
: createQuerySerializer(options.querySerializer),
url: options.url,
});
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: Pick<BuildUrlOptions, 'path' | 'query' | 'url'> & {
baseUrl?: string;
querySerializer: QuerySerializer;
}) => {
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
let url = (baseUrl ?? '') + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : '';
if (search.startsWith('?')) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b };
if (config.baseURL?.endsWith('/')) {
config.baseURL = config.baseURL.substring(0, config.baseURL.length - 1);
}
config.headers = mergeHeaders(a.headers, b.headers);
return config;
};
export const mergeHeaders = (
...headers: Array<Required<Config>['headers'] | undefined>
): Headers => {
const mergedHeaders = new Headers();
for (const header of headers) {
if (!header || typeof header !== 'object') {
continue;
}
let h: unknown = header;
if (isRef(h)) {
h = unref(h);
}
const iterator =
h instanceof Headers
? h.entries()
: Object.entries(h as Record<string, unknown>);
for (const [key, value] of iterator) {
if (value === null) {
mergedHeaders.delete(key);
} else if (Array.isArray(value)) {
for (const v of value) {
mergedHeaders.append(key, unwrapRefs(v) as string);
}
} else if (value !== undefined) {
const v = unwrapRefs(value);
// assume object headers are meant to be JSON stringified, i.e. their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(
key,
typeof v === 'object' ? JSON.stringify(v) : (v as string),
);
}
}
}
return mergedHeaders;
};
export const mergeInterceptors = <T>(...args: Array<MaybeArray<T>>): Array<T> =>
args.reduce<Array<T>>((acc, item) => {
if (typeof item === 'function') {
acc.push(item);
} else if (Array.isArray(item)) {
return acc.concat(item);
}
return acc;
}, []);
const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
explode: true,
style: 'form',
},
object: {
explode: true,
style: 'deepObject',
},
});
const defaultHeaders = {
'Content-Type': 'application/json',
};
export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer,
headers: defaultHeaders,
querySerializer: defaultQuerySerializer,
...override,
});
type UnwrapRefs<T> =
T extends Ref<infer V>
? V
: T extends ComputedRef<infer V>
? V
: T extends Record<string, unknown> // this doesn't handle functions well
? { [K in keyof T]: UnwrapRefs<T[K]> }
: T;
const unwrapRefs = <T>(value: T): UnwrapRefs<T> => {
if (value === null || typeof value !== 'object' || value instanceof Headers) {
return (isRef(value) ? unref(value) : value) as UnwrapRefs<T>;
}
if (Array.isArray(value)) {
return value.map((item) => unwrapRefs(item)) as UnwrapRefs<T>;
}
if (isRef(value)) {
return unwrapRefs(unref(value) as T);
}
// unwrap into new object to avoid modifying the source
const result: Record<string, unknown> = {};
for (const key in value) {
result[key] = unwrapRefs(value[key] as T);
}
return result as UnwrapRefs<T>;
};
export const serializeBody = (
opts: Pick<Parameters<Client['request']>[0], 'body' | 'bodySerializer'>,
) => {
if (opts.body && opts.bodySerializer) {
return opts.bodySerializer(opts.body);
}
return opts.body;
};
export const executeFetchFn = (
opts: Omit<Parameters<Client['request']>[0], 'composable'>,
fetchFn: Required<Config>['$fetch'],
) => {
const unwrappedOpts = unwrapRefs(opts);
unwrappedOpts.body = serializeBody(unwrappedOpts);
return fetchFn(
buildUrl(opts),
// @ts-expect-error
unwrappedOpts,
);
};