You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/arts/sium/core/introspect.ts

268 lines
6.5 KiB

import type { MetaAnnotations, Schema, SchemaEffect, SchemaKind, SchemaWrapper } from './types';
export interface SerializedSchema {
kind: SchemaKind;
wrappers: SchemaWrapper[];
effects: SchemaEffect[];
async: boolean;
meta: Record<string, unknown>;
shape?: SerializedShape;
}
export type SerializedShape =
| { kind: 'truncated'; inner: string }
| { kind: 'object'; unknownKeys: string; fields: Record<string, SerializedSchema> }
| { kind: 'array'; item: SerializedSchema }
| { kind: 'tuple'; members: SerializedSchema[] }
| { kind: 'record'; value: SerializedSchema }
| { kind: 'union'; members: SerializedSchema[] }
| { kind: 'discriminated'; key: string; members: SerializedSchema[] }
| { kind: 'optional' | 'nullable' | 'defaulted'; inner: SerializedSchema }
| { kind: 'enum'; values: Array<string | number> }
| { kind: 'literal'; value: unknown };
function serializeMetaValue(value: unknown, seen: WeakSet<object>, depth: number): unknown {
if (
value === null ||
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
return value;
}
if (value === undefined) return undefined;
if (typeof value === 'function') {
return `[Function ${(value as Function).name || 'anonymous'}]`;
}
if (typeof value === 'symbol') {
return String(value);
}
if (Array.isArray(value)) {
if (depth <= 0) return '[Array]';
return value.map((entry) => serializeMetaValue(entry, seen, depth - 1));
}
if (typeof value === 'object') {
if (seen.has(value)) return '[Circular]';
if (depth <= 0) return '[Object]';
seen.add(value);
const out = Object.fromEntries(
Object.entries(value).map(([key, entry]) => [key, serializeMetaValue(entry, seen, depth - 1)])
);
seen.delete(value);
return out;
}
return String(value);
}
function serializeMeta(meta: MetaAnnotations): Record<string, unknown> {
const seen = new WeakSet<object>();
return Object.fromEntries(
Object.entries(meta).map(([key, value]) => [key, serializeMetaValue(value, seen, 2)])
);
}
function serializeSchemaInner(
schema: Schema<unknown, unknown>,
maxDepth: number,
depth: number,
seen: WeakSet<object>
): SerializedSchema {
const sium = schema['~sium'];
const base: SerializedSchema = {
kind: sium.kind,
wrappers: [...sium.wrappers],
effects: [...sium.effects],
async: sium.async,
meta: serializeMeta(sium.meta)
};
if (!sium.shape) return base;
if (depth >= maxDepth) {
return {
...base,
shape: { kind: 'truncated', inner: sium.shape.kind }
};
}
if (seen.has(schema as object)) {
return {
...base,
shape: { kind: 'truncated', inner: 'cycle' }
};
}
seen.add(schema as object);
try {
switch (sium.shape.kind) {
case 'object':
return {
...base,
shape: {
kind: 'object',
unknownKeys: sium.shape.unknownKeys,
fields: Object.fromEntries(
Object.entries(sium.shape.fields).map(([key, child]) => [
key,
serializeSchemaInner(child, maxDepth, depth + 1, seen)
])
)
}
};
case 'array':
return {
...base,
shape: {
kind: 'array',
item: serializeSchemaInner(sium.shape.item, maxDepth, depth + 1, seen)
}
};
case 'tuple':
return {
...base,
shape: {
kind: 'tuple',
members: sium.shape.members.map((member) =>
serializeSchemaInner(member, maxDepth, depth + 1, seen)
)
}
};
case 'record':
return {
...base,
shape: {
kind: 'record',
value: serializeSchemaInner(sium.shape.value, maxDepth, depth + 1, seen)
}
};
case 'union':
return {
...base,
shape: {
kind: 'union',
members: sium.shape.members.map((member) =>
serializeSchemaInner(member, maxDepth, depth + 1, seen)
)
}
};
case 'discriminated':
return {
...base,
shape: {
kind: 'discriminated',
key: sium.shape.key,
members: sium.shape.members.map((member) =>
serializeSchemaInner(member, maxDepth, depth + 1, seen)
)
}
};
case 'optional':
case 'nullable':
case 'defaulted':
return {
...base,
shape: {
kind: sium.shape.kind,
inner: serializeSchemaInner(sium.shape.inner, maxDepth, depth + 1, seen)
}
};
case 'enum':
return {
...base,
shape: {
kind: 'enum',
values: [...sium.shape.values]
}
};
case 'literal':
return {
...base,
shape: {
kind: 'literal',
value: sium.shape.value
}
};
}
} finally {
seen.delete(schema as object);
}
}
/** Serialize a schema into a JSON-friendly tree for debug panels and tooling. */
export function serializeSchema(schema: Schema<unknown, unknown>, maxDepth = 6): SerializedSchema {
return serializeSchemaInner(schema, maxDepth, 0, new WeakSet<object>());
}
/** Count field leaves in a schema tree. Objects recurse; arrays/unions count as one slot. */
export function countLeafFields(schema: Schema<unknown, unknown>): number {
const shape = schema['~sium'].shape;
if (!shape) return 1;
switch (shape.kind) {
case 'object':
return Object.values(shape.fields).reduce((acc, child) => acc + countLeafFields(child), 0);
case 'optional':
case 'nullable':
case 'defaulted':
return countLeafFields(shape.inner);
case 'tuple':
return shape.members.reduce((acc, child) => acc + countLeafFields(child), 0);
case 'array':
case 'record':
case 'union':
case 'discriminated':
case 'enum':
case 'literal':
return 1;
}
}
/**
* Visit a schema tree in pre-order. Object fields, array items and wrappers recurse automatically.
* Union/discriminated members are intentionally not recursed in v1.
*/
export function walkSchema(
schema: Schema<unknown, unknown>,
visit: (schema: Schema<unknown, unknown>, path: ReadonlyArray<string | number>) => void,
basePath: ReadonlyArray<string | number> = []
): void {
visit(schema, basePath);
const shape = schema['~sium'].shape;
if (!shape) return;
switch (shape.kind) {
case 'object':
for (const [key, child] of Object.entries(shape.fields)) {
walkSchema(child, visit, [...basePath, key]);
}
return;
case 'array':
walkSchema(shape.item, visit, [...basePath, 0]);
return;
case 'tuple':
shape.members.forEach((member, index) => {
walkSchema(member, visit, [...basePath, index]);
});
return;
case 'optional':
case 'nullable':
case 'defaulted':
walkSchema(shape.inner, visit, basePath);
return;
case 'record':
case 'union':
case 'discriminated':
case 'enum':
case 'literal':
return;
}
}

Powered by TurnKey Linux.