mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Fix Parameter Serialization
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"fumadocs-openapi": patch
|
||||
---
|
||||
|
||||
Fix Parameter Serialization
|
||||
@@ -12,6 +12,7 @@ export default [
|
||||
'no-console': 'off',
|
||||
// some arrays will not be changed
|
||||
'react/no-array-index-key': 'off',
|
||||
'no-fallthrough': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -33,12 +33,7 @@ export function createBrowserFetcher(
|
||||
|
||||
for (const key in options.header) {
|
||||
const param = options.header[key];
|
||||
|
||||
if (!Array.isArray(param.value)) {
|
||||
headers.append(key, param.value);
|
||||
} else {
|
||||
headers.append(key, param.value.join(','));
|
||||
}
|
||||
headers.append(key, param.value);
|
||||
}
|
||||
|
||||
const proxyUrl = options.proxyUrl ? new URL(options.proxyUrl, document.baseURI) : null;
|
||||
|
||||
@@ -32,7 +32,7 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
s.push('var client = new HttpClient();');
|
||||
const headerLines: string[] = [];
|
||||
|
||||
function addHeader(key: string, value: unknown) {
|
||||
function addHeader(key: string, value: string) {
|
||||
headerLines.push(`client.DefaultRequestHeaders.Add("${key}", ${JSON.stringify(value)});`);
|
||||
}
|
||||
for (const k in headers) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(data.header)) {
|
||||
headers[k] = v.value as string;
|
||||
headers[k] = v.value;
|
||||
}
|
||||
|
||||
const cookies = Object.entries(data.cookie);
|
||||
|
||||
@@ -25,7 +25,7 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(data.header)) {
|
||||
headers[k] = v.value as string;
|
||||
headers[k] = v.value;
|
||||
}
|
||||
|
||||
if (Object.keys(headers).length > 0) {
|
||||
@@ -37,7 +37,7 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
const cookies: Record<string, string> = {};
|
||||
|
||||
for (const [k, v] of inputCookies) {
|
||||
cookies[k] = v.value as string;
|
||||
cookies[k] = v.value;
|
||||
}
|
||||
|
||||
params.push(`cookies = ${generatePythonObject(cookies, imports)}`);
|
||||
|
||||
@@ -5,117 +5,67 @@ import type { ParameterObject } from '@/types';
|
||||
import type { RawRequestData, RequestData } from '@/requests/types';
|
||||
|
||||
export interface EncodedParameter {
|
||||
readonly value: string | string[];
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
const FormDelimiter = {
|
||||
spaceDelimited: ' ',
|
||||
pipeDelimited: '|',
|
||||
};
|
||||
|
||||
const PathPrefix = {
|
||||
label: '.',
|
||||
matrix: ';',
|
||||
};
|
||||
export interface EncodedParameterMultiple {
|
||||
readonly values: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* serialize parameters, see https://swagger.io/docs/specification/v3_0/serialization.
|
||||
*/
|
||||
export function encodeRequestData(
|
||||
from: RawRequestData,
|
||||
adapters: Record<string, MediaAdapter>,
|
||||
parameters: NoReference<ParameterObject>[],
|
||||
): RequestData {
|
||||
const result: Partial<RequestData> = {
|
||||
const result: RequestData = {
|
||||
method: from.method,
|
||||
body: from.body,
|
||||
bodyMediaType: from.bodyMediaType,
|
||||
cookie: {},
|
||||
header: {},
|
||||
path: {},
|
||||
query: {},
|
||||
};
|
||||
|
||||
for (const type of ['cookie', 'query', 'header', 'path'] as const) {
|
||||
const output: Record<string, EncodedParameter> = {};
|
||||
|
||||
for (const key in from[type]) {
|
||||
const value = from[type][key];
|
||||
if (value == null) continue;
|
||||
|
||||
const field = parameters.find((p) => p.name === key && p.in === type);
|
||||
if (!field) {
|
||||
output[key] = { value: String(value) };
|
||||
continue;
|
||||
}
|
||||
// skip unknown parameters
|
||||
if (!field) continue;
|
||||
|
||||
const encoder = getMediaEncoder(field, adapters);
|
||||
if (encoder) {
|
||||
output[key] = { value: encoder(value) };
|
||||
result[type][key] = { value: encoder(value) };
|
||||
continue;
|
||||
}
|
||||
|
||||
const explode = field.explode ?? true;
|
||||
let prefix = '';
|
||||
let sep = ',';
|
||||
|
||||
if (field.in === 'path') {
|
||||
const style = field.style ?? 'simple';
|
||||
|
||||
if (style in PathPrefix) {
|
||||
prefix = PathPrefix[style as keyof typeof PathPrefix];
|
||||
|
||||
if (explode) sep = prefix;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// header & cookie doesn't support explode for array values
|
||||
if (explode && field.in !== 'header' && field.in !== 'cookie') {
|
||||
output[key] = {
|
||||
value: prefix + value.map(String),
|
||||
switch (type) {
|
||||
case 'path':
|
||||
serializePathParameter(field, value, result.path);
|
||||
break;
|
||||
case 'query':
|
||||
serializeQueryParameter(field, value, result.query);
|
||||
break;
|
||||
case 'header': {
|
||||
result.header[key] = {
|
||||
value: serializeSimple(value, field.explode ?? false),
|
||||
};
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
if (field.in === 'query') {
|
||||
const style = field.style ?? 'form';
|
||||
|
||||
if (style in FormDelimiter) sep = FormDelimiter[style as keyof typeof FormDelimiter];
|
||||
}
|
||||
|
||||
output[key] = {
|
||||
value: prefix + value.map(String).join(sep),
|
||||
};
|
||||
continue;
|
||||
case 'cookie':
|
||||
serializeCookieParameter(field, value, result.cookie);
|
||||
break;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value) {
|
||||
// header & path creates key-value pairs
|
||||
if (explode && (field.in === 'header' || field.in === 'path')) {
|
||||
output[key] = {
|
||||
value:
|
||||
prefix +
|
||||
Object.entries(value)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(sep),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (explode || field.style === 'deepObject') {
|
||||
writeObject(key, value, field.style === 'deepObject', output);
|
||||
continue;
|
||||
}
|
||||
|
||||
output[key] = {
|
||||
value: prefix + Object.entries(value).flat().join(sep),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
output[key] = {
|
||||
value: prefix + String(value),
|
||||
};
|
||||
}
|
||||
|
||||
result[type] = output;
|
||||
}
|
||||
|
||||
return result as RequestData;
|
||||
return result;
|
||||
}
|
||||
|
||||
function getMediaEncoder(
|
||||
@@ -132,25 +82,177 @@ function getMediaEncoder(
|
||||
}
|
||||
}
|
||||
|
||||
function writeObject(
|
||||
parentKey: string,
|
||||
value: object,
|
||||
deep: boolean,
|
||||
function serializeSimple(value: NonNullable<unknown>, explode: boolean): string {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(',');
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return explode
|
||||
? Object.entries(value)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(',')
|
||||
: Object.entries(value).flat().join(',');
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function serializePathParameter(
|
||||
field: NoReference<ParameterObject>,
|
||||
value: NonNullable<unknown>,
|
||||
// write output
|
||||
output: Record<string, EncodedParameter>,
|
||||
) {
|
||||
for (const k in value) {
|
||||
const prop: unknown = value[k as keyof object];
|
||||
if (prop == null) continue;
|
||||
): void {
|
||||
const { explode = false, name } = field;
|
||||
|
||||
const key = deep ? `${parentKey}[${k}]` : k;
|
||||
if (!deep || typeof prop !== 'object') {
|
||||
output[key] = {
|
||||
value: String(prop),
|
||||
switch (field.style) {
|
||||
case 'label':
|
||||
if (Array.isArray(value)) {
|
||||
output[field.name] = {
|
||||
value: '.' + value.join(explode ? '.' : ','),
|
||||
};
|
||||
break;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
output[field.name] = {
|
||||
value:
|
||||
'.' +
|
||||
(explode
|
||||
? Object.entries(value)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('.')
|
||||
: Object.entries(value).flat().join(',')),
|
||||
};
|
||||
break;
|
||||
}
|
||||
output[field.name] = {
|
||||
value: `.${value}`,
|
||||
};
|
||||
break;
|
||||
case 'matrix': {
|
||||
const specifier = `;${name}=`;
|
||||
|
||||
continue;
|
||||
if (Array.isArray(value)) {
|
||||
output[field.name] = {
|
||||
value: explode
|
||||
? `${specifier}${value.join(',')}`
|
||||
: `${specifier}${value.join(specifier)}`,
|
||||
};
|
||||
break;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
output[field.name] = {
|
||||
value: explode
|
||||
? Object.entries(value)
|
||||
.map(([k, v]) => `;${k}=${v}`)
|
||||
.join('')
|
||||
: specifier + Object.entries(value).flat().join(','),
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
output[field.name] = {
|
||||
value: `${specifier}${value}`,
|
||||
};
|
||||
break;
|
||||
}
|
||||
// simple
|
||||
default:
|
||||
output[field.name] = {
|
||||
value: serializeSimple(value, explode),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
writeObject(key, value, deep, output);
|
||||
function serializeQueryParameter(
|
||||
field: NoReference<ParameterObject>,
|
||||
value: NonNullable<unknown>,
|
||||
// write output
|
||||
output: Record<string, EncodedParameterMultiple>,
|
||||
): void {
|
||||
const { explode = true } = field;
|
||||
|
||||
switch (field.style) {
|
||||
case 'spaceDelimited':
|
||||
if (!explode && Array.isArray(value)) {
|
||||
output[field.name] = {
|
||||
values: [value.join(' ')],
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'pipeDelimited':
|
||||
if (!explode && Array.isArray(value)) {
|
||||
output[field.name] = {
|
||||
values: [value.join('|')],
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'deepObject':
|
||||
if (!Array.isArray(value) && typeof value === 'object') {
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
output[`${field.name}[${k}]`] = {
|
||||
// note: the behaviour of nested array is undefined, we do this to avoid edge cases
|
||||
values: Array.isArray(v) ? v : [String(v)],
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
// form
|
||||
default:
|
||||
if (Array.isArray(value)) {
|
||||
output[field.name] = {
|
||||
values: explode ? value : [value.join(',')],
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && explode) {
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
output[k] = {
|
||||
values: [String(v)],
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
output[field.name] = {
|
||||
values: [Object.entries(value).flat().join(',')],
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
output[field.name] = {
|
||||
values: [String(value)],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function serializeCookieParameter(
|
||||
field: NoReference<ParameterObject>,
|
||||
value: NonNullable<unknown>,
|
||||
// write output
|
||||
output: Record<string, EncodedParameter>,
|
||||
) {
|
||||
const { explode = true } = field;
|
||||
|
||||
// form
|
||||
if (Array.isArray(value)) {
|
||||
output[field.name] = {
|
||||
value: explode ? value.map((v) => `${field.name}=${v}`).join('&') : value.join(','),
|
||||
};
|
||||
} else if (typeof value === 'object' && explode) {
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
output[k] = {
|
||||
value: String(v),
|
||||
};
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
output[field.name] = {
|
||||
value: Object.entries(value).flat().join(','),
|
||||
};
|
||||
} else {
|
||||
output[field.name] = {
|
||||
value: String(value),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { EncodedParameter } from '@/requests/media/encode';
|
||||
import type { EncodedParameter, EncodedParameterMultiple } from '@/requests/media/encode';
|
||||
|
||||
export type SampleGenerator<ServerContext = unknown> = (
|
||||
url: string,
|
||||
@@ -26,7 +26,7 @@ export interface RequestData {
|
||||
method: string;
|
||||
|
||||
path: Record<string, EncodedParameter>;
|
||||
query: Record<string, EncodedParameter>;
|
||||
query: Record<string, EncodedParameterMultiple>;
|
||||
header: Record<string, EncodedParameter>;
|
||||
cookie: Record<string, EncodedParameter>;
|
||||
|
||||
|
||||
@@ -37,11 +37,7 @@ export function resolveRequestData(pathname: string, { path, query }: RequestDat
|
||||
for (const key in path) {
|
||||
const param = path[key];
|
||||
|
||||
if (Array.isArray(param.value)) {
|
||||
pathname = pathname.replace(`{${key}}`, param.value.join('/'));
|
||||
} else {
|
||||
pathname = pathname.replace(`{${key}}`, param.value);
|
||||
}
|
||||
pathname = pathname.replace(`{${key}}`, param.value);
|
||||
}
|
||||
|
||||
// Check if pathname already contains query parameters (legacy API support)
|
||||
@@ -53,16 +49,12 @@ export function resolveRequestData(pathname: string, { path, query }: RequestDat
|
||||
// Add new query parameters from the RequestData
|
||||
for (const key in query) {
|
||||
const param = query[key];
|
||||
if (param.values.length === 0) continue;
|
||||
|
||||
if (Array.isArray(param.value)) {
|
||||
// Remove existing parameter first to avoid duplicates
|
||||
searchParams.delete(key);
|
||||
for (const item of param.value) {
|
||||
searchParams.append(key, item);
|
||||
}
|
||||
} else {
|
||||
// Set (replace if exists) the parameter value
|
||||
searchParams.set(key, param.value);
|
||||
// Remove existing parameter first to avoid duplicates
|
||||
searchParams.delete(key);
|
||||
for (const item of param.values) {
|
||||
searchParams.append(key, item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('Code Sample Generators', () => {
|
||||
authorization: { value: 'Bearer' },
|
||||
},
|
||||
query: {
|
||||
search: { value: 'ai' },
|
||||
search: { values: ['ai'] },
|
||||
},
|
||||
};
|
||||
const url = resolveRequestData('http://localhost:8080/{test}', data);
|
||||
|
||||
@@ -315,7 +315,7 @@ describe('URL utilities', () => {
|
||||
const requestData: RequestData = {
|
||||
method: 'GET',
|
||||
path: {
|
||||
segments: { value: ['api', 'v1', 'users'] },
|
||||
segments: { value: 'api/v1/users' },
|
||||
},
|
||||
query: {},
|
||||
header: {},
|
||||
@@ -330,8 +330,8 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
limit: { value: '10' },
|
||||
offset: { value: '20' },
|
||||
limit: { values: ['10'] },
|
||||
offset: { values: ['20'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -345,7 +345,7 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
tags: { value: ['javascript', 'typescript'] },
|
||||
tags: { values: ['javascript', 'typescript'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -362,7 +362,7 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
limit: { value: '5' },
|
||||
limit: { values: ['5'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -378,8 +378,8 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
limit: { value: '10' },
|
||||
sort: { value: 'date' },
|
||||
limit: { values: ['10'] },
|
||||
sort: { values: ['date'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -397,7 +397,7 @@ describe('URL utilities', () => {
|
||||
userId: { value: '123' },
|
||||
},
|
||||
query: {
|
||||
include: { value: 'profile' },
|
||||
include: { values: ['profile'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -413,7 +413,7 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
verbose: { value: 'false' },
|
||||
verbose: { values: ['false'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -429,7 +429,7 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
newParam: { value: 'value' },
|
||||
newParam: { values: ['value'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -445,7 +445,7 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
categories: { value: ['tech', 'science'] },
|
||||
categories: { values: ['tech', 'science'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -461,7 +461,7 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
tags: { value: ['new', 'updated'] },
|
||||
tags: { values: ['new', 'updated'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -494,9 +494,9 @@ describe('URL utilities', () => {
|
||||
projectId: { value: 'web-app' },
|
||||
},
|
||||
query: {
|
||||
include: { value: ['members', 'settings'] },
|
||||
format: { value: 'json' },
|
||||
debug: { value: 'true' },
|
||||
include: { values: ['members', 'settings'] },
|
||||
format: { values: ['json'] },
|
||||
debug: { values: ['true'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
|
||||
Reference in New Issue
Block a user