lib-humus 0.5.0

Helps creating configurable frontends for humans and computers using axum, Tera and toml.
use mime::Mime;
use serde::Deserialize;
use serde::Serialize;

/// Defines how the response should be rendered.
#[derive(Debug, Clone, Copy)]
pub enum HumusFormatFamily {
    /// When rendering the templating engine will be invoked.
    Template,

    /// When rendering the [View] is asked to [generate an API response].
    ///
    /// [View]: ./trait.HumusView.html
    /// [generate an API response]: ./trait.HumusView.html#method.get_api_response
    API,
}

/// Provides information on what to render and how to deliver it to the [HumusEngine].
///
/// It is an integral part of a [HumusQuerySettings] implementation.
///
/// It is best implemented on an enum.
///
/// Also have a look at the provided methods! They may be sane defaults
/// for testing and prototyping, implementing them yourself on enum match logic
/// will improve performance and help with providing missing values.
///
/// For a quick prototype implementing ToString and the `from_name()`
/// method is enough in most cases.
///
/// Recommended macros:
/// ```rust,ignore
/// #[derive(Clone,Serialize,Deserialize,Default)]
/// #[serde(rename_all="lowercase")]
/// ```
///
/// For an example implementation see:
///  [HtmlTextJsonFormat](./enum.HtmlTextJsonFormat.html)
///
/// [HumusEngine]: ./struct.HumusEngine.html
/// [HumusQuerySettings]: ./trait.HumusQuerySettings.html
pub trait HumusFormat: ToString + Clone + Default + Send {
    /// Return wheter this format should be processed in Template or API mode.
    fn get_family(&self) -> HumusFormatFamily {
        match self.get_name().as_str() {
            "json" => HumusFormatFamily::API,
            _ => HumusFormatFamily::Template,
        }
    }

    /// Returns the file extnsion for the format.
    ///
    /// Used for deriving the path for the template name.
    ///
    /// Defaults to `.{self.get_name()}`
    /// with the exception of the name being `text`
    /// then it defaults to `.txt`.
    fn get_file_extension(&self) -> String {
        match self.get_name().as_str() {
            "text" => ".txt".to_string(),
            _ => ".".to_owned() + &self.get_name(),
        }
    }

    /// Returns the name of the format,
    /// by default taken from the ToString implementation.
    fn get_name(&self) -> String {
        self.to_string()
    }

    /// Allows adding extra mimetypes quickly for prototyping
    ///
    /// Implementing get_mime_type() properly is recommended
    /// for production use.
    fn get_less_well_known_mimetype(&self) -> Option<Mime> {
        None
    }

    /// Returns the Mimetype that is expected for this output format.
    ///
    /// It is recommended to implement this when in production use.
    ///
    /// For prototyping the default implementation makes assumptions
    /// based on the output of get_name(), falling back
    /// to get_less_well_known_mimetype() and the "application/octet-stream" type.
    ///
    /// The default implementation knows the following associations:
    /// * `text`: `text/plain; charset=utf-8`
    /// * `html`: `text/html; charset=utf-8`
    /// * `json`: `application/json`
    /// * `xml`: `application/xml`
    /// * `rss`: `application/rss+xml`
    /// * `atom`: `application/atom+xml`
    ///
    /// *Implementation Note:* It may be possible that two different views
    /// have the same MimeType (maybe two json representations for different consumers).
    ///
    fn get_mime_type(&self) -> Mime {
        match self.get_name().as_str() {
            "text" => mime::TEXT_PLAIN_UTF_8,
            "html" => mime::TEXT_HTML_UTF_8,
            "json" => mime::APPLICATION_JSON,
            "xml" => "application/xml"
                .parse()
                .expect("Parse static application/xml"),
            "rss" => "application/rss+xml"
                .parse()
                .expect("Parse static application/rss+xml"),
            "atom" => "application/atom+xml"
                .parse()
                .expect("Parse static application/atom+xml"),
            _ => self
                .get_less_well_known_mimetype()
                .unwrap_or(mime::APPLICATION_OCTET_STREAM),
        }
    }

    /// Constructs a view from its name.
    fn from_name(name: &str) -> Option<Self>;
}

// Some Sample implementations

/// Ready to use example implementation of a [HumusFormat]
/// featuring `html`, `text` and `json`.
///
/// When using this it is recommended to crete a type alias
/// in case this isn't sufficient in the future.
/// ```
/// type MyResponseFormat = lib_humus::HtmlTextJsonFormat;
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum HtmlTextJsonFormat {
    /// An html response.
    #[default]
    Html,

    /// A plain text response to be viewed in a terminal.
    Text,

    /// A json response for other programs.
    Json,
}

impl ToString for HtmlTextJsonFormat {
    fn to_string(&self) -> String {
        match self {
            Self::Html => "html",
            Self::Text => "text",
            Self::Json => "json",
        }
        .to_owned()
    }
}

impl HumusFormat for HtmlTextJsonFormat {
    fn get_family(&self) -> HumusFormatFamily {
        match self {
            Self::Json => HumusFormatFamily::API,
            _ => HumusFormatFamily::Template,
        }
    }

    fn get_file_extension(&self) -> String {
        match self {
            Self::Text => ".txt",
            Self::Html => ".html",
            Self::Json => ".json",
        }
        .to_string()
    }

    fn get_mime_type(&self) -> Mime {
        match self {
            Self::Text => mime::TEXT_PLAIN_UTF_8,
            Self::Html => mime::TEXT_HTML_UTF_8,
            Self::Json => mime::APPLICATION_JSON,
        }
    }

    fn from_name(name: &str) -> Option<Self> {
        match name {
            "html" => Some(Self::Html),
            "text" => Some(Self::Text),
            "json" => Some(Self::Json),
            _ => None,
        }
    }
}