mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 10:55:57 +00:00
OpenAPI: handle recursive references
This commit is contained in:
@@ -239,15 +239,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"V1DecryptRequestBody": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"description": "A URL to the JSON Schema for this object.",
|
||||
"example": "https://api.unkey.dev/schemas/V1DecryptRequestBody.json",
|
||||
"format": "uri",
|
||||
"readOnly": true,
|
||||
"type": "string"
|
||||
},
|
||||
"encrypted": {
|
||||
"description": "The encrypted base64 string.",
|
||||
"minLength": 1,
|
||||
@@ -260,17 +252,7 @@
|
||||
"test": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["keyring", "data"],
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"description": "A URL to the JSON Schema for this object.",
|
||||
"example": "https://api.unkey.dev/schemas/V1EncryptBulkRequestBody.json",
|
||||
"format": "uri",
|
||||
"readOnly": true,
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
"$ref": "#/components/schemas/V1DecryptRequestBody"
|
||||
},
|
||||
{
|
||||
"oneOf": [
|
||||
@@ -304,7 +286,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["keyring", "encrypted"],
|
||||
"type": "object"
|
||||
},
|
||||
"V1DecryptResponseBody": {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
'use client';
|
||||
import {
|
||||
createContext,
|
||||
type FC,
|
||||
Fragment,
|
||||
type HTMLAttributes,
|
||||
lazy,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
@@ -47,9 +45,11 @@ import type { RequestData } from '@/requests/_shared';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { cn } from 'fumadocs-ui/utils/cn';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { resolve } from '@/playground/resolve';
|
||||
import { Ajv2020 } from 'ajv/dist/2020';
|
||||
import { getDefaultValue } from '@/playground/get-default-values';
|
||||
import {
|
||||
type FieldInfo,
|
||||
SchemaProvider,
|
||||
useResolvedSchema,
|
||||
} from '@/playground/schema';
|
||||
|
||||
interface FormValues {
|
||||
authorization:
|
||||
@@ -108,22 +108,6 @@ export type ClientProps = HTMLAttributes<HTMLFormElement> & {
|
||||
}>;
|
||||
};
|
||||
|
||||
interface SchemaContextType {
|
||||
fieldInfoMap: Map<string, FieldInfo>;
|
||||
}
|
||||
|
||||
interface FieldInfo {
|
||||
selectedType?: string;
|
||||
oneOf: number;
|
||||
}
|
||||
|
||||
const ajv = new Ajv2020({
|
||||
validateSchema: false,
|
||||
strict: false,
|
||||
});
|
||||
|
||||
const SchemaContext = createContext<SchemaContextType | undefined>(undefined);
|
||||
|
||||
function toRequestData(
|
||||
method: string,
|
||||
mediaType: string | undefined,
|
||||
@@ -144,8 +128,8 @@ export default function Client({
|
||||
route,
|
||||
method = 'GET',
|
||||
authorization,
|
||||
parameters: _parameters,
|
||||
body: _body,
|
||||
parameters,
|
||||
body,
|
||||
fields,
|
||||
references,
|
||||
proxyUrl,
|
||||
@@ -156,12 +140,6 @@ export default function Client({
|
||||
const requestData = useRequestData();
|
||||
const fieldInfoMap = useMemo(() => new Map<string, FieldInfo>(), []);
|
||||
const authInfo = usePersistentAuthInfo(authorization);
|
||||
const parameters = useMemo(
|
||||
() => resolve(_parameters, references),
|
||||
[_parameters, references],
|
||||
);
|
||||
const body = useMemo(() => resolve(_body, references), [_body, references]);
|
||||
|
||||
const defaultValues: FormValues = useMemo(
|
||||
() => ({
|
||||
authorization: authInfo.info,
|
||||
@@ -230,14 +208,7 @@ export default function Client({
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<SchemaContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
fieldInfoMap,
|
||||
}),
|
||||
[fieldInfoMap],
|
||||
)}
|
||||
>
|
||||
<SchemaProvider fieldInfoMap={fieldInfoMap} references={references}>
|
||||
<form
|
||||
{...rest}
|
||||
className={cn(
|
||||
@@ -274,7 +245,7 @@ export default function Client({
|
||||
{testQuery.data ? <ResultDisplay data={testQuery.data} /> : null}
|
||||
{authorization && <AuthFooter authorization={authorization} />}
|
||||
</form>
|
||||
</SchemaContext.Provider>
|
||||
</SchemaProvider>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -285,12 +256,11 @@ const paramTypes = ['header', 'cookie', 'query', 'path'] as const;
|
||||
function FormBody({
|
||||
authorization,
|
||||
parameters = [],
|
||||
body,
|
||||
fields = {},
|
||||
body,
|
||||
}: Pick<ClientProps, 'parameters' | 'authorization' | 'body' | 'fields'>) {
|
||||
const { servers } = useApiContext();
|
||||
const { server, setServer, setServerVariables } = useServerSelectContext();
|
||||
|
||||
const params = useMemo(() => {
|
||||
return paramTypes.map((param) => parameters.filter((v) => v.in === param));
|
||||
}, [parameters]);
|
||||
@@ -400,7 +370,8 @@ const bodyTypeVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
function BodyInput({ field }: { field: RequestSchema }) {
|
||||
function BodyInput({ field: _field }: { field: RequestSchema }) {
|
||||
const field = useResolvedSchema(_field);
|
||||
const [isJson, setIsJson] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -635,83 +606,3 @@ function writeAuthHeader(
|
||||
header.Authorization = input;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to store dynamic info of a field, such as selected schema of `oneOf`.
|
||||
*
|
||||
* @param fieldName - field name of form.
|
||||
* @param schema - The JSON Schema to generate initial values.
|
||||
* @param depth - The depth to avoid duplicated field name with same schema (e.g. nested `oneOf`).
|
||||
*/
|
||||
export function useFieldInfo(
|
||||
fieldName: string,
|
||||
schema: Exclude<RequestSchema, boolean>,
|
||||
depth: number,
|
||||
): {
|
||||
info: FieldInfo;
|
||||
updateInfo: (value: Partial<FieldInfo>) => void;
|
||||
} {
|
||||
const keyName = `${fieldName}:${depth}`;
|
||||
const { fieldInfoMap } = useContext(SchemaContext)!;
|
||||
const [_, trigger] = useState(0);
|
||||
const form = useFormContext();
|
||||
const value = form.getValues(fieldName as 'body');
|
||||
|
||||
const info = fieldInfoMap.get(keyName) ?? {
|
||||
oneOf: -1,
|
||||
};
|
||||
|
||||
// update info if needed
|
||||
if (!info.selectedType && Array.isArray(schema.type)) {
|
||||
info.selectedType =
|
||||
schema.type.find((type) =>
|
||||
ajv.validate(
|
||||
{
|
||||
...schema,
|
||||
type,
|
||||
},
|
||||
value,
|
||||
),
|
||||
) ?? schema.type[0];
|
||||
}
|
||||
|
||||
if (info.oneOf === -1 && schema.oneOf) {
|
||||
info.oneOf = schema.oneOf.findIndex((item) => ajv.validate(item, value));
|
||||
if (info.oneOf === -1) info.oneOf = 0;
|
||||
}
|
||||
|
||||
fieldInfoMap.set(keyName, info);
|
||||
|
||||
return {
|
||||
info,
|
||||
updateInfo: useEffectEvent((value) => {
|
||||
const prev = fieldInfoMap.get(keyName);
|
||||
if (!prev) return;
|
||||
|
||||
const updated = {
|
||||
...prev,
|
||||
...value,
|
||||
};
|
||||
|
||||
if (
|
||||
updated.oneOf === prev.oneOf &&
|
||||
updated.selectedType === prev.selectedType
|
||||
)
|
||||
return;
|
||||
|
||||
fieldInfoMap.set(keyName, updated);
|
||||
form.setValue(
|
||||
fieldName,
|
||||
getDefaultValue(
|
||||
schema.oneOf && updated.oneOf !== -1
|
||||
? schema.oneOf[updated.oneOf]
|
||||
: {
|
||||
...schema,
|
||||
type: value.selectedType ?? schema.type,
|
||||
},
|
||||
),
|
||||
);
|
||||
trigger((prev) => prev + 1);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,16 +58,16 @@ export async function APIPlayground({
|
||||
parameters: method.parameters?.map((v) => ({
|
||||
name: v.name,
|
||||
in: v.in as ParameterField['in'],
|
||||
schema: writeReferences(v.schema ?? true, context) as ParsedSchema,
|
||||
schema: writeReferences((v.schema ?? true) as ParsedSchema, context),
|
||||
description: v.description,
|
||||
})),
|
||||
body:
|
||||
bodyContent && mediaType
|
||||
? ({
|
||||
schema: writeReferences(
|
||||
bodyContent[mediaType].schema,
|
||||
bodyContent[mediaType].schema as ParsedSchema,
|
||||
context,
|
||||
) as ParsedSchema,
|
||||
),
|
||||
mediaType,
|
||||
} as ClientProps['body'])
|
||||
: undefined,
|
||||
@@ -80,33 +80,46 @@ export async function APIPlayground({
|
||||
}
|
||||
|
||||
function writeReferences(
|
||||
schema: unknown,
|
||||
schema: ParsedSchema,
|
||||
ctx: Context,
|
||||
stack: WeakMap<object, string> = new WeakMap(),
|
||||
): unknown {
|
||||
stack: WeakMap<object, object> = new WeakMap(),
|
||||
): RequestSchema {
|
||||
if (typeof schema !== 'object' || !schema) return schema;
|
||||
|
||||
if (stack.has(schema)) {
|
||||
const id = stack.get(schema)!;
|
||||
ctx.references[id] = schema;
|
||||
const out = stack.get(schema)!;
|
||||
const id = ctx.nextId();
|
||||
ctx.references[id] = out;
|
||||
|
||||
return {
|
||||
$ref: id,
|
||||
};
|
||||
}
|
||||
|
||||
stack.set(schema, ctx.nextId());
|
||||
const output = { ...schema };
|
||||
stack.set(schema, output);
|
||||
|
||||
if (Array.isArray(schema)) {
|
||||
return schema.map((item) => writeReferences(item, ctx, stack));
|
||||
for (const name of ['items', 'additionalProperties'] as const) {
|
||||
if (!output[name]) continue;
|
||||
output[name] = writeReferences(output[name], ctx, stack);
|
||||
}
|
||||
|
||||
const v: Record<string, unknown> = {};
|
||||
for (const key in schema) {
|
||||
v[key] = writeReferences(schema[key as keyof object], ctx, stack);
|
||||
for (const name of ['oneOf', 'allOf', 'anyOf'] as const) {
|
||||
if (!output[name]) continue;
|
||||
output[name] = output[name].map((item) =>
|
||||
writeReferences(item, ctx, stack),
|
||||
);
|
||||
}
|
||||
|
||||
return v;
|
||||
for (const name of ['properties', 'patternProperties'] as const) {
|
||||
if (!output[name]) continue;
|
||||
output[name] = { ...output[name] };
|
||||
|
||||
for (const key in output[name]) {
|
||||
output[name][key] = writeReferences(output[name][key], ctx, stack);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function getAuthorizationField(
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
type HTMLAttributes,
|
||||
type LabelHTMLAttributes,
|
||||
type ReactNode,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
@@ -23,17 +22,15 @@ import {
|
||||
import type { RequestSchema } from '@/playground/index';
|
||||
import { Input, labelVariants } from '@/ui/components/input';
|
||||
import { getDefaultValue } from './get-default-values';
|
||||
import { useFieldInfo } from './client';
|
||||
import { cn } from 'fumadocs-ui/utils/cn';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { combineSchema } from '@/utils/combine-schema';
|
||||
import { schemaToString } from '@/utils/schema-to-string';
|
||||
|
||||
const anyFields = {
|
||||
type: ['string', 'number', 'boolean', 'array', 'object'],
|
||||
items: true,
|
||||
additionalProperties: true,
|
||||
} satisfies RequestSchema;
|
||||
import {
|
||||
anyFields,
|
||||
useFieldInfo,
|
||||
useResolvedSchema,
|
||||
} from '@/playground/schema';
|
||||
|
||||
interface InputHeaderProps {
|
||||
name?: ReactNode;
|
||||
@@ -56,7 +53,7 @@ function FieldHeader({
|
||||
{required ? <span className="text-red-500">*</span> : null}
|
||||
<div className="flex-1" />
|
||||
{type ? (
|
||||
<code data-type className="text-xs text-fd-muted-foreground">
|
||||
<code data-type={true} className="text-xs text-fd-muted-foreground">
|
||||
{type}
|
||||
</code>
|
||||
) : null}
|
||||
@@ -73,11 +70,9 @@ export function ObjectInput({
|
||||
field: Exclude<RequestSchema, boolean>;
|
||||
fieldName: string;
|
||||
} & HTMLAttributes<HTMLDivElement>) {
|
||||
const field = useMemo(() => {
|
||||
return fallbackAny(
|
||||
combineSchema([_field, ...(_field.allOf ?? []), ...(_field.anyOf ?? [])]),
|
||||
);
|
||||
}, [_field]);
|
||||
const field = useResolvedSchema(
|
||||
combineSchema([_field, ...(_field.allOf ?? []), ...(_field.anyOf ?? [])]),
|
||||
);
|
||||
|
||||
return (
|
||||
<div {...props} className={cn('flex flex-col gap-6', props.className)}>
|
||||
@@ -99,12 +94,11 @@ export function ObjectInput({
|
||||
getType={(key) => {
|
||||
for (const pattern in field.patternProperties) {
|
||||
if (key.match(RegExp(pattern))) {
|
||||
return fallbackAny(field.patternProperties[pattern]);
|
||||
return field.patternProperties[pattern];
|
||||
}
|
||||
}
|
||||
|
||||
if (field.additionalProperties)
|
||||
return fallbackAny(field.additionalProperties);
|
||||
if (field.additionalProperties) return field.additionalProperties;
|
||||
|
||||
return anyFields;
|
||||
}}
|
||||
@@ -114,10 +108,6 @@ export function ObjectInput({
|
||||
);
|
||||
}
|
||||
|
||||
function fallbackAny(schema: RequestSchema): Exclude<RequestSchema, boolean> {
|
||||
return typeof schema === 'boolean' ? anyFields : schema;
|
||||
}
|
||||
|
||||
export function JsonInput({ fieldName }: { fieldName: string }) {
|
||||
const controller = useController({
|
||||
name: fieldName,
|
||||
@@ -150,7 +140,7 @@ function DynamicProperties({
|
||||
}: {
|
||||
fieldName: string;
|
||||
filterKey?: (key: string) => boolean;
|
||||
getType: (key: string) => Exclude<RequestSchema, boolean>;
|
||||
getType: (key: string) => RequestSchema;
|
||||
}) {
|
||||
const { control, setValue, getValues } = useFormContext();
|
||||
const [nextName, setNextName] = useState('');
|
||||
@@ -352,10 +342,30 @@ export function FieldSet({
|
||||
depth?: number;
|
||||
toolbar?: ReactNode;
|
||||
}) {
|
||||
const field = fallbackAny(_field);
|
||||
const field = useResolvedSchema(_field);
|
||||
const [show, setShow] = useState(
|
||||
() =>
|
||||
typeof _field !== 'object' ||
|
||||
!_field.$ref ||
|
||||
(field.type !== 'object' && !field.allOf && !field.oneOf && !field.anyOf),
|
||||
);
|
||||
const { info, updateInfo } = useFieldInfo(fieldName, field, depth);
|
||||
|
||||
if (_field === false) return null;
|
||||
if (!show) {
|
||||
return (
|
||||
<FieldHeader {...props} name={name} required={isRequired}>
|
||||
{toolbar}
|
||||
<button
|
||||
type="button"
|
||||
className={cn(buttonVariants({ size: 'sm' }))}
|
||||
onClick={() => setShow(true)}
|
||||
>
|
||||
Show
|
||||
</button>
|
||||
</FieldHeader>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.oneOf) {
|
||||
return (
|
||||
@@ -365,7 +375,7 @@ export function FieldSet({
|
||||
name={name}
|
||||
fieldName={fieldName}
|
||||
isRequired={isRequired}
|
||||
field={fallbackAny(field.oneOf[info.oneOf])}
|
||||
field={field.oneOf[info.oneOf]}
|
||||
depth={depth + 1}
|
||||
toolbar={
|
||||
<>
|
||||
@@ -416,16 +426,14 @@ export function FieldSet({
|
||||
{toolbar}
|
||||
</FieldHeader>
|
||||
<p className="text-xs text-fd-muted-foreground">{field.description}</p>
|
||||
{info.selectedType && (
|
||||
<FieldInput
|
||||
isRequired={isRequired}
|
||||
field={{
|
||||
...field,
|
||||
type: info.selectedType,
|
||||
}}
|
||||
fieldName={fieldName}
|
||||
/>
|
||||
)}
|
||||
<FieldInput
|
||||
isRequired={isRequired}
|
||||
field={{
|
||||
...field,
|
||||
type: info.selectedType,
|
||||
}}
|
||||
fieldName={fieldName}
|
||||
/>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
@@ -443,7 +451,17 @@ export function FieldSet({
|
||||
>
|
||||
{toolbar}
|
||||
</FieldHeader>
|
||||
<FieldInput field={field} fieldName={fieldName} isRequired={isRequired} />
|
||||
{show ? (
|
||||
<FieldInput
|
||||
field={field}
|
||||
fieldName={fieldName}
|
||||
isRequired={isRequired}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<button onClick={() => setShow((prev) => !prev)}>Show</button>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
@@ -500,9 +518,7 @@ function ArrayInput({
|
||||
}),
|
||||
)}
|
||||
onClick={() => {
|
||||
const itemType = fallbackAny(items);
|
||||
|
||||
append(getDefaultValue(itemType));
|
||||
append(getDefaultValue(items));
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { type RequestSchema } from '@/playground/index';
|
||||
|
||||
/**
|
||||
* Resolve reference
|
||||
*/
|
||||
export function resolve<T>(
|
||||
schema: T,
|
||||
references: Record<string, RequestSchema>,
|
||||
): T {
|
||||
if (!schema || typeof schema !== 'object') return schema;
|
||||
|
||||
if (Array.isArray(schema))
|
||||
return schema.map((item) => resolve(item, references)) as T;
|
||||
|
||||
if ('$ref' in schema && schema.$ref) {
|
||||
return resolve(references[schema.$ref as string], references) as T;
|
||||
}
|
||||
|
||||
for (const key in schema) {
|
||||
schema[key] = resolve(schema[key], references);
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Ajv2020 } from 'ajv/dist/2020';
|
||||
import type { RequestSchema } from '@/playground/index';
|
||||
import { createContext, ReactNode, useContext, useMemo, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event';
|
||||
import { getDefaultValue } from '@/playground/get-default-values';
|
||||
|
||||
interface SchemaContextType {
|
||||
references: Record<string, RequestSchema>;
|
||||
fieldInfoMap: Map<string, FieldInfo>;
|
||||
ajv: Ajv2020;
|
||||
}
|
||||
|
||||
export interface FieldInfo {
|
||||
selectedType?: string;
|
||||
oneOf: number;
|
||||
}
|
||||
|
||||
const SchemaContext = createContext<SchemaContextType | undefined>(undefined);
|
||||
export const anyFields = {
|
||||
type: ['string', 'number', 'boolean', 'array', 'object'],
|
||||
items: true,
|
||||
additionalProperties: true,
|
||||
} satisfies RequestSchema;
|
||||
|
||||
export function SchemaProvider({
|
||||
references,
|
||||
fieldInfoMap,
|
||||
children,
|
||||
}: Omit<SchemaContextType, 'ajv'> & { children: ReactNode }) {
|
||||
const ajv = useMemo(
|
||||
() =>
|
||||
new Ajv2020({
|
||||
strict: false,
|
||||
validateSchema: false,
|
||||
validateFormats: false,
|
||||
schemas: references,
|
||||
}),
|
||||
[references],
|
||||
);
|
||||
|
||||
return (
|
||||
<SchemaContext.Provider
|
||||
value={useMemo(
|
||||
() => ({ references, fieldInfoMap, ajv }),
|
||||
[fieldInfoMap, references, ajv],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SchemaContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to store dynamic info of a field, such as selected schema of `oneOf`.
|
||||
*
|
||||
* @param fieldName - field name of form.
|
||||
* @param schema - The JSON Schema to generate initial values.
|
||||
* @param depth - The depth to avoid duplicated field name with same schema (e.g. nested `oneOf`).
|
||||
*/
|
||||
export function useFieldInfo(
|
||||
fieldName: string,
|
||||
schema: Exclude<RequestSchema, boolean>,
|
||||
depth: number,
|
||||
): {
|
||||
info: FieldInfo;
|
||||
updateInfo: (value: Partial<FieldInfo>) => void;
|
||||
} {
|
||||
const { fieldInfoMap, ajv } = useContext(SchemaContext)!;
|
||||
const [_, trigger] = useState(0);
|
||||
const form = useFormContext();
|
||||
const keyName = `${fieldName}:${depth}`;
|
||||
const value = form.getValues(fieldName as 'body');
|
||||
const info = fieldInfoMap.get(keyName) ?? {
|
||||
oneOf: -1,
|
||||
};
|
||||
|
||||
// update info if needed
|
||||
if (!info.selectedType && Array.isArray(schema.type)) {
|
||||
info.selectedType =
|
||||
schema.type.find((type) =>
|
||||
ajv.validate(
|
||||
{
|
||||
...schema,
|
||||
type,
|
||||
},
|
||||
value,
|
||||
),
|
||||
) ?? schema.type[0];
|
||||
}
|
||||
|
||||
if (info.oneOf === -1 && schema.oneOf) {
|
||||
info.oneOf = schema.oneOf.findIndex((item) => {
|
||||
return ajv.validate(item, value);
|
||||
});
|
||||
if (info.oneOf === -1) info.oneOf = 0;
|
||||
}
|
||||
|
||||
fieldInfoMap.set(keyName, info);
|
||||
|
||||
return {
|
||||
info,
|
||||
updateInfo: useEffectEvent((value) => {
|
||||
const prev = fieldInfoMap.get(keyName);
|
||||
if (!prev) return;
|
||||
|
||||
const updated = {
|
||||
...prev,
|
||||
...value,
|
||||
};
|
||||
|
||||
if (
|
||||
updated.oneOf === prev.oneOf &&
|
||||
updated.selectedType === prev.selectedType
|
||||
)
|
||||
return;
|
||||
|
||||
fieldInfoMap.set(keyName, updated);
|
||||
form.setValue(
|
||||
fieldName,
|
||||
getDefaultValue(
|
||||
schema.oneOf && updated.oneOf !== -1
|
||||
? schema.oneOf[updated.oneOf]
|
||||
: {
|
||||
...schema,
|
||||
type: value.selectedType ?? schema.type,
|
||||
},
|
||||
),
|
||||
);
|
||||
trigger((prev) => prev + 1);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `$ref` in the schema, **not recursive**.
|
||||
*/
|
||||
export function useResolvedSchema(
|
||||
schema: RequestSchema,
|
||||
): Exclude<RequestSchema, boolean> {
|
||||
const { references } = useContext(SchemaContext)!;
|
||||
|
||||
return useMemo(() => {
|
||||
if (typeof schema === 'boolean') return anyFields;
|
||||
if (schema.$ref) return fallbackAny(references[schema.$ref]);
|
||||
return schema;
|
||||
}, [references, schema]);
|
||||
}
|
||||
|
||||
export function fallbackAny(
|
||||
schema: RequestSchema,
|
||||
): Exclude<RequestSchema, boolean> {
|
||||
return typeof schema === 'boolean' ? anyFields : schema;
|
||||
}
|
||||
@@ -96,9 +96,11 @@ export function Operation({
|
||||
bodyNode = (
|
||||
<>
|
||||
{heading(headingLevel, 'Request Body', ctx)}
|
||||
<div className="mb-8 flex flex-row items-center justify-between gap-2">
|
||||
<div className="mb-4 p-3 bg-fd-card rounded-xl border flex flex-row items-center justify-between gap-2">
|
||||
<code>{type}</code>
|
||||
<span>{body.required ? 'Required' : 'Optional'}</span>
|
||||
<span className="text-xs">
|
||||
{body.required ? 'Required' : 'Optional'}
|
||||
</span>
|
||||
</div>
|
||||
{body.description ? <Markdown text={body.description} /> : null}
|
||||
<Schema
|
||||
|
||||
@@ -173,29 +173,30 @@ export function Schema({
|
||||
...(schema.type === 'array' && schema.items ? [schema.items] : []),
|
||||
].filter((s) => isComplexType(s) && !stack.includes(s));
|
||||
|
||||
footer = mentionedObjectTypes.map((s, idx) => {
|
||||
return (
|
||||
<renderer.ObjectCollapsible
|
||||
key={idx}
|
||||
name={
|
||||
typeof s === 'object' && s.title
|
||||
? s.title
|
||||
: mentionedObjectTypes.length === 1
|
||||
? 'Show Attributes'
|
||||
: `Object ${idx + 1}`
|
||||
}
|
||||
>
|
||||
<Schema
|
||||
name="element"
|
||||
schema={s}
|
||||
ctx={{
|
||||
...ctx,
|
||||
stack: [schema, ...stack],
|
||||
}}
|
||||
/>
|
||||
</renderer.ObjectCollapsible>
|
||||
);
|
||||
});
|
||||
footer = (
|
||||
<div className="flex flex-col gap-2">
|
||||
{mentionedObjectTypes.map((s, idx) => {
|
||||
let title = typeof s === 'object' ? s.title : null;
|
||||
title ??=
|
||||
mentionedObjectTypes.length === 1
|
||||
? 'Show Attributes'
|
||||
: `Object ${idx + 1}`;
|
||||
|
||||
return (
|
||||
<renderer.ObjectCollapsible key={idx} name={title}>
|
||||
<Schema
|
||||
name="element"
|
||||
schema={s}
|
||||
ctx={{
|
||||
...ctx,
|
||||
stack: [schema, ...stack],
|
||||
}}
|
||||
/>
|
||||
</renderer.ObjectCollapsible>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user