Skip to content

erdantic.d2

render_d2

render_d2(diagram: EntityRelationshipDiagram) -> str

Renders an EntityRelationshipDiagram into the D2 class diagram format.

Source code in erdantic/d2.py
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def render_d2(diagram: EntityRelationshipDiagram) -> str:
    """Renders an EntityRelationshipDiagram into the D2 class diagram format."""
    d2_parts: list[str] = []

    # Define all class shapes first
    for model in diagram.models.values():
        class_name = _quote_identifier(model.name)
        class_def = [f"{class_name}: {{", "  shape: class"]

        if not model.fields:
            class_def.append("  # This class has no fields to display in the diagram.")
        else:
            for field in model.fields.values():
                field_type = _maybe_quote_value(field.type_name)
                visibility = _get_visibility_prefix(field.name)
                class_def.append(f"  {visibility}{field.name}: {field_type}")

        class_def.append("}\n")
        d2_parts.append("\n".join(class_def))

    # Define all relationships between classes
    for edge in diagram.edges.values():
        source_model = diagram.models.get(str(edge.source_model_full_name))
        target_model = diagram.models.get(str(edge.target_model_full_name))
        if not source_model or not target_model:
            continue

        source_model_name = _quote_identifier(source_model.name)
        target_model_name = _quote_identifier(target_model.name)
        label = _quote_identifier(edge.source_field_name)

        connection = "->"  # Directed from source to target

        target_shape = _get_crowsfoot_d2(edge.target_cardinality, edge.target_modality)
        attributes = [f"target-arrowhead.shape: {target_shape}"]

        # Source side: omit entirely when both are UNSPECIFIED. Otherwise, map and include.
        if not (
            edge.source_cardinality == Cardinality.UNSPECIFIED
            and edge.source_modality == Modality.UNSPECIFIED
        ):
            source_shape = _get_crowsfoot_d2(edge.source_cardinality, edge.source_modality)
            attributes.append(f"source-arrowhead.shape: {source_shape}")
            connection = "<->"  # Bidirectional if source side is specified

        d2_parts.append(
            _REL_DEF_TEMPLATE.format(
                source_model_name=source_model_name,
                connection=connection,
                target_model_name=target_model_name,
                label=label,
                attributes=indent("\n".join(attributes), " " * 2),
            )
        )

    return "\n".join(d2_parts)