| # Copyright 2026 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import functools |
| from typing import Any, ClassVar, Generic, TypeVar |
| |
| from pydantic import BaseModel, ConfigDict, TypeAdapter |
| |
| PROTOCOL_VERSION = 16 |
| |
| T_Resp = TypeVar("T_Resp") |
| |
| |
| class BaseRequest(BaseModel, Generic[T_Resp]): |
| """Base class for all requests, enforcing keyword-only instantiation.""" |
| |
| model_config = ConfigDict(kw_only=True) |
| |
| command: str |
| last_seen_seq: int | None = None |
| ack_seq: int | None = None |
| response_type: ClassVar[Any] = dict[str, Any] |
| |
| |
| class Response(BaseModel, Generic[T_Resp]): |
| """Standard response wrapper.""" |
| |
| success: bool |
| message: str | None = None |
| body: T_Resp | None = None |
| events: list[dict[str, Any]] | None = None |
| |
| |
| class ProtocolRegistry: |
| request_adapter: TypeAdapter[Any] | None = None |
| response_adapter: TypeAdapter[Any] | None = None |
| |
| |
| def serialize(obj: BaseModel) -> str: |
| return obj.model_dump_json() + "\n" |
| |
| |
| def make_request(data: dict[str, Any]) -> BaseRequest[Any]: |
| if ProtocolRegistry.request_adapter is None: |
| raise RuntimeError("ProtocolRegistry not initialized") |
| return ProtocolRegistry.request_adapter.validate_python(data) |
| |
| |
| def deserialize_request(line: str) -> BaseRequest[Any]: |
| if ProtocolRegistry.request_adapter is None: |
| raise RuntimeError("ProtocolRegistry not initialized") |
| return ProtocolRegistry.request_adapter.validate_json(line.strip()) |
| |
| |
| @functools.lru_cache(maxsize=32) |
| def _get_response_adapter(resp_type: Any) -> TypeAdapter[Any]: |
| """Build and cache a TypeAdapter for the specialized Response[resp_type]. |
| |
| Constructing a TypeAdapter compiles Pydantic's core validation graph. |
| Caching by response_type allows reuse across responses. |
| """ |
| target_cls: Any = Response[resp_type] |
| return TypeAdapter(target_cls) |
| |
| |
| def deserialize_response( |
| line: str, req: BaseRequest[T_Resp] |
| ) -> Response[T_Resp]: |
| """Deserialize and validate a raw JSON response string against req's response_type.""" |
| return _get_response_adapter(req.response_type).validate_json(line.strip()) |
| |
| |
| def get_schema() -> dict[str, Any]: |
| if ( |
| ProtocolRegistry.request_adapter is None |
| or ProtocolRegistry.response_adapter is None |
| ): |
| raise RuntimeError("ProtocolRegistry not initialized") |
| return { |
| "$schema": "https://json-schema.org/draft/2020-12/schema", |
| "title": "zxdb-cli Protocol Schema", |
| "description": ( |
| "JSON schema for requests and responses in the " |
| "zxdb-cli UDS protocol" |
| ), |
| "version": PROTOCOL_VERSION, |
| "requests": ProtocolRegistry.request_adapter.json_schema(), |
| "responses": ProtocolRegistry.response_adapter.json_schema(), |
| } |