Skip to main content

az_aio_client/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3
4use az_config_center_contract::{
5    DESKTOP_SESSION_TOKEN_HEADER, DesktopBackendStatus, ShellComponent, ShellComponentBuildRequest,
6    ShellComponentBuildResult, ShellComponentConfigUpdate, ShellComponentPatch,
7    ShellComponentRegistry, ShellComponentRemove, ShellComponentUpsert,
8};
9use reqwest::Method;
10use reqwest::blocking::{Client, Response};
11use serde::Serialize;
12use serde::de::DeserializeOwned;
13use thiserror::Error;
14
15pub type AioClientResult<T> = Result<T, AioClientError>;
16
17#[derive(Debug, Error)]
18pub enum AioClientError {
19    #[error("request to {url} failed: {source}")]
20    Transport {
21        url: String,
22        #[source]
23        source: reqwest::Error,
24    },
25    #[error("request to {url} returned {status}: {body}")]
26    Http {
27        url: String,
28        status: reqwest::StatusCode,
29        body: String,
30    },
31}
32
33#[derive(Clone, Debug)]
34pub struct AioClient {
35    base_url: String,
36    desktop_token: Option<String>,
37    http: Client,
38}
39
40impl AioClient {
41    pub fn new(base_url: impl Into<String>) -> Self {
42        Self {
43            base_url: normalize_base_url(base_url.into()),
44            desktop_token: None,
45            http: Client::new(),
46        }
47    }
48
49    pub fn with_desktop_token(
50        base_url: impl Into<String>,
51        desktop_token: impl Into<String>,
52    ) -> Self {
53        Self {
54            base_url: normalize_base_url(base_url.into()),
55            desktop_token: Some(desktop_token.into()),
56            http: Client::new(),
57        }
58    }
59
60    pub fn base_url(&self) -> &str {
61        &self.base_url
62    }
63
64    pub fn desktop_status(&self) -> AioClientResult<DesktopBackendStatus> {
65        self.request(Method::GET, "/api/desktop/status", None::<&()>)
66    }
67
68    pub fn list_shell_components(&self) -> AioClientResult<ShellComponentRegistry> {
69        self.request(Method::GET, "/api/shell-components", None::<&()>)
70    }
71
72    pub fn get_shell_component(&self, name: &str) -> AioClientResult<Option<ShellComponent>> {
73        self.request(
74            Method::GET,
75            &format!("/api/shell-components/{}", urlencoding::encode(name)),
76            None::<&()>,
77        )
78    }
79
80    pub fn upsert_shell_component(
81        &self,
82        input: &ShellComponentUpsert,
83    ) -> AioClientResult<ShellComponent> {
84        self.request(Method::POST, "/api/shell-components/upsert", Some(input))
85    }
86
87    pub fn patch_shell_component(
88        &self,
89        input: &ShellComponentPatch,
90    ) -> AioClientResult<ShellComponent> {
91        self.request(Method::POST, "/api/shell-components/patch", Some(input))
92    }
93
94    pub fn remove_shell_component(
95        &self,
96        input: &ShellComponentRemove,
97    ) -> AioClientResult<ShellComponent> {
98        self.request(Method::POST, "/api/shell-components/remove", Some(input))
99    }
100
101    pub fn save_shell_component_config(
102        &self,
103        input: &ShellComponentConfigUpdate,
104    ) -> AioClientResult<ShellComponentRegistry> {
105        self.request(Method::POST, "/api/shell-components/config", Some(input))
106    }
107
108    pub fn build_shell_components(
109        &self,
110        input: &ShellComponentBuildRequest,
111    ) -> AioClientResult<ShellComponentBuildResult> {
112        self.request(Method::POST, "/api/shell-components/build", Some(input))
113    }
114
115    fn request<T, B>(&self, method: Method, path: &str, body: Option<B>) -> AioClientResult<T>
116    where
117        T: DeserializeOwned,
118        B: Serialize,
119    {
120        let url = format!("{}{}", self.base_url, path);
121        let mut request = self.http.request(method, &url);
122        if let Some(token) = &self.desktop_token {
123            request = request.header(DESKTOP_SESSION_TOKEN_HEADER, token);
124        }
125        if let Some(body) = body {
126            request = request.json(&body);
127        }
128
129        let response = request.send().map_err(|source| AioClientError::Transport {
130            url: url.clone(),
131            source,
132        })?;
133        decode_json(url, response)
134    }
135}
136
137fn normalize_base_url(base_url: String) -> String {
138    base_url.trim_end_matches('/').to_string()
139}
140
141fn decode_json<T>(url: String, response: Response) -> AioClientResult<T>
142where
143    T: DeserializeOwned,
144{
145    let status = response.status();
146    if !status.is_success() {
147        let body = response.text().unwrap_or_default();
148        return Err(AioClientError::Http { url, status, body });
149    }
150    response
151        .json()
152        .map_err(|source| AioClientError::Transport { url, source })
153}