161 lines
5.4 KiB
TypeScript
161 lines
5.4 KiB
TypeScript
import {HttpParams, HttpParameterCodec} from '@angular/common/http';
|
|
import {CustomHttpParameterCodec, IdentityHttpParameterCodec} from './encoder';
|
|
|
|
export enum QueryParamStyle {
|
|
Json,
|
|
Form,
|
|
DeepObject,
|
|
SpaceDelimited,
|
|
PipeDelimited,
|
|
}
|
|
|
|
export type Delimiter = "," | " " | "|" | "\t";
|
|
|
|
export interface ParamOptions {
|
|
/** When true, serialized as multiple repeated key=value pairs. When false, serialized as a single key with joined values using `delimiter`. */
|
|
explode?: boolean;
|
|
/** Delimiter used when explode=false. The delimiter itself is inserted unencoded between encoded values. */
|
|
delimiter?: Delimiter;
|
|
}
|
|
|
|
interface ParamEntry {
|
|
values: string[];
|
|
options: Required<ParamOptions>;
|
|
}
|
|
|
|
export class OpenApiHttpParams {
|
|
private params: Map<string, ParamEntry> = new Map();
|
|
private defaults: Required<ParamOptions>;
|
|
private encoder: HttpParameterCodec;
|
|
|
|
/**
|
|
* @param encoder Parameter serializer
|
|
* @param defaults Global defaults used when a specific parameter has no explicit options.
|
|
* By OpenAPI default, explode is true for query params with style=form.
|
|
*/
|
|
constructor(encoder?: HttpParameterCodec, defaults?: { explode?: boolean; delimiter?: Delimiter }) {
|
|
this.encoder = encoder || new CustomHttpParameterCodec();
|
|
this.defaults = {
|
|
explode: defaults?.explode ?? true,
|
|
delimiter: defaults?.delimiter ?? ",",
|
|
};
|
|
}
|
|
|
|
private resolveOptions(local?: ParamOptions): Required<ParamOptions> {
|
|
return {
|
|
explode: local?.explode ?? this.defaults.explode,
|
|
delimiter: local?.delimiter ?? this.defaults.delimiter,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Replace the parameter's values and (optionally) its options.
|
|
* Options are stored per-parameter (not global).
|
|
*/
|
|
set(key: string, values: string[] | string, options?: ParamOptions): this {
|
|
const arr = Array.isArray(values) ? values.slice() : [values];
|
|
const opts = this.resolveOptions(options);
|
|
this.params.set(key, {values: arr, options: opts});
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Append a single value to the parameter. If the parameter didn't exist it will be created
|
|
* and use resolved options (global defaults merged with any provided options).
|
|
*/
|
|
append(key: string, value: string, options?: ParamOptions): this {
|
|
const entry = this.params.get(key);
|
|
if (entry) {
|
|
// If new options provided, override the stored options for subsequent serialization
|
|
if (options) {
|
|
entry.options = this.resolveOptions({...entry.options, ...options});
|
|
}
|
|
entry.values.push(value);
|
|
} else {
|
|
this.set(key, [value], options);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Serialize to a query string according to per-parameter OpenAPI options.
|
|
* - If explode=true for that parameter → repeated key=value pairs (each value encoded).
|
|
* - If explode=false for that parameter → single key=value where values are individually encoded
|
|
* and joined using the configured delimiter. The delimiter character is inserted AS-IS
|
|
* (not percent-encoded).
|
|
*/
|
|
toString(): string {
|
|
const records = this.toRecord();
|
|
const parts: string[] = [];
|
|
|
|
for (const key in records) {
|
|
parts.push(`${key}=${records[key]}`);
|
|
}
|
|
|
|
return parts.join("&");
|
|
}
|
|
|
|
/**
|
|
* Return parameters as a plain record.
|
|
* - If a parameter has exactly one value, returns that value directly.
|
|
* - If a parameter has multiple values, returns a readonly array of values.
|
|
*/
|
|
toRecord(): Record<string, string | number | boolean | ReadonlyArray<string | number | boolean>> {
|
|
const parts: Record<string, string | number | boolean | ReadonlyArray<string | number | boolean>> = {};
|
|
|
|
for (const [key, entry] of this.params.entries()) {
|
|
const encodedKey = this.encoder.encodeKey(key);
|
|
|
|
if (entry.options.explode) {
|
|
parts[encodedKey] = entry.values.map((v) => this.encoder.encodeValue(v));
|
|
} else {
|
|
const encodedValues = entry.values.map((v) => this.encoder.encodeValue(v));
|
|
|
|
// join with the delimiter *unencoded*
|
|
parts[encodedKey] = encodedValues.join(entry.options.delimiter);
|
|
}
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
|
|
/**
|
|
* Return an Angular's HttpParams with an identity parameter codec as the parameters are already encoded.
|
|
*/
|
|
toHttpParams(): HttpParams {
|
|
const records = this.toRecord();
|
|
|
|
let httpParams = new HttpParams({encoder: new IdentityHttpParameterCodec()});
|
|
|
|
return httpParams.appendAll(records);
|
|
}
|
|
}
|
|
|
|
export function concatHttpParamsObject(httpParams: OpenApiHttpParams, key: string, item: {
|
|
[index: string]: any
|
|
}, delimiter: Delimiter): OpenApiHttpParams {
|
|
let keyAndValues: string[] = [];
|
|
|
|
for (const k in item) {
|
|
keyAndValues.push(k);
|
|
|
|
const value = item[k];
|
|
|
|
if (Array.isArray(value)) {
|
|
keyAndValues.push(...value.map(convertToString));
|
|
} else {
|
|
keyAndValues.push(convertToString(value));
|
|
}
|
|
}
|
|
|
|
return httpParams.set(key, keyAndValues, {explode: false, delimiter: delimiter});
|
|
}
|
|
|
|
function convertToString(value: any): string {
|
|
if (value instanceof Date) {
|
|
return value.toISOString();
|
|
} else {
|
|
return value.toString();
|
|
}
|
|
}
|