clan-api: wrap all api responses with error/success envelop type

This commit is contained in:
Johannes Kirschbauer
2024-06-05 09:46:48 +02:00
parent db88e63148
commit 6576290160
4 changed files with 72 additions and 15 deletions

View File

@@ -1,10 +1,12 @@
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Generic, Literal, TypeVar from functools import wraps
from typing import Annotated, Any, Generic, Literal, TypeVar, get_type_hints
from clan_cli.errors import ClanError
T = TypeVar("T") T = TypeVar("T")
ResponseDataType = TypeVar("ResponseDataType") ResponseDataType = TypeVar("ResponseDataType")
@@ -16,22 +18,55 @@ class ApiError:
@dataclass @dataclass
class ApiResponse(Generic[ResponseDataType]): class SuccessDataClass(Generic[ResponseDataType]):
status: Literal["success", "error"] status: Annotated[Literal["success"], "The status of the response."]
errors: list[ApiError] | None data: ResponseDataType
data: ResponseDataType | None
@dataclass
class ErrorDataClass:
status: Literal["error"]
errors: list[ApiError]
ApiResponse = SuccessDataClass[ResponseDataType] | ErrorDataClass
class _MethodRegistry: class _MethodRegistry:
def __init__(self) -> None: def __init__(self) -> None:
self._orig: dict[str, Callable[[Any], Any]] = {}
self._registry: dict[str, Callable[[Any], Any]] = {} self._registry: dict[str, Callable[[Any], Any]] = {}
def register(self, fn: Callable[..., T]) -> Callable[..., T]: def register(self, fn: Callable[..., T]) -> Callable[..., T]:
self._registry[fn.__name__] = fn self._orig[fn.__name__] = fn
@wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> ApiResponse[T]:
try:
data: T = fn(*args, **kwargs)
return SuccessDataClass(status="success", data=data)
except ClanError as e:
return ErrorDataClass(
status="error",
errors=[
ApiError(
message=e.msg,
description=e.description,
location=[fn.__name__, e.location],
)
],
)
# @wraps preserves all metadata of fn
# we need to update the annotation, because our wrapper changes the return type
# This overrides the new return type annotation with the generic typeVar filled in
orig_return_type = get_type_hints(fn).get("return")
wrapper.__annotations__["return"] = ApiResponse[orig_return_type] # type: ignore
self._registry[fn.__name__] = wrapper
return fn return fn
def to_json_schema(self) -> dict[str, Any]: def to_json_schema(self) -> dict[str, Any]:
# Import only when needed
from typing import get_type_hints from typing import get_type_hints
from clan_cli.api.util import type_to_dict from clan_cli.api.util import type_to_dict

View File

@@ -8,9 +8,8 @@ import {
import { OperationResponse, pyApi } from "./message"; import { OperationResponse, pyApi } from "./message";
export const makeCountContext = () => { export const makeCountContext = () => {
const [machines, setMachines] = createSignal< const [machines, setMachines] =
OperationResponse<"list_machines"> createSignal<OperationResponse<"list_machines">>();
>([]);
const [loading, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
pyApi.list_machines.receive((machines) => { pyApi.list_machines.receive((machines) => {
@@ -41,7 +40,7 @@ export const CountContext = createContext<CountContextType>([
loading: () => false, loading: () => false,
// eslint-disable-next-line // eslint-disable-next-line
machines: () => ([]), machines: () => undefined,
}, },
{ {
// eslint-disable-next-line // eslint-disable-next-line

View File

@@ -72,6 +72,12 @@ const deserialize =
// Create the API object // Create the API object
const pyApi: PyApi = {} as PyApi; const pyApi: PyApi = {} as PyApi;
pyApi.create_clan.receive((r) => {
if (r.status === "success") {
r.status;
}
});
operationNames.forEach((opName) => { operationNames.forEach((opName) => {
const name = opName as OperationNames; const name = opName as OperationNames;
// @ts-expect-error - TODO: Fix this. Typescript is not recognizing the receive function correctly // @ts-expect-error - TODO: Fix this. Typescript is not recognizing the receive function correctly

View File

@@ -1,13 +1,30 @@
import { For, Match, Switch, createEffect, type Component } from "solid-js"; import {
For,
Match,
Switch,
createEffect,
createSignal,
type Component,
} from "solid-js";
import { useCountContext } from "../../Config"; import { useCountContext } from "../../Config";
import { route } from "@/src/App"; import { route } from "@/src/App";
export const MachineListView: Component = () => { export const MachineListView: Component = () => {
const [{ machines, loading }, { getMachines }] = useCountContext(); const [{ machines, loading }, { getMachines }] = useCountContext();
const [data, setData] = createSignal<string[]>([]);
createEffect(() => { createEffect(() => {
if (route() === "machines") getMachines(); if (route() === "machines") getMachines();
}); });
createEffect(() => {
const response = machines();
if (response?.status === "success") {
console.log(response.data);
setData(response.data);
}
});
return ( return (
<div class="max-w-screen-lg"> <div class="max-w-screen-lg">
<div class="tooltip" data-tip="Refresh "> <div class="tooltip" data-tip="Refresh ">
@@ -32,12 +49,12 @@ export const MachineListView: Component = () => {
</div> </div>
</div> </div>
</Match> </Match>
<Match when={!loading() && machines().length === 0}> <Match when={!loading() && data().length === 0}>
No machines found No machines found
</Match> </Match>
<Match when={!loading()}> <Match when={!loading()}>
<ul> <ul>
<For each={machines()}> <For each={data()}>
{(entry) => ( {(entry) => (
<li> <li>
<div class="card card-side m-2 bg-base-100 shadow-lg"> <div class="card card-side m-2 bg-base-100 shadow-lg">