OpenAPI: improve default value generation

This commit is contained in:
Fuma Nama
2026-02-26 16:52:48 +08:00
parent bd8c33a9c7
commit a6e3b585ba
5 changed files with 61 additions and 72 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
improve default value generation
@@ -1,29 +1,6 @@
import type { ParsedSchema } from '@/utils/schema';
import { sample } from 'openapi-sampler';
export function getDefaultValue(schema: ParsedSchema): unknown {
if (typeof schema === 'boolean') return null;
const type = schema.type;
if (Array.isArray(type))
return getDefaultValue({
...schema,
type: type[0],
});
if (type === 'object' && typeof schema === 'object')
return Object.fromEntries(
Object.entries(schema.properties ?? {}).map(([key, prop]) => {
return [key, getDefaultValue(prop)];
}),
);
if (type === 'array') return [];
if (type === 'null') return null;
if (type === 'string') {
if (typeof schema === 'object' && schema.format === 'binary') return undefined;
return '';
}
if (type === 'number' || type === 'integer') return 0;
if (type === 'boolean') return false;
return sample(schema as never, { skipNonRequired: true, skipReadOnly: true, quiet: true });
}
+20 -11
View File
@@ -55,7 +55,7 @@ export interface OnInitContext {
}
export interface NamespaceConfig {
reset?: () => void;
reset?: (ctx: { engine: DataEngine }) => void;
}
class ListenerManager {
@@ -145,9 +145,12 @@ export class DataEngine {
init(field: FieldKey, defaultValue?: DefaultValue, ctx: OnInitContext = {}): unknown {
if (field.length === 0) return this.data;
const parentKey: FieldKey = [];
const parentUpdateCtx = { swallow: true, ...ctx };
const parentUpdateCtx: OnUpdateContext = { swallow: true, ...ctx };
let parent = this.data as Record<string, unknown> | unknown[];
const fieldsToInit: FieldKey[] = [];
let initStart: FieldKey | null = null;
for (let i = 0; i < field.length; i++) {
const key = field[i];
// @ts-expect-error -- allow access
@@ -158,8 +161,9 @@ export class DataEngine {
// @ts-expect-error -- allow access
const out = (parent[key] = getDefaultValue(defaultValue));
this.listeners.onUpdate(parentKey, parentUpdateCtx);
this.listeners.onInit(field, ctx);
fieldsToInit.push(field);
for (const initField of fieldsToInit) this.listeners.onInit(initField, ctx);
this.listeners.onUpdate(initStart ?? parentKey, parentUpdateCtx);
return out;
} else if (isPlainObject(value) || Array.isArray(value)) {
parent = value;
@@ -169,13 +173,16 @@ export class DataEngine {
// @ts-expect-error -- allow access
parent = parent[key] = typeof nextKey === 'number' ? new Array(nextKey + 1) : {};
this.listeners.onUpdate(parentKey, parentUpdateCtx);
parentKey.push(key);
if (value !== undefined) {
if (value === undefined) {
initStart ??= [...parentKey];
parentKey.push(key);
fieldsToInit.push([...parentKey]);
} else {
parentKey.push(key);
this.listeners.onUpdate(parentKey, parentUpdateCtx);
console.warn(
`the original value of field ${parentKey.join('.')} is overidden, this might be unexpected.`,
);
this.listeners.onUpdate(parentKey, parentUpdateCtx);
}
}
}
@@ -188,14 +195,16 @@ export class DataEngine {
const parent = this.get(parentKey);
if (typeof prop === 'number' && Array.isArray(parent)) {
if (parent.length === 0) return;
const isLast = prop === parent.length - 1;
const deleted: unknown[] = parent.splice(prop, 1);
if (deleted.length === 0) return;
this.listeners.onDelete(key, ctx);
// it will change children's field value when removed at middle
this.listeners.onUpdate(parentKey, { swallow: false, ...ctx });
this.listeners.onUpdate(parentKey, { swallow: isLast, ...ctx });
return deleted[0];
} else if (isPlainObject(parent)) {
} else if (isPlainObject(parent) && prop in parent) {
const temp = parent[prop];
delete parent[prop];
this.listeners.onDelete(key, ctx);
@@ -243,6 +252,6 @@ export class DataEngine {
reset(data: NonNullable<object>) {
this.update([], data);
for (const { reset } of this.namespaces.values()) reset?.();
for (const { engine, reset } of this.namespaces.values()) reset?.({ engine });
}
}
+29 -31
View File
@@ -6,7 +6,7 @@ import {
type DefaultValue,
} from './data-engine';
import type { FieldKey } from './types';
import { deepEqual } from './utils';
import { deepEqual, isPlainObject } from './utils';
const Context = createContext<Stf | null>(null);
@@ -24,18 +24,14 @@ export function useStf(options: {
*/
defaultValues?: DefaultValue<Record<string, unknown>>;
}): Stf {
const { defaultValues } = options;
const optionsRef = useRef(options);
optionsRef.current = options;
const dataEngine = useMemo(
() => new DataEngine(defaultValues),
// eslint-disable-next-line react-hooks/exhaustive-deps -- assume unchanged
[],
);
return useMemo(
() => ({
dataEngine,
dataEngine: new DataEngine(optionsRef.current.defaultValues),
}),
[dataEngine],
[],
);
}
@@ -59,13 +55,13 @@ export function useArray(
defaultValue: options.defaultValue,
compute(value) {
const items: ArrayItemInfo[] = [];
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
items.push({
field: [...field, i],
index: i,
});
}
if (!Array.isArray(value)) return items;
for (let i = 0; i < value.length; i++) {
items.push({
field: [...field, i],
index: i,
});
}
return items;
},
@@ -114,7 +110,7 @@ export function useObject<T>(
const [objectKeys] = useFieldValue(field, {
defaultValue: options.defaultValue,
compute(currentValue) {
return currentValue ? Object.keys(currentValue) : [];
return isPlainObject(currentValue) ? Object.keys(currentValue) : [];
},
isChanged(prev, next) {
return !deepEqual(prev, next);
@@ -196,25 +192,28 @@ export function useFieldValue<V = unknown>(
) {
const { stf, compute = (v) => v as V, defaultValue, isChanged = (a, b) => a !== b } = options;
const engine = useDataEngine(stf);
const [value, setValue] = useState<V>(() => compute(engine.init(key, defaultValue)));
const [value, setValue] = useState<V>(() =>
compute(defaultValue === undefined ? engine.get(key) : engine.init(key, defaultValue)),
);
const prevEngineRef = useRef(engine);
if (prevEngineRef.current !== engine) {
setValue(compute(engine.init(key, defaultValue)));
setValue(
compute(defaultValue === undefined ? engine.get(key) : engine.init(key, defaultValue)),
);
prevEngineRef.current = engine;
}
function onUpdate() {
const computed = compute(engine.get(key));
setValue((prev) => (isChanged(prev, computed) ? computed : prev));
}
useListener({
field: key,
stf,
onInit() {
const computed = compute(engine.get(key));
setValue((prev) => (isChanged(prev, computed) ? computed : prev));
},
onUpdate() {
const computed = compute(engine.get(key));
setValue((prev) => (isChanged(prev, computed) ? computed : prev));
},
onInit: onUpdate,
onUpdate: onUpdate,
onDelete() {
const computed = compute(undefined);
setValue((prev) => (isChanged(prev, computed) ? computed : prev));
@@ -257,10 +256,9 @@ export function useNamespace(options: {
}) {
const { namespace, stf, initial } = options;
const engine = useDataEngine(stf);
const value = engine.namespace(namespace, initial, {
reset() {
value.update([], getDefaultValue(initial));
return engine.namespace(namespace, initial, {
reset({ engine }) {
engine.update([], getDefaultValue(initial));
},
});
return value;
}
+5 -5
View File
@@ -48,13 +48,13 @@ test('data engine: basic', () => {
`);
expect(logs).toMatchInlineSnapshot(`
[
"update true",
"update hello true",
"update hello,world true",
"init hello",
"init hello,world",
"init hello,world,2",
"update hello,world true",
"update hello,world,1 true",
"update true",
"init hello,world,1",
"init hello,world,1,property",
"update hello,world true",
"delete hello,world,1",
"update hello,world false",
]