waki 0.5.1

HTTP client and server library for WASI
Documentation
mod constants;
pub(crate) mod parser;

use crate::header::{HeaderMap, HeaderValue, IntoHeaderName, CONTENT_DISPOSITION, CONTENT_TYPE};

use anyhow::{Error, Result};
use mime::Mime;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use std::fs::File;
use std::io::Read;
use std::path::Path;

pub struct Form {
    parts: Vec<Part>,
    boundary: String,
}

impl Default for Form {
    fn default() -> Self {
        Self::new()
    }
}

impl Form {
    pub fn new() -> Self {
        Self {
            parts: vec![],
            boundary: format!("--FormBoundary{}", generate_random_string(10)),
        }
    }

    pub(crate) fn boundary(&self) -> &str {
        &self.boundary
    }

    pub fn text<S, V>(mut self, key: S, value: V) -> Self
    where
        S: Into<String>,
        V: Into<Vec<u8>>,
    {
        self.parts.push(Part::new(key, value));
        self
    }

    pub fn file<S, P>(mut self, key: S, path: P) -> Result<Self>
    where
        S: Into<String>,
        P: AsRef<Path>,
    {
        self.parts.push(Part::file(key, path)?);
        Ok(self)
    }

    pub fn part(mut self, part: Part) -> Self {
        self.parts.push(part);
        self
    }

    pub fn build(self) -> Vec<u8> {
        let mut buf = vec![];
        for part in self.parts {
            buf.extend_from_slice(
                format!(
                    "{}{}{}{}: form-data; name={}",
                    constants::BOUNDARY_EXT,
                    self.boundary,
                    constants::CRLF,
                    CONTENT_DISPOSITION,
                    part.key
                )
                .as_bytes(),
            );
            if let Some(filename) = part.filename {
                buf.extend_from_slice(format!("; filename=\"{}\"", filename).as_bytes());
            }
            if let Some(mime) = part.mime {
                buf.extend_from_slice(
                    format!("{}{}: {}", constants::CRLF, CONTENT_TYPE, mime).as_bytes(),
                );
            }
            for (k, v) in part.headers.iter() {
                buf.extend_from_slice(format!("{}{}: ", constants::CRLF, k).as_bytes());
                buf.extend_from_slice(v.as_bytes());
            }

            buf.extend_from_slice(constants::CRLF_CRLF.as_bytes());
            buf.extend_from_slice(&part.value);
            buf.extend_from_slice(constants::CRLF.as_bytes());
        }
        buf.extend_from_slice(
            format!(
                "{}{}{}",
                constants::BOUNDARY_EXT,
                self.boundary,
                constants::BOUNDARY_EXT,
            )
            .as_bytes(),
        );
        buf
    }
}

fn generate_random_string(length: usize) -> String {
    thread_rng()
        .sample_iter(&Alphanumeric)
        .take(length)
        .map(char::from)
        .collect()
}

pub struct Part {
    pub key: String,
    pub value: Vec<u8>,
    pub filename: Option<String>,
    pub mime: Option<Mime>,
    pub headers: HeaderMap,
}

impl Part {
    pub fn new<S, V>(key: S, value: V) -> Self
    where
        S: Into<String>,
        V: Into<Vec<u8>>,
    {
        Self {
            key: key.into(),
            value: value.into(),
            filename: None,
            mime: None,
            headers: HeaderMap::new(),
        }
    }

    pub fn file<S, P>(key: S, path: P) -> Result<Self>
    where
        S: Into<String>,
        P: AsRef<Path>,
    {
        let path = path.as_ref();
        let mime = mime_guess::from_path(path).first_or_octet_stream();
        let mut file = File::open(path)?;
        let mut buffer = vec![];
        file.read_to_end(&mut buffer)?;
        let part = Part::new(key, buffer).mime(mime);

        match path
            .file_name()
            .map(|filename| filename.to_string_lossy().to_string())
        {
            Some(name) => Ok(part.filename(name)),
            None => Ok(part),
        }
    }

    pub fn mime(mut self, mime: Mime) -> Self {
        self.mime = Some(mime);
        self
    }

    pub fn mime_str(mut self, mime: &str) -> Result<Self> {
        self.mime = Some(mime.parse()?);
        Ok(self)
    }

    pub fn filename<S: Into<String>>(mut self, name: S) -> Self {
        self.filename = Some(name.into());
        self
    }

    pub fn headers<K, V, I>(mut self, headers: I) -> Result<Self>
    where
        K: IntoHeaderName,
        V: TryInto<HeaderValue>,
        <V as TryInto<HeaderValue>>::Error: Into<Error>,
        I: IntoIterator<Item = (K, V)>,
    {
        for (key, value) in headers.into_iter() {
            self.headers
                .insert(key, value.try_into().map_err(|e| e.into())?);
        }
        Ok(self)
    }
}