1use std::{
6 fmt::{self, Debug, Display, Formatter},
7 str::FromStr,
8};
9
10use chrono::{DateTime, Utc};
11use log::{Level, debug, log_enabled};
12use oauth10a::client::{
13 ClientError, Request, RestClient,
14 reqwest::{
15 self, Body, Method,
16 header::{CONTENT_LENGTH, CONTENT_TYPE, HeaderValue},
17 },
18 url,
19};
20use serde::{Deserialize, Serialize};
21
22use crate::Client;
23
24pub const MIME_APPLICATION_WASM: &str = "application/wasm";
28
29#[derive(thiserror::Error, Debug)]
33pub enum Error {
34 #[error(
35 "failed to parse the webassembly platform '{0}', available values are 'rust', 'javascript' ('js'), 'tiny_go' ('go') and 'assemblyscript'"
36 )]
37 ParsePlatform(String),
38 #[error(
39 "failed to parse the status '{0}', available values are 'waiting_for_upload', 'deploying', 'packaging', 'ready' and 'error'"
40 )]
41 ParseStatus(String),
42 #[error("failed to parse endpoint '{0}', {1}")]
43 ParseUrl(String, url::ParseError),
44 #[error("failed to list deployments for function '{0}' of organisation '{1}', {2}")]
45 List(String, String, ClientError),
46 #[error("failed to create deployment for function '{0}' on organisation '{1}', {2}")]
47 Create(String, String, ClientError),
48 #[error("failed to get deployment '{0}' of function '{1}' on organisation '{2}', {3}")]
49 Get(String, String, String, ClientError),
50 #[error("failed to trigger deployment '{0}' of function '{1}' on organisation '{2}', {3}")]
51 Trigger(String, String, String, ClientError),
52 #[error("failed to delete deployment '{0}' of function '{1}' on organisation '{2}', {3}")]
53 Delete(String, String, String, ClientError),
54 #[error("failed to create request, {0}")]
55 Request(reqwest::Error),
56 #[error("failed to execute request, {0}")]
57 Execute(ClientError),
58 #[error("failed to execute request, got status code {0}")]
59 StatusCode(u16),
60}
61
62#[derive(Serialize, Deserialize, Hash, Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
66pub enum Platform {
67 #[serde(rename = "RUST")]
68 Rust,
69 #[serde(rename = "ASSEMBLY_SCRIPT")]
70 AssemblyScript,
71 #[serde(rename = "TINY_GO")]
72 TinyGo,
73 #[serde(rename = "JAVA_SCRIPT")]
74 JavaScript,
75}
76
77impl Display for Platform {
78 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
79 match self {
80 Self::Rust => write!(f, "RUST"),
81 Self::AssemblyScript => write!(f, "ASSEMBLY_SCRIPT"),
82 Self::JavaScript => write!(f, "JAVA_SCRIPT"),
83 Self::TinyGo => write!(f, "TINY_GO"),
84 }
85 }
86}
87
88impl FromStr for Platform {
89 type Err = Error;
90
91 fn from_str(s: &str) -> Result<Self, Self::Err> {
92 match s.to_lowercase().trim().replace('_', "").as_str() {
93 "rust" => Ok(Self::Rust),
94 "javascript" | "js" => Ok(Self::JavaScript),
95 "tinygo" | "go" => Ok(Self::TinyGo),
96 "assemblyscript" => Ok(Self::AssemblyScript),
97 _ => Err(Error::ParsePlatform(s.to_string())),
98 }
99 }
100}
101
102#[derive(Serialize, Deserialize, Hash, Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
106pub enum Status {
107 #[serde(rename = "WAITING_FOR_UPLOAD")]
108 WaitingForUpload,
109 #[serde(rename = "PACKAGING")]
110 Packaging,
111 #[serde(rename = "DEPLOYING")]
112 Deploying,
113 #[serde(rename = "READY")]
114 Ready,
115 #[serde(rename = "ERROR")]
116 Error,
117}
118
119impl Display for Status {
120 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
121 match self {
122 Self::WaitingForUpload => write!(f, "WAITING_FOR_UPLOAD"),
123 Self::Packaging => write!(f, "PACKAGING"),
124 Self::Deploying => write!(f, "DEPLOYING"),
125 Self::Ready => write!(f, "READY"),
126 Self::Error => write!(f, "ERROR"),
127 }
128 }
129}
130
131impl FromStr for Status {
132 type Err = Error;
133
134 fn from_str(s: &str) -> Result<Self, Self::Err> {
135 match s.to_lowercase().trim().replace('_', "").as_str() {
136 "waitingforupload" => Ok(Self::WaitingForUpload),
137 "packaging" => Ok(Self::Packaging),
138 "deploying" => Ok(Self::Deploying),
139 "ready" => Ok(Self::Ready),
140 "error" => Ok(Self::Error),
141 _ => Err(Error::ParseStatus(s.to_string())),
142 }
143 }
144}
145
146#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
150pub struct Opts {
151 #[serde(rename = "name")]
152 pub name: Option<String>,
153 #[serde(rename = "description")]
154 pub description: Option<String>,
155 #[serde(rename = "tag")]
156 pub tag: Option<String>,
157 #[serde(rename = "platform")]
158 pub platform: Platform,
159}
160
161#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
165pub struct DeploymentCreation {
166 #[serde(rename = "id")]
167 pub id: String,
168 #[serde(rename = "functionId")]
169 pub function_id: String,
170 #[serde(rename = "name")]
171 pub name: Option<String>,
172 #[serde(rename = "description")]
173 pub description: Option<String>,
174 #[serde(rename = "tag")]
175 pub tag: Option<String>,
176 #[serde(rename = "platform")]
177 pub platform: Platform,
178 #[serde(rename = "status")]
179 pub status: Status,
180 #[serde(rename = "errorReason")]
181 pub reason: Option<String>,
182 #[serde(rename = "uploadUrl")]
183 pub upload_url: String,
184 #[serde(rename = "createdAt")]
185 pub created_at: DateTime<Utc>,
186 #[serde(rename = "updatedAt")]
187 pub updated_at: DateTime<Utc>,
188}
189
190#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
194pub struct Deployment {
195 #[serde(rename = "id")]
196 pub id: String,
197 #[serde(rename = "functionId")]
198 pub function_id: String,
199 #[serde(rename = "name")]
200 pub name: Option<String>,
201 #[serde(rename = "description")]
202 pub description: Option<String>,
203 #[serde(rename = "tag")]
204 pub tag: Option<String>,
205 #[serde(rename = "platform")]
206 pub platform: Platform,
207 #[serde(rename = "status")]
208 pub status: Status,
209 #[serde(rename = "errorReason")]
210 pub reason: Option<String>,
211 #[serde(rename = "url")]
212 pub url: Option<String>,
213 #[serde(rename = "createdAt")]
214 pub created_at: DateTime<Utc>,
215 #[serde(rename = "updatedAt")]
216 pub updated_at: DateTime<Utc>,
217}
218
219#[cfg_attr(feature = "tracing", tracing::instrument)]
223pub async fn list(
225 client: &Client,
226 organisation_id: &str,
227 function_id: &str,
228) -> Result<Vec<Deployment>, Error> {
229 let path = format!(
230 "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments",
231 client.endpoint
232 );
233
234 #[cfg(feature = "logging")]
235 if log_enabled!(Level::Debug) {
236 debug!(
237 "execute a request to list deployments for functions, path: '{path}', organisation: '{organisation_id}', function_id: '{function_id}'"
238 );
239 }
240
241 client
242 .get(&path)
243 .await
244 .map_err(|err| Error::List(function_id.to_string(), organisation_id.to_string(), err))
245}
246
247#[cfg_attr(feature = "tracing", tracing::instrument)]
248pub async fn create(
250 client: &Client,
251 organisation_id: &str,
252 function_id: &str,
253 opts: &Opts,
254) -> Result<DeploymentCreation, Error> {
255 let path = format!(
256 "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments",
257 client.endpoint
258 );
259
260 #[cfg(feature = "logging")]
261 if log_enabled!(Level::Debug) {
262 debug!(
263 "execute a request to create deployment, path: '{path}', organisation: {organisation_id}, function_id: '{function_id}'"
264 );
265 }
266
267 client
268 .post(&path, opts)
269 .await
270 .map_err(|err| Error::Create(function_id.to_string(), organisation_id.to_string(), err))
271}
272
273#[cfg_attr(feature = "tracing", tracing::instrument)]
274pub async fn get(
276 client: &Client,
277 organisation_id: &str,
278 function_id: &str,
279 deployment_id: &str,
280) -> Result<Deployment, Error> {
281 let path = format!(
282 "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}",
283 client.endpoint
284 );
285
286 #[cfg(feature = "logging")]
287 if log_enabled!(Level::Debug) {
288 debug!(
289 "execute a request to get deployment, path: '{path}', organisation: {organisation_id}, function: {function_id}, deployment: {deployment_id}"
290 );
291 }
292
293 client.get(&path).await.map_err(|err| {
294 Error::Get(
295 deployment_id.to_string(),
296 function_id.to_string(),
297 organisation_id.to_string(),
298 err,
299 )
300 })
301}
302
303#[cfg_attr(feature = "tracing", tracing::instrument)]
304pub async fn trigger(
306 client: &Client,
307 organisation_id: &str,
308 function_id: &str,
309 deployment_id: &str,
310) -> Result<(), Error> {
311 let path = format!(
312 "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}/trigger",
313 client.endpoint
314 );
315
316 #[cfg(feature = "logging")]
317 if log_enabled!(Level::Debug) {
318 debug!(
319 "execute a request to get deployment, path: '{path}', organisation: {organisation_id}, function: {function_id}, deployment: {deployment_id}"
320 );
321 }
322
323 let req = reqwest::Request::new(
324 Method::POST,
325 path.parse().map_err(|err| Error::ParseUrl(path, err))?,
326 );
327
328 let res = client.execute(req).await.map_err(Error::Execute)?;
329 let status = res.status();
330 if !status.is_success() {
331 return Err(Error::StatusCode(status.as_u16()));
332 }
333
334 Ok(())
335}
336
337#[cfg_attr(feature = "tracing", tracing::instrument)]
338pub async fn upload(client: &Client, endpoint: &str, buf: Vec<u8>) -> Result<(), Error> {
340 let mut req = reqwest::Request::new(
341 Method::PUT,
342 endpoint
343 .parse()
344 .map_err(|err| Error::ParseUrl(endpoint.to_string(), err))?,
345 );
346
347 req.headers_mut().insert(
348 CONTENT_TYPE,
349 HeaderValue::from_static(MIME_APPLICATION_WASM),
350 );
351 req.headers_mut()
352 .insert(CONTENT_LENGTH, HeaderValue::from(buf.len()));
353 *req.body_mut() = Some(Body::from(buf));
354
355 #[cfg(feature = "logging")]
356 if log_enabled!(Level::Debug) {
357 debug!("execute a request to upload webassembly, endpoint: '{endpoint}'");
358 }
359
360 let res = client
361 .inner()
362 .execute(req)
363 .await
364 .map_err(|err| Error::Execute(ClientError::Request(err)))?;
365
366 let status = res.status();
367 if !status.is_success() {
368 return Err(Error::StatusCode(status.as_u16()));
369 }
370
371 Ok(())
372}
373
374#[cfg_attr(feature = "tracing", tracing::instrument)]
375pub async fn delete(
377 client: &Client,
378 organisation_id: &str,
379 function_id: &str,
380 deployment_id: &str,
381) -> Result<(), Error> {
382 let path = format!(
383 "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}",
384 client.endpoint
385 );
386
387 #[cfg(feature = "logging")]
388 if log_enabled!(Level::Debug) {
389 debug!(
390 "execute a request to delete deployment, path: '{path}', organisation: {organisation_id}, function: {function_id}, deployment: {deployment_id}"
391 );
392 }
393
394 client.delete(&path).await.map_err(|err| {
395 Error::Delete(
396 deployment_id.to_string(),
397 function_id.to_string(),
398 organisation_id.to_string(),
399 err,
400 )
401 })
402}