mirror of
https://github.com/sst/opencode.git
synced 2025-08-24 06:54:09 +00:00
417 lines
10 KiB
TypeScript
417 lines
10 KiB
TypeScript
import { getAuthToken } from '../core/auth';
|
|
import type {
|
|
QuerySerializer,
|
|
QuerySerializerOptions,
|
|
} from '../core/bodySerializer';
|
|
import { jsonBodySerializer } from '../core/bodySerializer';
|
|
import {
|
|
serializeArrayParam,
|
|
serializeObjectParam,
|
|
serializePrimitiveParam,
|
|
} from '../core/pathSerializer';
|
|
import type { Client, ClientOptions, Config, RequestOptions } from './types';
|
|
|
|
interface PathSerializer {
|
|
path: Record<string, unknown>;
|
|
url: string;
|
|
}
|
|
|
|
const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
|
|
|
type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
|
|
type MatrixStyle = 'label' | 'matrix' | 'simple';
|
|
type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
|
|
|
|
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 = 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[] = [];
|
|
if (queryParams && typeof queryParams === 'object') {
|
|
for (const name in queryParams) {
|
|
const value = queryParams[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;
|
|
};
|
|
|
|
/**
|
|
* Infers parseAs value from provided Content-Type header.
|
|
*/
|
|
export const getParseAs = (
|
|
contentType: string | null,
|
|
): Exclude<Config['parseAs'], 'auto'> => {
|
|
if (!contentType) {
|
|
// If no Content-Type header is provided, the best we can do is return the raw response body,
|
|
// which is effectively the same as the 'stream' option.
|
|
return 'stream';
|
|
}
|
|
|
|
const cleanContent = contentType.split(';')[0]?.trim();
|
|
|
|
if (!cleanContent) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
cleanContent.startsWith('application/json') ||
|
|
cleanContent.endsWith('+json')
|
|
) {
|
|
return 'json';
|
|
}
|
|
|
|
if (cleanContent === 'multipart/form-data') {
|
|
return 'formData';
|
|
}
|
|
|
|
if (
|
|
['application/', 'audio/', 'image/', 'video/'].some((type) =>
|
|
cleanContent.startsWith(type),
|
|
)
|
|
) {
|
|
return 'blob';
|
|
}
|
|
|
|
if (cleanContent.startsWith('text/')) {
|
|
return 'text';
|
|
}
|
|
|
|
return;
|
|
};
|
|
|
|
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 = {};
|
|
}
|
|
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,
|
|
}: {
|
|
baseUrl?: string;
|
|
path?: Record<string, unknown>;
|
|
query?: Record<string, unknown>;
|
|
querySerializer: QuerySerializer;
|
|
url: string;
|
|
}) => {
|
|
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;
|
|
}
|
|
|
|
const iterator =
|
|
header instanceof Headers ? header.entries() : Object.entries(header);
|
|
|
|
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, v as string);
|
|
}
|
|
} else if (value !== undefined) {
|
|
// assume object headers are meant to be JSON stringified, i.e. their
|
|
// content value in OpenAPI specification is 'application/json'
|
|
mergedHeaders.set(
|
|
key,
|
|
typeof value === 'object' ? JSON.stringify(value) : (value as string),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return mergedHeaders;
|
|
};
|
|
|
|
type ErrInterceptor<Err, Res, Req, Options> = (
|
|
error: Err,
|
|
response: Res,
|
|
request: Req,
|
|
options: Options,
|
|
) => Err | Promise<Err>;
|
|
|
|
type ReqInterceptor<Req, Options> = (
|
|
request: Req,
|
|
options: Options,
|
|
) => Req | Promise<Req>;
|
|
|
|
type ResInterceptor<Res, Req, Options> = (
|
|
response: Res,
|
|
request: Req,
|
|
options: Options,
|
|
) => Res | Promise<Res>;
|
|
|
|
class Interceptors<Interceptor> {
|
|
_fns: (Interceptor | null)[];
|
|
|
|
constructor() {
|
|
this._fns = [];
|
|
}
|
|
|
|
clear() {
|
|
this._fns = [];
|
|
}
|
|
|
|
getInterceptorIndex(id: number | Interceptor): number {
|
|
if (typeof id === 'number') {
|
|
return this._fns[id] ? id : -1;
|
|
} else {
|
|
return this._fns.indexOf(id);
|
|
}
|
|
}
|
|
exists(id: number | Interceptor) {
|
|
const index = this.getInterceptorIndex(id);
|
|
return !!this._fns[index];
|
|
}
|
|
|
|
eject(id: number | Interceptor) {
|
|
const index = this.getInterceptorIndex(id);
|
|
if (this._fns[index]) {
|
|
this._fns[index] = null;
|
|
}
|
|
}
|
|
|
|
update(id: number | Interceptor, fn: Interceptor) {
|
|
const index = this.getInterceptorIndex(id);
|
|
if (this._fns[index]) {
|
|
this._fns[index] = fn;
|
|
return id;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
use(fn: Interceptor) {
|
|
this._fns = [...this._fns, fn];
|
|
return this._fns.length - 1;
|
|
}
|
|
}
|
|
|
|
// `createInterceptors()` response, meant for external use as it does not
|
|
// expose internals
|
|
export interface Middleware<Req, Res, Err, Options> {
|
|
error: Pick<
|
|
Interceptors<ErrInterceptor<Err, Res, Req, Options>>,
|
|
'eject' | 'use'
|
|
>;
|
|
request: Pick<Interceptors<ReqInterceptor<Req, Options>>, 'eject' | 'use'>;
|
|
response: Pick<
|
|
Interceptors<ResInterceptor<Res, Req, Options>>,
|
|
'eject' | 'use'
|
|
>;
|
|
}
|
|
|
|
// do not add `Middleware` as return type so we can use _fns internally
|
|
export const createInterceptors = <Req, Res, Err, Options>() => ({
|
|
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
|
|
request: new Interceptors<ReqInterceptor<Req, Options>>(),
|
|
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
|
|
});
|
|
|
|
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,
|
|
parseAs: 'auto',
|
|
querySerializer: defaultQuerySerializer,
|
|
...override,
|
|
});
|