ESC

Type to search...

Custom Adapters

EVOID is a runtime, not a web framework. The adapter is the only layer that touches HTTP specifics — the runtime itself never sees requests, responses, or protocols. You write adapters for your specific needs.

The Adapter Contract

Every adapter implements two methods:

from typing import Any, Protocol
from evoid.core import Intent

class Adapter(Protocol):
    def intent_from(self, event: Any) -> Intent: ...
    def response_from(self, result: Any) -> Any: ...

intent_from converts external data (HTTP request, CLI args, MQTT message) into an Intent. response_from converts a Result back into whatever the external world expects.

Minimal Starlette Adapter

Here is a complete adapter that wraps any ASGI framework:

import json
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route

from evoid.core import Intent, Level, execute


def intent_from_request(method: str, path: str, body: bytes, headers: dict) -> Intent:
    data = {}
    if body:
        try:
            data = json.loads(body)
        except json.JSONDecodeError:
            data = {"raw": body.decode("utf-8", errors="replace")}

    level = Level.STANDARD
    intent_header = headers.get("x-intent-level", "").lower()
    if intent_header == "critical":
        level = Level.CRITICAL

    return Intent(
        name=f"{method.lower()}:{path}",
        level=level,
        metadata={"method": method, "path": path, "body": data, "headers": headers},
    )


async def handle(request: Request) -> JSONResponse:
    method = request.method
    path = request.url.path
    body = await request.body() if method in ("POST", "PUT", "PATCH") else b""
    headers = dict(request.headers)

    intent = intent_from_request(method, path, body, headers)
    result = await execute(intent)

    if result.success:
        return JSONResponse({"status": "success", "result": result.value})
    return JSONResponse({"error": str(result.error)}, status_code=500)


app = Starlette(routes=[Route("/{path:path}", handle, methods=["GET", "POST", "PUT", "DELETE"])])

Using the Protocol Directly

You can also implement the Adapter protocol as a class:

from dataclasses import dataclass
from evoid.contracts.adapter import Adapter
from evoid.core import Intent, Level, execute


@dataclass
class HttpAdapter:
    def intent_from(self, event: dict) -> Intent:
        return Intent(
            name=f"{event['method']}:{event['path']}",
            level=Level.STANDARD,
            metadata=event,
        )

    def response_from(self, result: Any) -> dict:
        if result.success:
            return {"status": "ok", "data": result.value}
        return {"status": "error", "error": str(result.error)}

Registering with the Runtime

The adapter integrates at the entry point. Your app calls the adapter, the adapter calls execute:

from evoid.core import Intent, execute

async def run_adapter(adapter, raw_event):
    intent = adapter.intent_from(raw_event)
    result = await execute(intent)
    return adapter.response_from(result)

@route and @controller Style

Both decorator styles auto-create intents — the adapter just needs to pass the request through:

from evoid.web.route import Service, get, post, run

app = Service("my-api")

@app.get("/users/{user_id}")
async def get_user(user_id: int) -> dict:
    return {"id": user_id}

# The ASGI adapter handles intent creation — you just write handlers
run(app, port=8000)

Native IOP Style

With native IOP, you define intents explicitly and write a handler that the adapter invokes:

from evoid import Intent, Level, add_intent

GET_USER = Intent(
    name="get:user",
    level=Level.STANDARD,
    metadata={"method": "GET", "path": "/users/{user_id}"},
)

async def handle_get_user(intent: Intent) -> dict:
    user_id = intent.metadata["params"]["user_id"]
    return {"id": user_id, "name": "Alice"}

add_intent(GET_USER, handle_get_user)

What Goes in metadata

Your adapter decides what goes into intent.metadata. Common fields:

FieldDescription
methodHTTP method (GET, POST, etc.)
pathRequest path
bodyParsed request body
headersRequest headers as dict
queryQuery parameters
paramsPath parameters