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}