import { getAuthToken } from "../core/auth.js" import type { QuerySerializer, QuerySerializerOptions } from "../core/bodySerializer.js" import { jsonBodySerializer } from "../core/bodySerializer.js" import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.js" import type { Client, ClientOptions, Config, RequestOptions } from "./types.js" interface PathSerializer { path: Record 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, 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 = ({ 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, ...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 => { 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, "security"> & Pick & { 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 query?: Record 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["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 = ( error: Err, response: Res, request: Req, options: Options, ) => Err | Promise type ReqInterceptor = (request: Req, options: Options) => Req | Promise type ResInterceptor = (response: Res, request: Req, options: Options) => Res | Promise class Interceptors { _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 { error: Pick>, "eject" | "use"> request: Pick>, "eject" | "use"> response: Pick>, "eject" | "use"> } // do not add `Middleware` as return type so we can use _fns internally export const createInterceptors = () => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), }) const defaultQuerySerializer = createQuerySerializer({ allowReserved: false, array: { explode: true, style: "form", }, object: { explode: true, style: "deepObject", }, }) const defaultHeaders = { "Content-Type": "application/json", } export const createConfig = ( override: Config & T> = {}, ): Config & T> => ({ ...jsonBodySerializer, headers: defaultHeaders, parseAs: "auto", querySerializer: defaultQuerySerializer, ...override, })