Skip to content

Promote per-adapter card helpers into shared/adapter_utils.py (emoji converter, button styles, GFM table, fallback text) #70

@patrick-chinchill

Description

@patrick-chinchill

Code-organization gap — drift risk across 8 adapters.

Current state

src/chat_sdk/shared/adapter_utils.py exposes only extract_card() and extract_files(). Every other cross-adapter helper lives duplicated inside each adapter's cards.py / format_converter.py:

  • Emoji placeholder resolution ({{emoji:thumbs_up}} → platform-native code). Each adapter imports DEFAULT_EMOJI_MAP from shared/ and walks children independently. Slack, Discord, Telegram, Teams, Google Chat, Linear, GitHub all do this; WhatsApp doesn't (because it doesn't translate emoji placeholders at all — consistent gap).
  • Button style mapping (style: "primary" | "secondary" | "danger" → Block Kit "primary" / AdaptiveCard "positive" / Discord 1..4 / Linear mention syntax). Scattered across each adapter.
  • Card fallback text (render a Card to plain markdown for platforms with limited card support, or for notification previews). Reinvented in Telegram and WhatsApp; partially in Discord.
  • GFM table rendering (render Table to an ASCII-aligned markdown table for adapters without native table blocks). Duplicated in Telegram, WhatsApp, Linear; Discord has its own; Slack has a separate Block-Kit-first path.
  • Table cell escaping (|, newlines, backticks inside cells). Each adapter that renders tables has its own escape routine; subtle bugs have bitten us in Slack tables specifically.

Why it matters

  • Drift: eight adapters solving the same problem independently means eight different bug surfaces for the same logical operation. We've already shipped one fix to Slack empty-header cells (0.4.26) that didn't propagate to other adapters because there was no shared routine to fix.
  • Maintenance: new platform emoji added to DEFAULT_EMOJI_MAP requires audit of every adapter to confirm it's picked up. A shared converter removes the audit.
  • Consistency: "render this Card as fallback text" should produce the same output regardless of which adapter is asking. Today it doesn't.

Proposed surface

In src/chat_sdk/shared/adapter_utils.py:

def create_emoji_converter(
    platform_map: Mapping[str, str] | None = None,
) -> Callable[[str], str]:
    """Return a function that replaces {{emoji:name}} placeholders with platform-native code."""

def map_button_style(
    style: ButtonStyle,
    target: Literal["slack", "teams", "discord", "telegram", "whatsapp", "gchat", "linear", "github"],
) -> str | int:
    """Translate SDK button style to the target platform's native representation."""

def card_to_fallback_text(card: Card, *, emoji_converter: Callable[[str], str] | None = None) -> str:
    """Render a Card to plain markdown text, honoring emoji placeholders."""

def render_gfm_table(table: Table, *, aligned: bool = True) -> str:
    """Render a Table to a GFM-flavored markdown table, optionally padding for visual alignment."""

def escape_table_cell(text: str) -> str:
    """Escape `|`, newlines, and backticks for safe inclusion inside a markdown table cell."""

Each helper replaces the per-adapter copy. Per-adapter code stays responsible for (a) choosing which helpers to call, (b) mapping to platform-specific payload shapes (Block Kit, AdaptiveCard, Discord components, etc.) — that's legitimately platform-specific and shouldn't move.

Migration

One PR per helper, in this order:

  1. escape_table_cell (most localized; low risk)
  2. render_gfm_table (builds on security: Fix all critical and high findings from security audit #1)
  3. card_to_fallback_text (uses security: Fix all critical and high findings from security audit #1 + fix: correct GitHub Actions commit SHAs #2)
  4. map_button_style (independent)
  5. create_emoji_converter (largest blast radius; save for last when the pattern is settled)

Each PR: promote the helper, update all adapters to use it, delete the duplicated copies, add regression tests in shared/ for the helper itself + keep existing adapter tests passing.

Acceptance

  • All five helpers live in shared/adapter_utils.py with direct unit tests
  • Each of the 8 adapters calls the shared helper instead of a local copy
  • Each migration PR shows net LOC deletion
  • No behavior change (captured by existing adapter tests; add a parity test that runs the same Card through every adapter's fallback path and snapshots the output)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions