web-static-pack 0.4.3

Embed static resources (GUI, assets, images, styles, html) within executable. Serve with hyper or any server of your choice.
Documentation
//! Hyper integration.
//! See examples/docs/main.rs for usage sample.
//!
//! Entry point for this module is `Responder`.
//! Create `Responder` providing reference to `Loader`.
//! Use `request_respond()` method to serve file in response to request.

use super::loader::Loader;
use http::{header, HeaderMap, Method, StatusCode, Uri};
use hyper::{Body, Request, Response};

/// Possible errors during `Responder` handling
pub enum ResponderError {
    /// Not supported HTTP Method, this maps to HTTP `METHOD_NOT_ALLOWED`.
    HttpMethodNotSupported,

    /// Request URI was not found in `Loader`. This maps to HTTP `NOT_FOUND`.
    LoaderPathNotFound,

    /// Error while parsing HTTP `Accept-Encoding`. This maps to HTTP `BAD_REQUEST`.
    UnparsableAcceptEncoding,
}
impl ResponderError {
    /// Converts error into best matching HTTP error code
    pub fn as_http_status_code(&self) -> StatusCode {
        match self {
            ResponderError::HttpMethodNotSupported => StatusCode::METHOD_NOT_ALLOWED,
            ResponderError::LoaderPathNotFound => StatusCode::NOT_FOUND,
            ResponderError::UnparsableAcceptEncoding => StatusCode::BAD_REQUEST,
        }
    }

    /// Creates default response (status code + empty body) for this error.
    pub fn as_default_response(&self) -> Response<Body> {
        Response::builder()
            .status(self.as_http_status_code())
            .body(Body::default())
            .unwrap()
    }
}

/// Main class for hyper integration.
/// Given `Loader`, responds to incoming requests serving files from `Loader`.
pub struct Responder<'l> {
    loader: &'l Loader,
}
impl<'l> Responder<'l> {
    /// Creates instance, using provided `Loader`.
    pub fn new(loader: &'l Loader) -> Self {
        Self { loader }
    }

    /// Given basic hyper request, responds to it, or returns `ResponderError`.
    /// To automatically cast `ResponderError` to response, use `request_respond` instead.
    pub fn request_respond_or_error(
        &self,
        request: &Request<Body>,
    ) -> Result<Response<Body>, ResponderError> {
        self.parts_respond_or_error(request.method(), request.uri(), request.headers())
    }

    /// Given set of parts (`method`, `uri` and `headers`), responds to it, or returns `ResponderError`.
    /// To automatically cast `ResponderError` to response, use `parts_respond` instead.
    pub fn parts_respond_or_error(
        &self,
        method: &Method,
        uri: &Uri,
        headers: &HeaderMap,
    ) -> Result<Response<Body>, ResponderError> {
        // Only GET requests are allowed.
        // TODO: Handle HEAD requests.
        match *method {
            Method::GET => (),
            _ => {
                return Err(ResponderError::HttpMethodNotSupported);
            }
        };

        // Find file for given request.
        let file_descriptor = match self.loader.get(uri.path()) {
            Some(file_descriptor) => file_descriptor,
            None => {
                return Err(ResponderError::LoaderPathNotFound);
            }
        };

        // Check for possible ETag.
        // If ETag exists and matches current file, return 304.
        if let Some(etag_request) = headers.get(header::IF_NONE_MATCH) {
            if etag_request.as_bytes() == file_descriptor.etag().as_bytes() {
                return Ok(Response::builder()
                    .status(StatusCode::NOT_MODIFIED)
                    .body(Body::default())
                    .unwrap());
            }
        };

        // Check accepted encodings
        let mut accepted_encoding_gzip = false;
        if let Some(accept_encoding) = headers.get(header::ACCEPT_ENCODING) {
            let accept_encoding = match accept_encoding.to_str() {
                Ok(accept_encoding) => accept_encoding,
                Err(_) => {
                    return Err(ResponderError::UnparsableAcceptEncoding);
                }
            };

            #[allow(clippy::single_match)]
            accept_encoding
                .split(", ")
                .for_each(|accept_encoding| match accept_encoding {
                    "gzip" => {
                        accepted_encoding_gzip = true;
                    }
                    _ => {}
                });
        }

        // Select data based on accepted encoding
        // (chunk, content_encoding)
        let (content, content_encoding) =
            if accepted_encoding_gzip && file_descriptor.content_gzip().is_some() {
                (file_descriptor.content_gzip().unwrap(), "gzip")
            } else {
                (file_descriptor.content(), "identity")
            };

        // Provide response.
        let response = Response::builder()
            .header(header::CONTENT_TYPE, file_descriptor.content_type())
            .header(header::CONTENT_LENGTH, content.len())
            .header(header::CONTENT_ENCODING, content_encoding)
            .header(header::ETAG, file_descriptor.etag())
            .body(Body::from(content))
            .unwrap();

        Ok(response)
    }

    /// Given basic hyper request, responds to it.
    /// In case of error creates default http response. If specific error control is needed, use `request_respond_or_error` instead.
    pub fn request_respond(
        &self,
        request: &Request<Body>,
    ) -> Response<Body> {
        match self.request_respond_or_error(request) {
            Ok(response) => response,
            Err(error) => error.as_default_response(),
        }
    }

    /// Given set of parts (`method`, `uri` and `headers`), responds to it.
    /// In case of error creates default http response. If specific error control is needed, use `parts_respond_or_error` instead.
    pub fn parts_respond(
        &self,
        method: &Method,
        uri: &Uri,
        headers: &HeaderMap,
    ) -> Response<Body> {
        match self.parts_respond_or_error(method, uri, headers) {
            Ok(response) => response,
            Err(error) => error.as_default_response(),
        }
    }
}