mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: improve default value generation
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user