Spesifikasi OpenAPI memberi tahu Anda seperti apa request yang valid dan seperti apa response yang valid. Spesifikasi tersebut tidak memberi tahu Anda apakah Anda diizinkan untuk memanggil POST /orders sebelum POST /auth, atau apa yang terjadi jika Anda memanggil GET /invoice/{id} setelah DELETE /invoice/{id}. Informasi itu berada dalam protocol spec, dan OpenAPI bukanlah protocol spec.

Ini adalah celahnya. Anda bisa menghasilkan setiap tipe pesan dalam sistem Anda dari dokumen OpenAPI. Anda tidak bisa menghasilkan aturan tentang kapan pesan-pesan tersebut diizinkan untuk dikirim. Aturan-aturan itu adalah session type, dan mereka berada di lapisan abstraksi yang berbeda.

Apa yang OpenAPI tangkap dan apa yang diabaikannya

OpenAPI adalah contract untuk luas permukaan HTTP. OpenAPI mendefinisikan path, method, query parameter, request body, response code, dan skema JSON. Ini berharga. Ini juga pada dasarnya statis. Setiap endpoint dideskripsikan secara terpisah. Hubungan antar endpoint, transisi state yang mereka picu, dan urutan yang legal dibiarkan sebagai latihan bagi pembaca.

Session types adalah kebalikannya. Session type adalah deskripsi formal dari protocol komunikasi. Session type menentukan urutan di mana pesan harus dikirim dan diterima, siapa yang mengirim apa, dan bagaimana protocol bercabang berdasarkan konten pesan. Session type biner mungkin mengatakan: klien mengirim pesan Login, lalu server merespons dengan Success dan protocol berlanjut, atau merespons dengan Failure dan protocol berakhir.

Anda tidak bisa menurunkan urutan itu dari spesifikasi OpenAPI karena urutan itu tidak pernah dituliskan. Dokumen OpenAPI hanya mencantumkan endpoint-endpoint. Dokumen tersebut tidak menentukan bahwa /auth harus mendahului /orders. Constraint itu berada dalam dokumentasi, kode, atau pikiran engineer yang merancang API.

Apa yang bisa Anda ekstrak: lapisan pesan

Apa yang OpenAPI berikan adalah definisi pesan yang presisi dan dapat dibaca mesin. Setiap skema request body, setiap skema response, setiap enum dan discriminator ditentukan dalam JSON Schema. Ini adalah bahan baku untuk tipe pesan dalam spesifikasi session type.

Berikut contoh konkret. Pertimbangkan fragmen OpenAPI ini:

paths:
  /auth:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                username: { type: string }
                password: { type: string }
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
        '401':
          description: Unauthorized

Dari ini, Anda bisa mengekstrak dua tipe pesan: sebuah AuthRequest yang berisi username dan password, dan sebuah AuthResponse yang berupa Success dengan token atau Failure. Session type membutuhkan tipe-tipe ini. Session type juga perlu mengetahui bahwa klien mengirim request dan server mengirim response, yang tersirat melalui semantik HTTP.

Anda bisa mengotomatisasi ekstraksi ini. Berikut skrip Python yang mengurai spesifikasi JSON OpenAPI dan menghasilkan definisi pesan mirip Rust:

import json
from dataclasses import dataclass

@dataclass
class Field:
    name: str
    type: str

@dataclass
class Message:
    name: str
    fields: list[Field]

TYPE_MAP = {"string": "String", "integer": "i64", "boolean": "bool"}

def sanitize_name(path: str, method: str, suffix: str) -> str:
    cleaned = path.replace("/", "").replace("{", "").replace("}", "")
    return f"{method.upper()}{cleaned}{suffix}"

def extract_messages(openapi_path: str) -> list[Message]:
    with open(openapi_path) as f:
        spec = json.load(f)

    messages = []
    for path, methods in spec.get("paths", {}).items():
        for method, operation in methods.items():
            if not isinstance(operation, dict):
                continue

            req = operation.get("requestBody", {})
            schema = req.get("content", {}).get("application/json", {}).get("schema", {})
            if schema:
                fields = [
                    Field(name=k, type=TYPE_MAP.get(v.get("type"), "serde_json::Value"))
                    for k, v in schema.get("properties", {}).items()
                ]
                messages.append(Message(name=sanitize_name(path, method, "Request"), fields=fields))

            for code, resp in operation.get("responses", {}).items():
                schema = resp.get("content", {}).get("application/json", {}).get("schema", {})
                if schema:
                    fields = [
                        Field(name=k, type=TYPE_MAP.get(v.get("type"), "serde_json::Value"))
                        for k, v in schema.get("properties", {}).items()
                    ]
                    messages.append(Message(name=sanitize_name(path, method, f"Response{code}"), fields=fields))

    return messages

if __name__ == "__main__":
    for m in extract_messages("api.json"):
        print(f"struct {m.name} {{")
        for f in m.fields:
            print(f"    {f.name}: {f.type},")
        print("}")

Ini bersifat mekanis, tetapi berfungsi. Ini mengubah dokumen OpenAPI Anda menjadi struct yang bisa direferensikan oleh implementasi session type. Tipe-tipe tersebut akurat. Nama-namanya literal. Hubungan-hubungannya hilang.

Di mana informasi urutan berada

Untuk mendapatkan protocol, Anda perlu mengetahui state machine. Beberapa API menyandikan ini secara implisit. Sebuah POST /orders mengembalikan ID pesanan, dan pemanggilan GET /orders/{id} berikutnya mereferensikannya. State protocol mencakup “sebuah pesanan telah dibuat.” Spesifikasi OpenAPI tidak memodelkan dependency ini.

Ada standar baru yang mencoba menjembatani ini. AsyncAPI menangani protocol yang digerakkan oleh event dengan semantik channel, tetapi masih tidak memberikan session type global. Smithy mendefinisikan operation dan traits, dan bisa memodelkan finite state machine melalui traits kustom, tetapi membutuhkan anotasi eksplisit. JSON Hyper-Schema mencoba menautkan resource tetapi tidak pernah melihat adopsi yang luas.

Untuk saat ini, pendekatan praktis adalah memperlakukan protocol sebagai artefak yang terpisah. Anda menghasilkan tipe pesan dari OpenAPI, lalu menulis session type secara manual di atasnya.

Hybrid praktis: tipe yang dihasilkan ditambah protocol yang ditulis tangan

Berikut tampilannya dalam praktik menggunakan Rust dan crate session_types. Pertama, hasilkan tipe pesan dari OpenAPI menggunakan skrip di atas atau alat seperti typify. Lalu definisikan session type secara eksplisit:

use session_types::*;

// Generated from OpenAPI
struct AuthRequest { username: String, password: String }
struct AuthSuccess { token: String }
struct AuthFailure { reason: String }
struct OrderRequest { item_id: u64, quantity: u64 }
struct OrderConfirmation { order_id: u64 }

// The protocol: authenticate, then optionally place an order
type ServerProto = Receive<AuthRequest, Offer<
    Send<AuthSuccess, Receive<OrderRequest, Send<OrderConfirmation, Eps>>>,
    Send<AuthFailure, Eps>
>>;

fn server(c: Chan<(), ServerProto>) {
    let (c, req) = c.recv();
    if authenticate(&req) {
        let c = c.sel1().send(AuthSuccess { token: "abc".into() });
        let (c, order) = c.recv();
        let c = c.send(OrderConfirmation { order_id: 42 });
        c.close();
    } else {
        let c = c.sel2().send(AuthFailure { reason: "bad creds".into() });
        c.close();
    }
}

Session type ServerProto menentukan persis apa yang dilakukan server. Session type menerima AuthRequest. Lalu session type menawarkan pilihan: kirim AuthSuccess dan lanjut menerima OrderRequest, atau kirim AuthFailure dan berakhir. Spesifikasi OpenAPI memberi kita struct-struct. Session type memberi kita tata bahasanya.

Ini adalah pembagian kerja. OpenAPI menangani bentuk pesan. Session types menangani urutan pesan. Salah satunya dihasilkan, yang lainnya dirancang.

Trade-off yang harus Anda ketahui

Ekstraksi otomatis dari OpenAPI memiliki sisi tajam. Yang pertama adalah polimorfisme. OpenAPI menggunakan oneOf dan anyOf untuk union, tetapi informasi discriminator tidak dipetakan dengan bersih ke percabangan session type. Sebuah oneOf dalam skema response mungkin merepresentasikan dua bentuk success yang berbeda, atau mungkin merepresentasikan percabangan protocol. Anda harus membaca spesifikasi untuk mengetahui yang mana.

Yang kedua adalah semantik khusus HTTP. Session types bersifat transport-agnostik. OpenAPI terikat erat dengan HTTP method, status code, dan header. Saat Anda mengekstrak pesan, Anda kehilangan informasi method dan path kecuali Anda menyandikannya ke dalam nama pesan. Inilah mengapa skrip Python di atas menghasilkan nama seperti POSTAuthResponse200. Ini jelek, tetapi menjaga jejak sumbernya.

Yang ketiga adalah spesifikasi parsial. Banyak dokumen OpenAPI dihasilkan dari kode dan tidak menyertakan semua response error. Jika session type Anda perlu menangani setiap percabangan yang mungkin, spesifikasi OpenAPI yang tidak lengkap akan menghasilkan session type yang tidak lengkap. Type checker tidak akan menangkap percabangan yang hilang jika percabangan tersebut tidak pernah ada dalam dokumen sumber.

Cara mengimplementasikan ini di codebase Anda

Mulailah dari pesan. Gunakan openapi-typescript, typify, atau skrip kustom untuk menghasilkan tipe spesifik bahasa dari skema OpenAPI Anda. Jangan mencoba menghasilkan protocol di tahap ini. Cukup dapatkan struct-structnya.

Selanjutnya, tulis session type untuk alur paling kritis dalam sistem Anda. Pilih alur di mana bug urutan akan paling mahal. Autentikasi dan pembayaran adalah kandidat yang baik. Definisikan session type dalam bahasa pilihan Anda. Rust memiliki session_types. Ada library eksperimental untuk TypeScript, OCaml, dan Scala. Jika bahasa Anda tidak memiliki library session type, tulis state machine test yang menegaskan urutan yang valid. Ini bukan jaminan yang sama, tetapi menangkap kelas bug yang sama.

Akhirnya, tetapkan dua artefak tetap sinkron dengan CI. Saat spesifikasi OpenAPI berubah, hasilkan ulang tipe pesan. Jika field baru yang wajib ditambahkan, kode session type akan gagal dikompilasi. Itulah keamanan yang Anda beli.

FAQ

Apakah ini berfungsi untuk API WebSocket atau gRPC?

Tidak langsung. OpenAPI mendeskripsikan HTTP. Untuk gRPC, Anda memiliki skema protobuf, yang memberi Anda tipe pesan tetapi tetap tidak memberikan urutan. Untuk WebSocket, AsyncAPI lebih sesuai, tetapi keterbatasan yang sama berlaku: AsyncAPI mendeskripsikan channel, bukan protocol session global.

Bisakah saya menghasilkan session types dari AsyncAPI?

AsyncAPI menambahkan semantik channel dan routing pesan, yang membawa Anda lebih dekat. AsyncAPI masih tidak menentukan state machine protocol global. Anda akan mengekstrak tipe pesan dari AsyncAPI dan menulis session type secara manual, sama seperti dengan OpenAPI.

Bagaimana jika API saya memiliki lima puluh endpoint?

Lima puluh endpoint berarti lima puluh tipe pesan, bukan satu session type dengan lima puluh langkah. Protocol nyata terdekomposisi menjadi session-session yang lebih kecil. API e-commerce mungkin memiliki session terpisah untuk autentikasi, checkout, dan inventory. Komposisikan mereka.

Apakah ada alat yang melakukan semua ini secara otomatis?

Belum. Risetnya ada. Ada paper tentang mengekstrak session types dari REST API dan dari choreographies. Tooling yang siap produksi belum ada. Untuk saat ini, pendekatan hybrid adalah jalan yang pragmatis.

Spesifikasi OpenAPI Anda adalah kamus. Spesifikasi memberi tahu Anda arti kata-kata. Session type adalah tata bahasa. Session type memberi tahu Anda kalimat mana yang legal. Hasilkan kosakata dari OpenAPI. Tulis tata bahasanya sendiri.