acts-package-http 0.17.2

acts package for http request
Documentation
use acts::{
    ActError, ActPackage, ActPackageCatalog, ActPackageMeta, ActRunAs, Result, Vars, include_json,
};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, InvalidHeaderValue};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;

const DATA_KEY: &str = "data";

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub enum ContentType {
    #[serde(rename(deserialize = "none"))]
    None,
    #[serde(rename(deserialize = "text"))]
    Text,
    #[serde(rename(deserialize = "html"))]
    Html,
    #[default]
    #[serde(rename(deserialize = "json"))]
    Json,
    #[serde(rename(deserialize = "urlencoded"))]
    UrlEncoded,
    #[serde(rename(deserialize = "form-data"))]
    FormData,
    #[serde(rename(deserialize = "binary"))]
    Binary,
    #[serde(rename(deserialize = "image"))]
    Image,
    #[serde(rename(deserialize = "video"))]
    Video,
    #[serde(rename(deserialize = "audio"))]
    Audio,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Pair {
    pub key: String,
    pub value: JsonValue,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HttpPackage {
    pub url: String,
    pub method: String,
    #[serde(default)]
    #[serde(rename(deserialize = "content-type"))]
    pub content_type: ContentType,
    #[serde(default)]
    pub headers: Vec<Pair>,
    #[serde(default)]
    pub params: Vec<Pair>,
    pub body: Option<JsonValue>,
}

impl ActPackage for HttpPackage {
    fn meta() -> ActPackageMeta {
        ActPackageMeta {
            name: "acts.core.http",
            desc: "do a http request",
            version: "0.1.0",
            icon: "icon-http",
            doc: "",
            schema: include_json!("./schema.json"),
            run_as: ActRunAs::Irq,
            resources: vec![],
            catalog: ActPackageCatalog::Core,
        }
    }
}

impl HttpPackage {
    pub fn create(inputs: &Vars) -> Result<Self> {
        let params = inputs
            .get::<serde_json::Value>("params")
            .ok_or(ActError::Package("missing 'params' in package".to_string()))?;

        let package = serde_json::from_value::<Self>(params)?;
        Ok(package)
    }

    pub async fn run(&self) -> Result<Vars> {
        let mut ret = Vars::new();
        let mut headers = HeaderMap::new();
        headers.insert(
            HeaderName::from_static("accept"),
            HeaderValue::from_static("*/*"),
        );

        for Pair { key, value } in &self.headers {
            headers.insert(
                key.parse::<HeaderName>()
                    .map_err(|err| ActError::Runtime(err.to_string()))?,
                value
                    .to_string()
                    .parse()
                    .map_err(|err: InvalidHeaderValue| ActError::Runtime(err.to_string()))?,
            );
        }
        let mut query = Vec::new();
        for Pair { key, value } in &self.params {
            query.push((key.clone(), value.clone()));
        }

        let c = reqwest::Client::new();
        let mut request = c
            .request(
                self.method
                    .parse()
                    .map_err(|_| ActError::Runtime(format!("invalid method '{}'", self.method)))?,
                &self.url,
            )
            .headers(headers)
            .query(&query);

        match self.content_type {
            ContentType::Text | ContentType::Html => {
                if let Some(text) = &self.body {
                    let data = text.as_str().ok_or(ActError::Package(
                        "content-type did not match the body content".to_string(),
                    ))?;
                    request = request.body::<String>(data.to_string());
                }
            }
            ContentType::Json => {
                if let Some(json) = &self.body {
                    let body = serde_json::to_vec(json)?;
                    request = request.body(body);
                }
            }
            ContentType::FormData | ContentType::UrlEncoded => {
                if let Some(form) = &self.body {
                    let data = form.as_object().ok_or(ActError::Package(
                        "content-type did not match the body content".to_string(),
                    ))?;
                    request = request.form(data);
                }
            }
            ContentType::Binary | ContentType::Image | ContentType::Video | ContentType::Audio => {
                if let Some(value) = &self.body {
                    let data = value.as_str().ok_or(ActError::Package(
                        "content-type did not match the body content".to_string(),
                    ))?;
                    let data = STANDARD
                        .decode(data)
                        .map_err(|err| ActError::Package(err.to_string()))?;
                    request = request.body(data);
                }
            }
            _ => {}
        }

        let res = request
            .send()
            .await
            .map_err(|err| ActError::Runtime(format!("Http error: {}", err)))?;

        let default_type = HeaderValue::from_static("application/json");
        let response_type = res
            .headers()
            .get(CONTENT_TYPE)
            .unwrap_or(&default_type)
            .to_str()
            .map_err(|err| ActError::Package(err.to_string()))?;
        let status = res.status();
        let response_type = get_content_type(response_type);
        match response_type {
            ContentType::Text | ContentType::Html => {
                ret.insert(
                    DATA_KEY.to_string(),
                    res.text().await.map_err(map_package_err)?.into(),
                );
            }
            ContentType::Json => {
                ret.insert(
                    DATA_KEY.to_string(),
                    res.json::<serde_json::Value>()
                        .await
                        .map_err(map_package_err)?,
                );
            }
            ContentType::Binary | ContentType::Image | ContentType::Video | ContentType::Audio => {
                let data = res.bytes().await.map_err(map_package_err)?.to_vec();
                let data = STANDARD.encode(&data);
                ret.insert(DATA_KEY.to_string(), data.into());
            }
            _ => {}
        }
        if !status.is_success() {
            return Err(ActError::Exception {
                ecode: status.as_u16().to_string(),
                message: ret.get(DATA_KEY).unwrap_or(status.to_string()),
            });
        }

        Ok(ret)
    }
}

fn map_package_err(err: reqwest::Error) -> ActError {
    ActError::Package(err.to_string())
}

fn get_content_type(mime_type: &str) -> ContentType {
    let mut ret = ContentType::None;
    if mime_type.starts_with("application/json") {
        ret = ContentType::Json;
    } else if mime_type.starts_with("text/html") {
        ret = ContentType::Html;
    } else if mime_type.starts_with("application/x-www-form-urlencoded") {
        ret = ContentType::UrlEncoded;
    } else if mime_type.starts_with("multipart/form-data") {
        ret = ContentType::FormData;
    } else if mime_type.starts_with("image/") {
        ret = ContentType::Image;
    } else if mime_type.starts_with("audio/") {
        ret = ContentType::Audio;
    } else if mime_type.starts_with("video/") {
        ret = ContentType::Video;
    } else if mime_type.starts_with("text/") || mime_type.starts_with("application/javascript") {
        ret = ContentType::Text;
    }

    ret
}