Любая схема, описанная с помощью Superstruct, по своей сути формирует иерархическое дерево. Узлы этого дерева соответствуют примитивным и составным типам, а связи отражают вложенность и композицию правил валидации.
Базовые примитивы — строки, числа, булевы значения — являются листьями структуры. Составные типы, такие как объекты, массивы, объединения и пересечения, формируют внутренние узлы, объединяющие несколько подструктур в единый контракт.
Рассмотрение структуры как дерева позволяет перейти от декларативного описания к визуальному представлению, удобному для анализа, отладки и документирования.
В Superstruct любая схема строится из функций-комбинаторов. Например:
import { object, string, number, array, union } from 'superstruct'
const User = object({
id: number(),
name: string(),
roles: array(string()),
status: union([string(), number()])
})
Эта конструкция уже содержит скрытую структуру:
User (object)
├── id (number)
├── name (string)
├── roles (array)
│ └── item (string)
└── status (union)
├── string
└── number
Каждый уровень вложенности соответствует вызову фабрики struct. Такая форма представления удобна для последующей автоматической визуализации.
Для построения визуализации используется обход дерева структуры. Основная идея заключается в том, что каждый struct содержит информацию о своём типе и дочерних элементах.
Типовая стратегия обхода:
Псевдокод:
function traverse(struct, depth = 0) {
const node = {
type: struct.type,
depth,
children: []
}
if (struct.type === 'object') {
for (const key in struct.schema) {
node.children.push({
key,
...traverse(struct.schema[key], depth + 1)
})
}
}
if (struct.type === 'array') {
node.children.push(traverse(struct.element, depth + 1))
}
if (struct.type === 'union') {
for (const variant of struct.members) {
node.children.push(traverse(variant, depth + 1))
}
}
return node
}
Полученная структура может быть использована как универсальный промежуточный формат.
Одним из наиболее простых способов визуализации является текстовое дерево. Оно подходит для логов, документации и отладки.
Форматирование осуществляется через отступы и маркеры:
function printTree(node, indent = '') {
console.log(indent + node.type)
if (node.children) {
node.children.forEach(child => {
printTree(child, indent + ' ')
})
}
}
Для более информативного вывода добавляются имена полей:
object
id: number
name: string
roles: array
string
status: union
string
number
Такое представление позволяет быстро оценить глубину и сложность структуры.
Для более сложных схем используется графовое представление. Superstruct-описания часто содержат повторяющиеся и пересекающиеся элементы, что делает дерево недостаточным.
Формат DOT позволяет визуализировать структуру как граф:
function toDot(node, id = 0, edges = [], nodes = []) {
const currentId = id++
nodes.push(`${currentId} [label="${node.type}"]`)
node.children?.forEach(child => {
const result = toDot(child, id, edges, nodes)
edges.push(`${currentId} -> ${result.id}`)
id = result.id
})
return { id: currentId, edges, nodes }
}
Пример итогового DOT-графа:
digraph Struct {
0 [label="object"]
1 [label="number"]
2 [label="string"]
3 [label="array"]
4 [label="union"]
0 -> 1
0 -> 2
0 -> 3
0 -> 4
}
Такое представление позволяет анализировать структуру как систему связей, а не как линейную иерархию.
Промежуточный формат часто строится в виде JSON-дерева. Это упрощает интеграцию с внешними инструментами визуализации.
Пример:
{
type: "object",
children: [
{ key: "id", type: "number" },
{ key: "name", type: "string" },
{
key: "roles",
type: "array",
children: [{ type: "string" }]
}
]
}
JSON-структура используется для построения интерфейсных визуализаторов, где каждый узел отображается как компонент.
Особую сложность в визуализации создают объединения типов. Union представляет альтернативные ветви, каждая из которых должна отображаться отдельно.
union
├── string
└── number
При пересечениях (intersection) структура становится многослойной:
intersection
├── object A
│ ├── field x
└── object B
├── field y
Визуально такие конструкции часто отображаются как наложенные или связанные узлы, поскольку они описывают один итоговый контракт.
Для повышения читаемости структуры в узлы добавляются дополнительные метаданные:
Пример расширенного узла:
{
key: "age",
type: "number",
constraints: {
min: 0,
max: 120
}
}
В текстовой визуализации это может выглядеть так:
age: number (0..120)
Superstruct не всегда хранит явное описание схемы в виде данных, так как многие структуры являются функциями. Поэтому применяется техника интроспекции.
Общий подход:
Пример:
function inspect(struct) {
return {
type: struct.typeName,
...(struct.schema ? mapSchema(struct.schema) : {})
}
}
Такая техника позволяет восстановить структуру даже из сложных композиций.
Массивы представляют собой особый случай, так как содержат повторяющийся элемент одного типа.
array
└── item: string
При вложенных массивах структура усложняется:
array
└── array
└── number
В графическом представлении такие конструкции часто отображаются с дополнительной маркировкой уровня вложенности или кратностью.
В реальных схемах часто встречаются повторяющиеся подструктуры. Для их отображения используется механизм ссылок.
Пример:
const Address = object({
city: string(),
zip: string()
})
const User = object({
name: string(),
address: Address
})
Визуализация:
User
├── name
└── address → Address
├── city
└── zip
Такое представление уменьшает дублирование и делает граф более компактным.
Визуализация используется не только для отображения, но и для сопоставления схем.
Алгоритм сравнения:
Результат может быть представлен как дифф-дерево:
- age: string
+ age: number
Это особенно полезно при эволюции API и схем данных.
Структуры Superstruct могут отображаться на разных уровнях детализации:
Каждый уровень требует отдельного способа визуализации, но базируется на одной и той же структуре дерева.
При построении сложных схем возможно появление циклов через повторное использование структур. Для предотвращения бесконечной рекурсии применяется механизм кеширования посещённых узлов.
const visited = new WeakSet()
function safeTraverse(struct) {
if (visited.has(struct)) return { type: 'ref' }
visited.add(struct)
// дальнейший обход
}
В визуализации такие узлы отображаются как ссылки, а не как повторные поддеревья.