{# Bengal OpenAPI Schema Component Library (Kida) ============================================== Recursive {% def %} component for displaying JSON Schema / OpenAPI schema objects. Migrated from partials/schema-viewer.html (kept as a reference/fallback) to an explicit, importable component with no ambient {% let %} context. Usage: {% from 'autodoc/openapi/_schema.html' import schema_viewer %} {{ schema_viewer(schema, name=schema_name, depth=0) }} Parameters: - schema: OpenAPISchemaMetadata or dict (schema_type/type, properties, required, enum, example, items, description, and — for advanced specs — oneOf/anyOf/allOf, discriminator, additionalProperties, and per-property constraints/flags). - name: Schema name for display (optional, default none) - depth: Nesting depth for indentation + recursion guard (default 0) Advanced rendering (#285) is driven by template-function filters so the same recursion handles top-level schemas and nested raw property schemas: schema_composition / schema_constraints / schema_flags / schema_additional_properties. Each returns an empty/None result when its construct is absent, so simple schemas render exactly as before. Kida Features (confirmed against installed kida-templates): - {% def name(param=default) %} with recursive self-invocation - {% cache %} for expensive recursive rendering - {% with %} for nil-safe optional sections - Optional chaining (?.) and null coalescing (??) #} {% def schema_viewer(schema, name=none, depth=0) %} {% let schema_name = name | default('') %} {% let schema_type = schema?.schema_type ?? schema?.type ?? 'object' %} {% let properties = schema?.properties ?? {} %} {% let required_props = schema?.required ?? () %} {% let enum_vals = schema?.enum ?? none %} {% let items_schema = schema?.items ?? none %} {% let description = schema?.description ?? '' %} {% let current_depth = depth ?? 0 %} {% let max_depth = 4 %} {% let composition = schema | schema_composition %} {% let constraints = schema | schema_constraints %} {% let flags = schema | schema_flags %} {% let additional = schema | schema_additional_properties %} {% let ref_name = schema | schema_ref %} {# Distinct cache namespace ('oa-schema-def-') so this {% def %} never collides with the legacy partials/schema-viewer.html {% include %} (key prefix 'schema-viewer-') that list/responses/request-body still use. The legacy partial keys only on (name, depth); sharing that namespace caused anonymous nested schemas to return each other's cached fragments. #} {% cache 'oa-schema-def-' ~ (schema_name or 'anon') ~ '-' ~ current_depth %} {% if ref_name is not none %} {# Cyclic $ref left by the extractor: show a readable indicator, not an empty box. #}
↻ {{ ref_name }} (circular reference)
{% else %}
{# Schema Header #} {% if schema_name %}
{{ schema_name }} {{ schema_type }} {% if flags?.nullable %}nullable{% end %} {% if flags?.deprecated %}deprecated{% end %}
{% end %} {# Description #} {% if description %}
{{ description | markdownify | safe }}
{% end %} {# Validation constraints on a primitive / leaf schema #} {% if constraints | length > 0 %}
{% for clabel, cvalue in constraints |> items %} {{ clabel }}{% if cvalue %}: {{ cvalue }}{% end %} {% end %}
{% end %} {# Composition: oneOf / anyOf / allOf (+ discriminator) #} {% if composition is not none %}
{% if composition.kind == 'oneOf' %}One of{% elif composition.kind == 'anyOf' %}Any of{% else %}All of{% end %}
{% if composition.discriminator %}
Discriminated by {{ composition.discriminator.property_name }} {% if composition.discriminator.mapping | length > 0 %}
    {% for row in composition.discriminator.mapping %}
  • {{ row.value }} {{ row.target }}
  • {% end %}
{% end %}
{% end %}
{% for branch in composition.branches %} {% if composition.kind == 'allOf' %} {{ branch.label }} {{ branch.schema_type }} {% elif current_depth < max_depth %}
{{ schema_viewer(branch.schema, name=branch.label, depth=current_depth + 1) }}
{% else %} {{ branch.label }} ({{ branch.schema_type }})… {% end %} {% end %}
{% end %} {# Object Properties #} {% if schema_type == 'object' and properties | length > 0 %}
{% for prop_name, prop_schema in properties |> items %} {% let is_required = prop_name in required_props %} {% let prop_type = prop_schema?.type ?? prop_schema?.schema_type ?? 'any' %} {% let prop_desc = prop_schema?.description ?? '' %} {% let prop_enum = prop_schema?.enum %} {% let prop_default = prop_schema?.default %} {% let prop_flags = prop_schema | schema_flags %} {% let prop_constraints = prop_schema | schema_constraints %} {% let prop_composition = prop_schema | schema_composition %} {% let prop_additional = prop_schema | schema_additional_properties %} {% let prop_structural = prop_schema?.properties or prop_composition is not none or prop_additional is not none %}
{{ prop_name }} {{ prop_type }} {% if is_required %}required{% end %} {% if prop_flags?.nullable %}nullable{% end %} {% if prop_flags?.readOnly %}read-only{% end %} {% if prop_flags?.writeOnly %}write-only{% end %} {% if prop_flags?.deprecated %}deprecated{% end %}
{% if prop_desc %}
{{ prop_desc }}
{% end %} {# Per-property validation constraints (format, pattern, min/max, ...) #} {% if prop_constraints | length > 0 %}
{% for clabel, cvalue in prop_constraints |> items %} {{ clabel }}{% if cvalue %}: {{ cvalue }}{% end %} {% end %}
{% end %} {# Structural property: nested object, composition (oneOf/anyOf/allOf), or a typed/open map (additionalProperties). Recurse into the full viewer so inline compositions and maps render, not just top-level component schemas. Depth-bounded with a truncation marker. #} {% if prop_structural %} {% if current_depth < max_depth %}{{ schema_viewer(prop_schema, name=prop_name, depth=current_depth + 1) }}{% else %}{{ prop_name }} (nested {{ prop_type }})…{% end %} {% end %} {# Array items #} {% if prop_type == 'array' and prop_schema?.items %}
Items: {% if current_depth < max_depth %}{{ schema_viewer(prop_schema.items, name=prop_name ~ ' items', depth=current_depth + 1) }}{% else %}{{ prop_schema.items?.type ?? prop_schema.items?.schema_type ?? 'item' }}…{% end %}
{% end %} {# Property enum #} {% with prop_enum as enums %} {% if enums | length > 0 %}
Allowed: {% for val in enums %} {{ val }} {% end %}
{% end %} {% end %} {# Property default #} {% with prop_default as default %}
Default: {{ default }}
{% end %}
{% end %}
{% end %} {# Additional properties (typed maps / open objects) #} {% if additional is not none %}
{% if additional?.value_schema %} Additional properties {% if current_depth < max_depth %}{{ schema_viewer(additional.value_schema, name='(values)', depth=current_depth + 1) }}{% else %}{{ additional.schema_type }}{% end %} {% elif additional?.allowed %} Allows additional properties {% else %} No additional properties {% end %}
{% end %} {# Array Items Schema (top-level array) #} {% if schema_type == 'array' and items_schema %}
Array of: {% if current_depth < max_depth %}{{ schema_viewer(items_schema, name=schema_name ~ ' items', depth=current_depth + 1) }}{% else %}{{ items_schema?.type ?? items_schema?.schema_type ?? 'item' }}…{% end %}
{% end %} {# Enum Values (top-level) #} {% with enum_vals as enums %} {% if enums | length > 0 %}
Allowed values: {% for val in enums %} {{ val }} {% end %}
{% end %} {% end %} {# Examples: a singular `example`, a 3.1 `examples` list, or a named `examples` map are all normalized to a uniform tuple and rendered here. #} {% for ex in (schema | schema_examples) %}
{% if ex.name %}{{ ex.name }}{% else %}Example{% end %} {% if ex.summary %}{{ ex.summary }}{% end %}
{{ ex.value | tojson(indent=2) }}
{% end %}
{% end %} {% end %} {% end %}