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
| Kriteria | JSON Patch (RFC 6902) | Key-Value Edits (Targeted) |
|---|---|---|
| Kopling Struktur | Tinggi (Klien harus tahu struktur internal AST template) | Rendah (Hanya memerlukan placeholder key) |
| Resiko Layout Breaking | Tinggi jika operasi remove/add mengubah container | Rendah (Mutasi terbatas pada nilai teks atau aset) |
| Ukuran Payload | Sedang hingga besar tergantung panjang path | Kecil dan mudah dibaca (Human-readable) |
| Validasi Schema | Sulit divalidasi secara kontekstual per slide | Mudah 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.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!