mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: fix $ref handling in stringifier
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
fix `$ref` handling in stringifier
|
||||
@@ -27,7 +27,7 @@ import { ChevronDown, LoaderCircle } from 'lucide-react';
|
||||
import { encodeRequestData } from '@/requests/media/encode';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { SchemaProvider, SchemaScope, useResolvedSchema } from '@/playground/schema';
|
||||
import { SchemaProvider, SchemaScope, useSchemaUtils } from '@/playground/schema';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -424,7 +424,7 @@ function FormBody({ parameters = [], body }: Pick<PlaygroundClientProps, 'parame
|
||||
}
|
||||
|
||||
function BodyInput({ field: _field }: { field: ParsedSchema }) {
|
||||
const field = useResolvedSchema(_field);
|
||||
const field = useSchemaUtils().resolve(_field);
|
||||
const [isJson, setIsJson] = useState(false);
|
||||
|
||||
if (field.format === 'binary') return <FieldSet field={field} fieldName={['body']} isRequired />;
|
||||
|
||||
@@ -10,11 +10,10 @@ import {
|
||||
SelectValue,
|
||||
} from '@/ui/components/select';
|
||||
import { Input, labelVariants } from '@/ui/components/input';
|
||||
import { getDefaultValue } from '../get-default-values';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { FormatFlags, schemaToString } from '@/utils/schema-to-string';
|
||||
import { anyFields, useFieldInfo, useResolvedSchema, useSchemaScope } from '@/playground/schema';
|
||||
import { FormatFlags } from '@/utils/schema-to-string';
|
||||
import { anyFields, useFieldInfo, useSchemaUtils, useSchemaScope } from '@/playground/schema';
|
||||
import type { ParsedSchema } from '@/utils/schema';
|
||||
import { stringifyFieldKey } from '@fumari/stf/lib/utils';
|
||||
import { cva } from 'class-variance-authority';
|
||||
@@ -37,16 +36,20 @@ export function ObjectInput({
|
||||
field: Exclude<ParsedSchema, boolean>;
|
||||
fieldName: FieldKey;
|
||||
} & ComponentProps<'div'>) {
|
||||
const field = useResolvedSchema(_field);
|
||||
const { patternProperties = {}, additionalProperties } = field;
|
||||
const { resolve, generateDefault } = useSchemaUtils();
|
||||
const field = resolve(_field);
|
||||
const schemaPropKeys = field.properties ? Object.keys(field.properties) : [];
|
||||
const isLazy = field['x-playground-lazy'] ?? schemaPropKeys.length > 100;
|
||||
const isDynamic = Object.keys(patternProperties).length > 0 || additionalProperties !== undefined;
|
||||
const {
|
||||
patternProperties = {},
|
||||
additionalProperties,
|
||||
'x-playground-lazy': isLazy = schemaPropKeys.length > 100,
|
||||
} = field;
|
||||
const isDynamic = Object.keys(patternProperties).length > 0 || additionalProperties;
|
||||
|
||||
const [nextName, setNextName] = useState('');
|
||||
const { properties, onAppend, onDelete, _objectKeys } = useObject(fieldName, {
|
||||
lazy: isLazy,
|
||||
defaultValue: () => getDefaultValue(field) as object,
|
||||
defaultValue: () => generateDefault(field) as object,
|
||||
properties: field.properties ?? {},
|
||||
fallback: additionalProperties,
|
||||
patternProperties: patternProperties,
|
||||
@@ -296,7 +299,8 @@ export function FieldSet({
|
||||
collapsible?: boolean;
|
||||
}) {
|
||||
const { readOnly, writeOnly } = useSchemaScope();
|
||||
const field = useResolvedSchema(_field);
|
||||
const { resolve, generateDefault, schemaToString } = useSchemaUtils();
|
||||
const field = resolve(_field);
|
||||
const [show, setShow] = useState(!collapsible);
|
||||
const { info, updateInfo } = useFieldInfo(fieldName, field, depth);
|
||||
const id = stringifyFieldKey(fieldName);
|
||||
@@ -321,7 +325,7 @@ export function FieldSet({
|
||||
type="button"
|
||||
className={cn(labelVariants(), 'inline-flex items-center gap-1 font-mono me-auto')}
|
||||
onClick={() => {
|
||||
dataEngine.init(fieldName, getDefaultValue(schema));
|
||||
dataEngine.init(fieldName, generateDefault(schema));
|
||||
setShow((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
@@ -381,7 +385,7 @@ export function FieldSet({
|
||||
>
|
||||
{union.map((item, i) => (
|
||||
<option key={i} value={i} className="bg-fd-popover text-fd-popover-foreground">
|
||||
{schemaToString(item, undefined, FormatFlags.UseAlias)}
|
||||
{schemaToString(item, FormatFlags.UseAlias)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -510,9 +514,8 @@ function ArrayInput({
|
||||
items: ParsedSchema;
|
||||
} & ComponentProps<'div'>) {
|
||||
const name = fieldName.at(-1) ?? '';
|
||||
const { items, insertItem, removeItem } = useArray(fieldName, {
|
||||
defaultValue: [],
|
||||
});
|
||||
const { generateDefault } = useSchemaUtils();
|
||||
const { items, insertItem, removeItem } = useArray(fieldName);
|
||||
|
||||
return (
|
||||
<div {...props} className={cn('flex flex-col gap-2', props.className)}>
|
||||
@@ -554,7 +557,7 @@ function ArrayInput({
|
||||
}),
|
||||
)}
|
||||
onClick={() => {
|
||||
insertItem(getDefaultValue(itemSchema));
|
||||
insertItem(generateDefault(itemSchema));
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { ParsedSchema } from '@/utils/schema';
|
||||
import { sample } from 'openapi-sampler';
|
||||
|
||||
export function getDefaultValue(schema: ParsedSchema): unknown {
|
||||
return sample(schema as never, { skipNonRequired: true, skipReadOnly: true, quiet: true });
|
||||
}
|
||||
@@ -80,7 +80,7 @@ function writeReferences(
|
||||
ctx.references[id] = out;
|
||||
|
||||
return {
|
||||
$ref: id,
|
||||
$ref: `#/${id}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Ajv2020 } from 'ajv/dist/2020';
|
||||
import { createContext, ReactNode, use, useMemo } from 'react';
|
||||
import { getDefaultValue } from '@/playground/get-default-values';
|
||||
import type { ParsedSchema } from '@/utils/schema';
|
||||
import type { ParsedSchema, ResolvedSchema } from '@/utils/schema';
|
||||
import { mergeAllOf } from '@/utils/merge-schema';
|
||||
import { FieldKey, useDataEngine, useFieldValue, useNamespace } from '@fumari/stf';
|
||||
import { stringifyFieldKey } from '@fumari/stf/lib/utils';
|
||||
import { sample } from 'openapi-sampler';
|
||||
import { FormatFlags, schemaToString } from '@/utils/schema-to-string';
|
||||
|
||||
interface SchemaContextType extends SchemaScope {
|
||||
references: Record<string, ParsedSchema>;
|
||||
@@ -96,6 +97,7 @@ export function useFieldInfo(
|
||||
} {
|
||||
const { ajv } = use(SchemaContext)!;
|
||||
const engine = useDataEngine();
|
||||
const { generateDefault } = useSchemaUtils();
|
||||
const fieldData = useNamespace({
|
||||
namespace: `field-info:${depth}:${stringifyFieldKey(fieldName)}`,
|
||||
initial(): FieldInfo {
|
||||
@@ -155,22 +157,42 @@ export function useFieldInfo(
|
||||
valueSchema = { ...schema, type: updated.selectedType };
|
||||
}
|
||||
|
||||
engine.update(fieldName, getDefaultValue(valueSchema));
|
||||
engine.update(fieldName, generateDefault(valueSchema));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `$ref` in the schema, **not recursive**.
|
||||
*/
|
||||
export function useResolvedSchema(schema: ParsedSchema): Exclude<ParsedSchema, boolean> {
|
||||
export function useSchemaUtils() {
|
||||
const { references } = use(SchemaContext)!;
|
||||
|
||||
return useMemo(() => {
|
||||
function resolve(schema: ParsedSchema): Exclude<ParsedSchema, boolean> {
|
||||
if (typeof schema === 'boolean') return anyFields;
|
||||
if (schema.$ref) return fallbackAny(references[schema.$ref]);
|
||||
let ref = schema.$ref;
|
||||
if (ref) {
|
||||
// use swallow resolution as it is already preprocessed in `playground/index.tsx`
|
||||
const prefix = '#/';
|
||||
if (ref.startsWith(prefix)) ref = ref.slice(prefix.length);
|
||||
if (ref in references) return fallbackAny(references[ref]);
|
||||
}
|
||||
return schema;
|
||||
}, [references, schema]);
|
||||
}
|
||||
|
||||
return {
|
||||
generateDefault(schema: ParsedSchema): unknown {
|
||||
return sample(
|
||||
schema as never,
|
||||
{ skipNonRequired: true, skipReadOnly: true, quiet: true },
|
||||
references,
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Resolve `$ref` in the schema, **not recursive**.
|
||||
*/
|
||||
resolve,
|
||||
schemaToString(value: ResolvedSchema, flags?: FormatFlags) {
|
||||
return schemaToString(value, (s) => ({ dereferenced: resolve(s), raw: s }), flags);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function fallbackAny(schema: ParsedSchema): Exclude<ParsedSchema, boolean> {
|
||||
|
||||
@@ -308,7 +308,7 @@ export function generateSchemaUI(
|
||||
});
|
||||
}
|
||||
|
||||
if (additionalProperties !== undefined && isVisible(additionalProperties)) {
|
||||
if (additionalProperties && isVisible(additionalProperties)) {
|
||||
const $type = getSchemaId(additionalProperties);
|
||||
scanRefs($type, additionalProperties);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ResolvedSchema } from '@/utils/schema';
|
||||
import type { ParsedSchema, ResolvedSchema } from '@/utils/schema';
|
||||
import type { ProcessedDocument } from '@/utils/process-document';
|
||||
|
||||
export enum FormatFlags {
|
||||
@@ -6,11 +6,27 @@ export enum FormatFlags {
|
||||
UseAlias = 1 << 0,
|
||||
}
|
||||
|
||||
type Resolver = (schema: ResolvedSchema) => {
|
||||
dereferenced: ResolvedSchema;
|
||||
raw?: Exclude<ParsedSchema, boolean>;
|
||||
};
|
||||
|
||||
export function schemaToString(
|
||||
value: ResolvedSchema,
|
||||
ctx?: ProcessedDocument,
|
||||
_resolver?: ProcessedDocument | Resolver,
|
||||
flags: FormatFlags = FormatFlags.None,
|
||||
): string {
|
||||
const resolver: Resolver =
|
||||
typeof _resolver === 'function'
|
||||
? _resolver
|
||||
: (schema) => {
|
||||
const ref = _resolver?.getRawRef(schema);
|
||||
|
||||
return {
|
||||
dereferenced: schema,
|
||||
raw: ref ? { $ref: ref } : undefined,
|
||||
};
|
||||
};
|
||||
function union(union: readonly ResolvedSchema[], sep: string, flags: FormatFlags) {
|
||||
const members = new Set();
|
||||
let nullable = false;
|
||||
@@ -30,13 +46,16 @@ export function schemaToString(
|
||||
}
|
||||
|
||||
function run(schema: ResolvedSchema, flags: FormatFlags): string {
|
||||
const resolved = resolver(schema);
|
||||
schema = resolved.dereferenced;
|
||||
|
||||
if (schema === true) return 'any';
|
||||
else if (schema === false) return 'never';
|
||||
|
||||
if ((flags & FormatFlags.UseAlias) === FormatFlags.UseAlias) {
|
||||
if (schema.title) return schema.title;
|
||||
|
||||
const ref = ctx?.getRawRef(schema)?.split('/');
|
||||
const ref = resolved.raw?.$ref?.split('/');
|
||||
if (ref && ref.length > 0) return ref[ref.length - 1];
|
||||
}
|
||||
|
||||
|
||||
@@ -51,8 +51,9 @@ export function useArray(
|
||||
options: { defaultValue?: DefaultValue<unknown[]> } = {},
|
||||
) {
|
||||
const engine = useDataEngine();
|
||||
const { defaultValue } = options;
|
||||
const [items] = useFieldValue(field, {
|
||||
defaultValue: options.defaultValue,
|
||||
defaultValue,
|
||||
compute(value) {
|
||||
const items: ArrayItemInfo[] = [];
|
||||
if (!Array.isArray(value)) return items;
|
||||
@@ -74,8 +75,8 @@ export function useArray(
|
||||
items,
|
||||
insertItem(itemValue?: unknown) {
|
||||
const value = engine.get(field);
|
||||
|
||||
engine.update(field, Array.isArray(value) ? [...value, itemValue] : [itemValue]);
|
||||
const idx = Array.isArray(value) ? value.length : 0;
|
||||
engine.init([...field, idx], itemValue);
|
||||
},
|
||||
removeItem(index: number) {
|
||||
engine.delete([...field, index]);
|
||||
|
||||
Reference in New Issue
Block a user