Визуализация структуры

Любая схема, описанная с помощью Superstruct, по своей сути формирует иерархическое дерево. Узлы этого дерева соответствуют примитивным и составным типам, а связи отражают вложенность и композицию правил валидации.

Базовые примитивы — строки, числа, булевы значения — являются листьями структуры. Составные типы, такие как объекты, массивы, объединения и пересечения, формируют внутренние узлы, объединяющие несколько подструктур в единый контракт.

Рассмотрение структуры как дерева позволяет перейти от декларативного описания к визуальному представлению, удобному для анализа, отладки и документирования.


Преобразование struct-описаний в иерархию

В 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 содержит информацию о своём типе и дочерних элементах.

Типовая стратегия обхода:

  1. Определение текущего узла (object, array, union и т.д.)
  2. Извлечение дочерних структур
  3. Рекурсивный обход каждого потомка
  4. Формирование промежуточного представления

Псевдокод:

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

Такое представление позволяет быстро оценить глубину и сложность структуры.


Графическое представление через Graphviz

Для более сложных схем используется графовое представление. 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-представление

Промежуточный формат часто строится в виде JSON-дерева. Это упрощает интеграцию с внешними инструментами визуализации.

Пример:

{
  type: "object",
  children: [
    { key: "id", type: "number" },
    { key: "name", type: "string" },
    {
      key: "roles",
      type: "array",
      children: [{ type: "string" }]
    }
  ]
}

JSON-структура используется для построения интерфейсных визуализаторов, где каждый узел отображается как компонент.


Обработка union и пересечений

Особую сложность в визуализации создают объединения типов. Union представляет альтернативные ветви, каждая из которых должна отображаться отдельно.

union
├── string
└── number

При пересечениях (intersection) структура становится многослойной:

intersection
├── object A
│   ├── field x
└── object B
    ├── field y

Визуально такие конструкции часто отображаются как наложенные или связанные узлы, поскольку они описывают один итоговый контракт.


Подписи узлов и аннотации

Для повышения читаемости структуры в узлы добавляются дополнительные метаданные:

  • название поля
  • тип значения
  • ограничения (min, max, pattern)
  • описание

Пример расширенного узла:

{
  key: "age",
  type: "number",
  constraints: {
    min: 0,
    max: 120
  }
}

В текстовой визуализации это может выглядеть так:

age: number (0..120)

Автоматическое извлечение схемы из struct

Superstruct не всегда хранит явное описание схемы в виде данных, так как многие структуры являются функциями. Поэтому применяется техника интроспекции.

Общий подход:

  1. Оборачивается struct-функция
  2. Перехватываются вызовы создания узлов
  3. Формируется мета-описание

Пример:

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 могут отображаться на разных уровнях детализации:

  1. Логический уровень — типы и связи
  2. Семантический уровень — смысл полей и ограничений
  3. Физический уровень — JSON/Graph представление
  4. Диагностический уровень — ошибки валидации и отклонения

Каждый уровень требует отдельного способа визуализации, но базируется на одной и той же структуре дерева.


Проблема циклических ссылок

При построении сложных схем возможно появление циклов через повторное использование структур. Для предотвращения бесконечной рекурсии применяется механизм кеширования посещённых узлов.

const visited = new WeakSet()

function safeTraverse(struct) {
  if (visited.has(struct)) return { type: 'ref' }

  visited.add(struct)
  // дальнейший обход
}

В визуализации такие узлы отображаются как ссылки, а не как повторные поддеревья.