Generator dokumen dan presentasi otomatis sering menghadapi dua masalah utama: layout overflow akibat mutasi struktur yang tidak terkontrol, serta duplikasi rendering akibat pengiriman ulang payload berukuran besar. Layanan modifikasi dokumen berbasis template menuntut kontrak API yang bersifat deterministik dan non-destructive.

Skema Payload Deklaratif: JSON Patch vs Key-Value Edits

Terdapat dua pendekatan utama dalam memodelkan instruksi modifikasi dokumen berbasis JSON:

  • RFC 6902 (JSON Patch): Menggunakan array operasi atomik (replace, add, remove) berbasis path pointer seperti /slides/0/shapes/2/text. Cocok untuk modifikasi granular, tetapi berisiko tinggi merusak struktur dokumen jika klien mengubah hirarki node yang salah.
  • Declarative Key-Value Edits: Menargetkan variabel atau identifier unik yang sudah didefinisikan pada template (misalnya slide_1_hero_title). Pendekatan ini mengisolasi struktur presentasi dari modifikasi teks sehingga mempertahankan layout dokumen secara konsisten.

Perbandingan Karakteristik

KriteriaJSON Patch (RFC 6902)Key-Value Edits (Targeted)
Kopling StrukturTinggi (Klien harus tahu struktur internal AST template)Rendah (Hanya memerlukan placeholder key)
Resiko Layout BreakingTinggi jika operasi remove/add mengubah containerRendah (Mutasi terbatas pada nilai teks atau aset)
Ukuran PayloadSedang hingga besar tergantung panjang pathKecil dan mudah dibaca (Human-readable)
Validasi SchemaSulit divalidasi secara kontekstual per slideMudah divalidasi dengan constraint regex dan panjang string

Validasi Schema Strict untuk Mencegah Layout Breaking

Untuk mencegah text overflow dan payload injection (seperti injeksi tag format XML/HTML tidak valid pada engine PPT), field harus divalidasi secara ketat menggunakan regex, sanitasi input, dan batasan panjang karakter.

from enum import Enum
from typing import List, Optional
from pydantic import BaseModel, Field, constr

class PatchOperation(str, Enum):
    REPLACE_TEXT = "replace_text"
    REPLACE_IMAGE = "replace_image"

class DocumentPatchItem(BaseModel):
    target_id: constr(regex=r"^[a-zA-Z0-9_-]{3,64}$") = Field(
        ..., description="ID placeholder unik pada template dokumen"
    )
    op: PatchOperation
    # Batasi panjang karakter untuk mencegah teks keluar dari bounding box
    value: constr(min_length=1, max_length=280) = Field(
        ..., description="Konten teks pengganti"
    )

    class Config:
        extra = "forbid"

class GenerateDocumentRequest(BaseModel):
    template_id: constr(regex=r"^[a-f0-9]{32}$")
    patches: List[DocumentPatchItem] = Field(..., max_items=100)
    webhook_url: Optional[str] = Field(None, regex=r"^https://.*$")

Idempotensi pada Payload Rendering Besar

Proses kompilasi PPT/PDF memakan waktu dan sumber daya komputasi CPU yang tinggi. Jika klien mengalami timeout saat HTTP request berlangsung, mereka cenderung mengirim ulang request yang sama. Mekanisme Idempotency-Key berbasis Redis mencegah kompilasi ganda.

import hashlib
import json
from fastapi import FastAPI, Header, HTTPException, status, Depends
from redis import Redis

app = FastAPI()
redis_client = Redis(host="localhost", port=6379, db=0, decode_responses=True)

def verify_idempotency(
    idempotency_key: str = Header(..., alias="Idempotency-Key"),
    payload: GenerateDocumentRequest = Depends()
) -> str:
    # Hash payload untuk mendeteksi payload drift dengan key yang sama
    payload_hash = hashlib.sha256(payload.json().encode("utf-8")).hexdigest()
    redis_key = f"idempotency:{idempotency_key}"
    
    # Atomic SETNX: set jika key belum ada, expire 3600 detik
    acquired = redis_client.set(redis_key, f"PROCESSING:{payload_hash}", nx=True, ex=3600)
    
    if not acquired:
        current_state = redis_client.get(redis_key)
        if current_state == f"PROCESSING:{payload_hash}":
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail="Request dengan Idempotency-Key ini sedang diproses."
            )
        state_parts = current_state.split(":", 1)
        if state_parts[0] == "COMPLETED":
            # Mengembalikan response cached jika kompilasi selesai
            return json.loads(state_parts[1])
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="Idempotency-Key sudah digunakan dengan payload berbeda."
        )
    return ""

Implementasi Endpoint FastAPI

Pola handler menerima mutasi dokumen, memastikan validasi lolos, dan mendelegasikan proses rendering ke task worker di latar belakang.

from fastapi import BackgroundTasks

def dispatch_render_pipeline(job_id: str, payload_data: dict, webhook_url: Optional[str]):
    # Simulasi eksekusi rendering ke LibreOffice / PPTX AST compiler
    pass

@app.post("/v1/documents/generate", status_code=status.HTTP_202_ACCEPTED)
async def generate_document(
    request: GenerateDocumentRequest,
    background_tasks: BackgroundTasks,
    idempotency_key: str = Header(..., alias="Idempotency-Key"),
    cached_response: str = Depends(verify_idempotency)
):
    if cached_response:
        return cached_response

    job_id = f"job_{idempotency_key}"
    background_tasks.add_task(dispatch_render_pipeline, job_id, request.dict(), request.webhook_url)

    response_body = {"job_id": job_id, "status": "QUEUED"}
    return response_body

Penanganan Error Backend & Kontrak Webhook

Jika proses kompilasi gagal di level backend (misal: engine formatting crash atau font placeholder hilang), pesan kegagalan harus dipublikasikan ke klien melalui kontrak webhook terstruktur.

Gunakan format standard RFC 7807 (Problem Details) di dalam payload webhook untuk memberikan konteks kegagalan yang seragam.
{
  "event": "document.generation.failed",
  "job_id": "job_a8b9c1d2",
  "idempotency_key": "req_01HP8S9...",
  "error": {
    "type": "https://api.example.com/errors/layout-overflow",
    "title": "Text Exceeds Bounding Box",
    "status": 422,
    "detail": "Karakter pada target 'slide_1_hero_title' melebihi batas render visual maksimal (280px).",
    "instance": "/jobs/job_a8b9c1d2",
    "invalid_target": "slide_1_hero_title"
  }
}

Dengan memadukan pembatasan schema deklaratif, validasi Pydantic strict, dan penanganan status idempotensi di layer cache, arsitektur dokumen terhindar dari kerusakan format dan eksekusi rendering ganda.