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,
});

results matching ""

    No results matching ""