use std::{
fmt::{self, Debug, Display, Formatter},
str::FromStr,
};
use chrono::{DateTime, Utc};
use log::{Level, debug, log_enabled};
use oauth10a::client::{
ClientError, Request, RestClient,
reqwest::{
self, Body, Method,
header::{CONTENT_LENGTH, CONTENT_TYPE, HeaderValue},
},
url,
};
use serde::{Deserialize, Serialize};
use crate::Client;
pub const MIME_APPLICATION_WASM: &str = "application/wasm";
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(
"failed to parse the webassembly platform '{0}', available values are 'rust', 'javascript' ('js'), 'tiny_go' ('go') and 'assemblyscript'"
)]
ParsePlatform(String),
#[error(
"failed to parse the status '{0}', available values are 'waiting_for_upload', 'deploying', 'packaging', 'ready' and 'error'"
)]
ParseStatus(String),
#[error("failed to parse endpoint '{0}', {1}")]
ParseUrl(String, url::ParseError),
#[error("failed to list deployments for function '{0}' of organisation '{1}', {2}")]
List(String, String, ClientError),
#[error("failed to create deployment for function '{0}' on organisation '{1}', {2}")]
Create(String, String, ClientError),
#[error("failed to get deployment '{0}' of function '{1}' on organisation '{2}', {3}")]
Get(String, String, String, ClientError),
#[error("failed to trigger deployment '{0}' of function '{1}' on organisation '{2}', {3}")]
Trigger(String, String, String, ClientError),
#[error("failed to delete deployment '{0}' of function '{1}' on organisation '{2}', {3}")]
Delete(String, String, String, ClientError),
#[error("failed to create request, {0}")]
Request(reqwest::Error),
#[error("failed to execute request, {0}")]
Execute(ClientError),
#[error("failed to execute request, got status code {0}")]
StatusCode(u16),
}
#[derive(Serialize, Deserialize, Hash, Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
pub enum Platform {
#[serde(rename = "RUST")]
Rust,
#[serde(rename = "ASSEMBLY_SCRIPT")]
AssemblyScript,
#[serde(rename = "TINY_GO")]
TinyGo,
#[serde(rename = "JAVA_SCRIPT")]
JavaScript,
}
impl Display for Platform {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
match self {
Self::Rust => write!(f, "RUST"),
Self::AssemblyScript => write!(f, "ASSEMBLY_SCRIPT"),
Self::JavaScript => write!(f, "JAVA_SCRIPT"),
Self::TinyGo => write!(f, "TINY_GO"),
}
}
}
impl FromStr for Platform {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().trim().replace('_', "").as_str() {
"rust" => Ok(Self::Rust),
"javascript" | "js" => Ok(Self::JavaScript),
"tinygo" | "go" => Ok(Self::TinyGo),
"assemblyscript" => Ok(Self::AssemblyScript),
_ => Err(Error::ParsePlatform(s.to_string())),
}
}
}
#[derive(Serialize, Deserialize, Hash, Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
pub enum Status {
#[serde(rename = "WAITING_FOR_UPLOAD")]
WaitingForUpload,
#[serde(rename = "PACKAGING")]
Packaging,
#[serde(rename = "DEPLOYING")]
Deploying,
#[serde(rename = "READY")]
Ready,
#[serde(rename = "ERROR")]
Error,
}
impl Display for Status {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
match self {
Self::WaitingForUpload => write!(f, "WAITING_FOR_UPLOAD"),
Self::Packaging => write!(f, "PACKAGING"),
Self::Deploying => write!(f, "DEPLOYING"),
Self::Ready => write!(f, "READY"),
Self::Error => write!(f, "ERROR"),
}
}
}
impl FromStr for Status {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().trim().replace('_', "").as_str() {
"waitingforupload" => Ok(Self::WaitingForUpload),
"packaging" => Ok(Self::Packaging),
"deploying" => Ok(Self::Deploying),
"ready" => Ok(Self::Ready),
"error" => Ok(Self::Error),
_ => Err(Error::ParseStatus(s.to_string())),
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
pub struct Opts {
#[serde(rename = "name")]
pub name: Option<String>,
#[serde(rename = "description")]
pub description: Option<String>,
#[serde(rename = "tag")]
pub tag: Option<String>,
#[serde(rename = "platform")]
pub platform: Platform,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
pub struct DeploymentCreation {
#[serde(rename = "id")]
pub id: String,
#[serde(rename = "functionId")]
pub function_id: String,
#[serde(rename = "name")]
pub name: Option<String>,
#[serde(rename = "description")]
pub description: Option<String>,
#[serde(rename = "tag")]
pub tag: Option<String>,
#[serde(rename = "platform")]
pub platform: Platform,
#[serde(rename = "status")]
pub status: Status,
#[serde(rename = "errorReason")]
pub reason: Option<String>,
#[serde(rename = "uploadUrl")]
pub upload_url: String,
#[serde(rename = "createdAt")]
pub created_at: DateTime<Utc>,
#[serde(rename = "updatedAt")]
pub updated_at: DateTime<Utc>,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
pub struct Deployment {
#[serde(rename = "id")]
pub id: String,
#[serde(rename = "functionId")]
pub function_id: String,
#[serde(rename = "name")]
pub name: Option<String>,
#[serde(rename = "description")]
pub description: Option<String>,
#[serde(rename = "tag")]
pub tag: Option<String>,
#[serde(rename = "platform")]
pub platform: Platform,
#[serde(rename = "status")]
pub status: Status,
#[serde(rename = "errorReason")]
pub reason: Option<String>,
#[serde(rename = "url")]
pub url: Option<String>,
#[serde(rename = "createdAt")]
pub created_at: DateTime<Utc>,
#[serde(rename = "updatedAt")]
pub updated_at: DateTime<Utc>,
}
#[cfg_attr(feature = "tracing", tracing::instrument)]
pub async fn list(
client: &Client,
organisation_id: &str,
function_id: &str,
) -> Result<Vec<Deployment>, Error> {
let path = format!(
"{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments",
client.endpoint
);
#[cfg(feature = "logging")]
if log_enabled!(Level::Debug) {
debug!(
"execute a request to list deployments for functions, path: '{path}', organisation: '{organisation_id}', function_id: '{function_id}'"
);
}
client
.get(&path)
.await
.map_err(|err| Error::List(function_id.to_string(), organisation_id.to_string(), err))
}
#[cfg_attr(feature = "tracing", tracing::instrument)]
pub async fn create(
client: &Client,
organisation_id: &str,
function_id: &str,
opts: &Opts,
) -> Result<DeploymentCreation, Error> {
let path = format!(
"{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments",
client.endpoint
);
#[cfg(feature = "logging")]
if log_enabled!(Level::Debug) {
debug!(
"execute a request to create deployment, path: '{path}', organisation: {organisation_id}, function_id: '{function_id}'"
);
}
client
.post(&path, opts)
.await
.map_err(|err| Error::Create(function_id.to_string(), organisation_id.to_string(), err))
}
#[cfg_attr(feature = "tracing", tracing::instrument)]
pub async fn get(
client: &Client,
organisation_id: &str,
function_id: &str,
deployment_id: &str,
) -> Result<Deployment, Error> {
let path = format!(
"{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}",
client.endpoint
);
#[cfg(feature = "logging")]
if log_enabled!(Level::Debug) {
debug!(
"execute a request to get deployment, path: '{path}', organisation: {organisation_id}, function: {function_id}, deployment: {deployment_id}"
);
}
client.get(&path).await.map_err(|err| {
Error::Get(
deployment_id.to_string(),
function_id.to_string(),
organisation_id.to_string(),
err,
)
})
}
#[cfg_attr(feature = "tracing", tracing::instrument)]
pub async fn trigger(
client: &Client,
organisation_id: &str,
function_id: &str,
deployment_id: &str,
) -> Result<(), Error> {
let path = format!(
"{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}/trigger",
client.endpoint
);
#[cfg(feature = "logging")]
if log_enabled!(Level::Debug) {
debug!(
"execute a request to get deployment, path: '{path}', organisation: {organisation_id}, function: {function_id}, deployment: {deployment_id}"
);
}
let req = reqwest::Request::new(
Method::POST,
path.parse().map_err(|err| Error::ParseUrl(path, err))?,
);
let res = client.execute(req).await.map_err(Error::Execute)?;
let status = res.status();
if !status.is_success() {
return Err(Error::StatusCode(status.as_u16()));
}
Ok(())
}
#[cfg_attr(feature = "tracing", tracing::instrument)]
pub async fn upload(client: &Client, endpoint: &str, buf: Vec<u8>) -> Result<(), Error> {
let mut req = reqwest::Request::new(
Method::PUT,
endpoint
.parse()
.map_err(|err| Error::ParseUrl(endpoint.to_string(), err))?,
);
req.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static(MIME_APPLICATION_WASM),
);
req.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from(buf.len()));
*req.body_mut() = Some(Body::from(buf));
#[cfg(feature = "logging")]
if log_enabled!(Level::Debug) {
debug!("execute a request to upload webassembly, endpoint: '{endpoint}'");
}
let res = client
.inner()
.execute(req)
.await
.map_err(|err| Error::Execute(ClientError::Request(err)))?;
let status = res.status();
if !status.is_success() {
return Err(Error::StatusCode(status.as_u16()));
}
Ok(())
}
#[cfg_attr(feature = "tracing", tracing::instrument)]
pub async fn delete(
client: &Client,
organisation_id: &str,
function_id: &str,
deployment_id: &str,
) -> Result<(), Error> {
let path = format!(
"{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}",
client.endpoint
);
#[cfg(feature = "logging")]
if log_enabled!(Level::Debug) {
debug!(
"execute a request to delete deployment, path: '{path}', organisation: {organisation_id}, function: {function_id}, deployment: {deployment_id}"
);
}
client.delete(&path).await.map_err(|err| {
Error::Delete(
deployment_id.to_string(),
function_id.to_string(),
organisation_id.to_string(),
err,
)
})
}