ark-api 0.17.0-pre.15

Ark API
Documentation
//! # 📡 HTTP Request API
//!
//! API to be able to issue HTTP GET, POST, PUT, DELETE, PATCH requests.
//!
//! This is useful and powerful to be able to access and communicate with local or internet services.
//!
//! ## Security note
//!
//! Currently there are no limitations on what domains and services can be accessed.
//! But this will change later as more of the Ark capability-based security gets put into place
//! and where modules have to opt-in and request access to minimal possible specific domains and URLs.
//!
//! To prepare for this, avoid connecting to arbitrary URLs or allow user input of what to connect to when possible.
//! Prefer domains and URLs the module can know statically at compile time, as it may need to declare them in its
//! manifest later to get access to it.
//!
//! ## Example usage
//!
//! ```rust,no_run
//! require_http_request_api!();
//!
//! // Get Google's frontpage
//! let html_bytes = http_get(Url::Static("https://google.com")).unwrap();
//! ```

use crate::{ffi::http_request_v1 as ffi, Error, ErrorCode};
use std::{
    borrow::Cow,
    future::Future,
    pin::Pin,
    string::FromUtf8Error,
    task::{Context, Poll},
};

#[doc(hidden)]
pub use ffi::API as FFI_API;

pub use ffi::Method;

/// URL for HTTP requests
///
/// This is intentionally quite restricted to use static domains and protocols for security and simplicity purposes
#[derive(Clone, Debug, PartialEq)]
pub enum Url {
    /// Fully static full URL
    ///
    /// This is required to start with `https://` protocol
    Static(&'static str),

    /// URL with static domain and static or dynamic path
    StaticDomain {
        /// Static domain of the URL
        ///
        /// Example: `ark.embark.dev`
        domain: &'static str,

        /// Optional dynamic URL path under the domain.
        ///
        /// Example: `/guide/getting-started/system-reqs.html`
        ///
        /// Note: This must start with `/`
        path: Cow<'static, str>,
    },
}

impl Url {
    /// Create a new URL from static string
    ///
    /// This is required to start with `https://` protocol, but this is currently not validated at compile-time
    ///
    /// # Example
    ///
    /// ```
    /// const TARGET: Url = Url::const_from_str("https://google.com/search?q=rust")
    /// ```
    pub const fn const_from_str(url: &'static str) -> Self {
        // TODO: Add compile-time validation that it is an https:// URL, failing earlier is better than later
        Self::Static(url)
    }

    /// Create a new URL from a static domain and path
    ///
    /// # Example
    ///
    /// ```
    /// const TARGET: Url = Url::const_with_path("google.com", "/search?q=rust")
    /// ```
    pub const fn const_with_path(domain: &'static str, path: &'static str) -> Self {
        Self::StaticDomain {
            domain,
            path: Cow::Borrowed(path),
        }
    }

    /// Create a new URL with an existing one as base and append the new path in the end
    pub fn join_path(&self, extra_path: &str) -> Self {
        match self {
            Self::StaticDomain { domain, path } => Self::StaticDomain {
                domain,
                path: format!("{path}{extra_path}").into(),
            },
            Self::Static(url) => {
                let url = url
                    .strip_prefix("https://")
                    .expect("invalid protocol in URL");
                let (domain, path) = url.split_once('/').expect("domain/path split not found");

                Self::StaticDomain {
                    domain,
                    path: format!("{path}{extra_path}").into(),
                }
            }
        }
    }
}
impl std::fmt::Display for Url {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Static(url) => write!(f, "{url}"),
            Self::StaticDomain { domain, path } => write!(f, "https://{domain}{path}"),
        }
    }
}

/// HTTP response object sent back from a server.
pub struct Response {
    /// HTTP status code.
    ///
    /// If it's different from the 200 range (from 200 to 299), indicates a
    /// failure that needs to be handled.
    pub status_code: u16,

    bytes: Vec<u8>,
}

impl Response {
    /// Consumes the `Response` object into the bytes it contains.
    pub fn into_bytes(self) -> Vec<u8> {
        self.bytes
    }

    /// Bytes sent back from the server.
    pub fn as_bytes(&self) -> &[u8] {
        &self.bytes
    }

    /// Bytes sent back from the server, interpreted as a string. Can fail if the bytes don't
    /// represent a valid utf8 encoding.
    pub fn as_text(&self) -> Result<String, FromUtf8Error> {
        String::from_utf8(self.bytes.clone())
    }

    /// Bytes sent back from the server, interpreted as a string. Will interpret invalid utf8
    /// encodings loosely, so as to return something.
    pub fn as_text_lossy(&self) -> Cow<'_, str> {
        String::from_utf8_lossy(&self.bytes)
    }
}

struct RequestFuture(Result<ffi::RequestHandle, ErrorCode>);

/// The response to any http request served by this API.
pub type ResponseResult = Result<Response, Error>;

impl Future for RequestFuture {
    type Output = ResponseResult;
    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        match self.0 {
            Ok(handle) => {
                let mut status: u32 = 0;
                if let Err(error_code) = ffi::is_ready(handle, &mut status) {
                    return Poll::Ready(Err(Error::from(error_code)));
                }
                if status != ffi::STATUS_PENDING {
                    match ffi::retrieve(handle) {
                        Ok(bytes) => {
                            assert!(u16::try_from(status).is_ok());
                            let response = Response {
                                bytes,
                                status_code: status as u16,
                            };
                            Poll::Ready(Ok(response))
                        }
                        Err(error_code) => Poll::Ready(Err(Error::from(error_code))),
                    }
                } else {
                    Poll::Pending
                }
            }
            Err(error_code) => Poll::Ready(Err(Error::from(error_code))),
        }
    }
}

/// Issue async HTTP request of the specific method
///
/// It is generally preferred to use the more explicit short-hand functions such as `http_get`, `http_post` instead.
///
/// # Returns
///
/// On badly-formed URL or
/// On 404 error this will return `Error::NotFound`
pub fn http_request(
    method: Method,
    url: &Url,
    body: &[u8],
) -> impl Future<Output = ResponseResult> {
    RequestFuture(ffi::request(method, &url.to_string(), body))
}

/// Issue async HTTP GET request
pub fn http_get(url: &Url) -> impl Future<Output = ResponseResult> {
    RequestFuture(ffi::request(ffi::Method::Get, &url.to_string(), &[]))
}

/// Issue async HTTP POST request
pub fn http_post(url: &Url, body: &[u8]) -> impl Future<Output = ResponseResult> {
    RequestFuture(ffi::request(ffi::Method::Post, &url.to_string(), body))
}

/// Issue async HTTP PUT request
pub fn http_put(url: &Url, body: &[u8]) -> impl Future<Output = ResponseResult> {
    RequestFuture(ffi::request(ffi::Method::Put, &url.to_string(), body))
}

/// Issue async HTTP DELETE request
pub fn http_delete(url: &Url) -> impl Future<Output = ResponseResult> {
    RequestFuture(ffi::request(ffi::Method::Delete, &url.to_string(), &[]))
}

/// Issue async HTTP PATCH request
pub fn http_patch(url: &Url, body: &[u8]) -> impl Future<Output = ResponseResult> {
    RequestFuture(ffi::request(ffi::Method::Patch, &url.to_string(), body))
}

/// Issue async HTTP HEAD request
pub fn http_head(url: &Url, body: &[u8]) -> impl Future<Output = ResponseResult> {
    RequestFuture(ffi::request(ffi::Method::Head, &url.to_string(), body))
}

/// Issue async HTTP OPTIONS request
pub fn http_options(url: &Url, body: &[u8]) -> impl Future<Output = ResponseResult> {
    RequestFuture(ffi::request(ffi::Method::Options, &url.to_string(), body))
}

/// Issue async HTTP TRACE request
pub fn http_trace(url: &Url, body: &[u8]) -> impl Future<Output = ResponseResult> {
    RequestFuture(ffi::request(ffi::Method::Trace, &url.to_string(), body))
}

/// Issue async HTTP CONNECT request
pub fn http_connect(url: &Url, body: &[u8]) -> impl Future<Output = ResponseResult> {
    RequestFuture(ffi::request(ffi::Method::Connect, &url.to_string(), body))
}