oxyde_cloud_client/
lib.rs

1mod errors;
2
3use headers_core::Header;
4use log::error;
5use reqwest::header::{HeaderName, HeaderValue};
6use reqwest::multipart::{Form, Part};
7use reqwest::Body;
8use serde::{Deserialize, Serialize};
9use std::path::Path;
10use tokio_util::codec::{BytesCodec, FramedRead};
11
12pub use errors::*;
13use oxyde_cloud_common::config::CloudConfig;
14use oxyde_cloud_common::net::{
15    AppMeta, CheckAvailabilityResponse, LogRequest, LogResponse, LoginResponse, NewAppRequest,
16    NewTeamRequest, SetTeamNameRequest, SuccessResponse, Team,
17};
18
19const BASE_URL: Option<&str> = option_env!("OXYDE_CLOUD_API_URL");
20const DEFAULT_BASE_URL: &str = "https://oxyde.cloud/api/v1/";
21
22#[derive(Clone)]
23pub struct Client {
24    client: reqwest::Client,
25    api_key: String,
26}
27
28impl Client {
29    pub fn new(api_key: String) -> Self {
30        Self {
31            client: reqwest::Client::new(),
32            api_key,
33        }
34    }
35
36    pub async fn teams(self) -> Result<Vec<Team>, ReqwestJsonError> {
37        let teams = self.get("teams").send().await?;
38
39        Ok(teams)
40    }
41
42    pub async fn new_app(self, app_slug: &str, team_slug: &str) -> Result<bool, ReqwestJsonError> {
43        let CheckAvailabilityResponse { available } = self
44            .post("apps/new")
45            .json(&NewAppRequest {
46                app_slug: app_slug.to_string(),
47                team_slug: team_slug.to_string(),
48            })?
49            .send()
50            .await?;
51
52        Ok(available)
53    }
54
55    pub async fn new_team(self, team_slug: &str) -> Result<bool, ReqwestJsonError> {
56        let CheckAvailabilityResponse { available } = self
57            .post("teams/new")
58            .json(&NewTeamRequest {
59                team_slug: team_slug.to_string(),
60            })?
61            .send()
62            .await?;
63
64        Ok(available)
65    }
66
67    pub async fn set_team_name(
68        self,
69        team_slug: &str,
70        team_name: &str,
71    ) -> Result<(), ReqwestJsonError> {
72        let _: SuccessResponse = self
73            .post("teams/name")
74            .json(&SetTeamNameRequest {
75                team_slug: team_slug.to_string(),
76                team_name: team_name.to_string(),
77            })?
78            .send()
79            .await?;
80
81        Ok(())
82    }
83
84    pub async fn login(self) -> Result<LoginResponse, ReqwestJsonError> {
85        Ok(self.post("login").json(())?.send().await?)
86    }
87
88    pub async fn upload_file(
89        self,
90        app_slug: impl AsRef<str>,
91        path: impl AsRef<Path>,
92    ) -> Result<(), UploadFileError> {
93        let file = tokio::fs::File::open(path.as_ref()).await?;
94        let read_stream = FramedRead::new(file, BytesCodec::new());
95
96        let stream_part = Part::stream(Body::wrap_stream(read_stream))
97            .file_name(path.as_ref().to_string_lossy().to_string());
98        let form = Form::new().part("file", stream_part);
99
100        let _: SuccessResponse = self
101            .post("apps/upload-file")
102            .multipart(form)
103            .header(
104                AppMeta::name(),
105                AppMeta {
106                    app_slug: app_slug.as_ref().to_string(),
107                }
108                .to_string_value(),
109            )
110            .send()
111            .await?;
112
113        Ok(())
114    }
115
116    pub async fn upload_done(self, config: &CloudConfig) -> Result<(), ReqwestJsonError> {
117        let _: SuccessResponse = self.post("apps/upload-done").json(config)?.send().await?;
118
119        Ok(())
120    }
121
122    pub async fn log(self, name: &str) -> Result<String, ReqwestJsonError> {
123        let res: LogResponse = self
124            .post("log")
125            .json(&LogRequest {
126                name: name.to_string(),
127            })?
128            .send()
129            .await?;
130
131        Ok(res.log)
132    }
133
134    pub fn post(self, route: &str) -> ClientBuilder {
135        let url = Self::build_route(route);
136
137        ClientBuilder(self.client.post(url)).auth_header(&self.api_key)
138    }
139
140    pub fn get(self, route: &str) -> ClientBuilder {
141        let url = Self::build_route(route);
142
143        ClientBuilder(self.client.get(url)).auth_header(&self.api_key)
144    }
145
146    fn build_route(route: &str) -> String {
147        let base_url = std::env::var("OXYDE_CLOUD_API_URL")
148            .unwrap_or(BASE_URL.unwrap_or(DEFAULT_BASE_URL).to_string());
149        format!("{base_url}{route}")
150    }
151}
152
153pub struct ClientBuilder(reqwest::RequestBuilder);
154
155impl ClientBuilder {
156    pub fn auth_header(self, api_key: &str) -> Self {
157        Self(
158            self.0
159                .header("Authorization", format!("Bearer {}", api_key)),
160        )
161    }
162
163    pub fn body<T: Into<reqwest::Body>>(self, body: T) -> Self {
164        Self(self.0.body(body))
165    }
166
167    pub fn multipart(self, form: Form) -> Self {
168        Self(self.0.multipart(form))
169    }
170
171    pub fn json<Body: Serialize>(self, json: Body) -> Result<ClientBuilder, serde_json::Error> {
172        let json = serde_json::to_string(&json)?;
173
174        Ok(Self(
175            self.0.header("Content-Type", "application/json").body(json),
176        ))
177    }
178
179    pub fn header<K, V>(self, key: K, value: V) -> Self
180    where
181        HeaderName: TryFrom<K>,
182        <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
183        HeaderValue: TryFrom<V>,
184        <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
185    {
186        Self(self.0.header(key, value))
187    }
188
189    pub async fn send<Resp>(self) -> Result<Resp, reqwest::Error>
190    where
191        for<'de> Resp: Deserialize<'de>,
192    {
193        let res = self.0.send().await?;
194
195        match res.error_for_status_ref() {
196            Ok(_) => Ok(res.json::<Resp>().await?),
197            Err(err) => {
198                let err_text = res.text().await?;
199                error!("Received error:\n{err_text:#?}");
200
201                Err(err)
202            }
203        }
204    }
205}