coil-runtime 0.1.1

HTTP runtime and request handling for the Coil framework.
Documentation
use super::support::validate_browser_value;
use super::*;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlashLevel {
    Info,
    Success,
    Warning,
    Error,
}

impl FlashLevel {
    fn as_str(self) -> &'static str {
        match self {
            Self::Info => "info",
            Self::Success => "success",
            Self::Warning => "warning",
            Self::Error => "error",
        }
    }

    fn parse(value: &str) -> Result<Self, RuntimeBrowserError> {
        match value {
            "info" => Ok(Self::Info),
            "success" => Ok(Self::Success),
            "warning" => Ok(Self::Warning),
            "error" => Ok(Self::Error),
            other => Err(RuntimeBrowserError::InvalidFlashLevel {
                level: other.to_string(),
            }),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlashMessage {
    pub level: FlashLevel,
    pub text: String,
}

impl FlashMessage {
    pub fn new(level: FlashLevel, text: impl Into<String>) -> Result<Self, RuntimeBrowserError> {
        let text = validate_browser_value("flash_message", text.into())?;
        Ok(Self { level, text })
    }
}

pub(super) fn serialize_flash_messages(
    messages: &[FlashMessage],
) -> Result<String, RuntimeBrowserError> {
    let mut payload = String::new();
    for message in messages {
        if message.text.trim().is_empty() {
            return Err(RuntimeBrowserError::EmptyValue {
                field: "flash_message",
            });
        }

        payload.push_str(message.level.as_str());
        payload.push(':');
        payload.push_str(&message.text.len().to_string());
        payload.push(':');
        payload.push_str(&message.text);
        payload.push('|');
    }

    Ok(payload)
}

pub(super) fn deserialize_flash_messages(
    payload: &str,
) -> Result<Vec<FlashMessage>, RuntimeBrowserError> {
    let mut remaining = payload;
    let mut messages = Vec::new();

    while !remaining.is_empty() {
        let Some(level_sep) = remaining.find(':') else {
            return Err(RuntimeBrowserError::InvalidFlashPayload);
        };
        let level = &remaining[..level_sep];
        remaining = &remaining[level_sep + 1..];

        let Some(length_sep) = remaining.find(':') else {
            return Err(RuntimeBrowserError::InvalidFlashPayload);
        };
        let length: usize = remaining[..length_sep]
            .parse()
            .map_err(|_| RuntimeBrowserError::InvalidFlashPayload)?;
        remaining = &remaining[length_sep + 1..];

        if remaining.len() < length + 1 {
            return Err(RuntimeBrowserError::InvalidFlashPayload);
        }

        let message = &remaining[..length];
        let separator = &remaining[length..length + 1];
        if separator != "|" {
            return Err(RuntimeBrowserError::InvalidFlashPayload);
        }

        messages.push(FlashMessage::new(FlashLevel::parse(level)?, message)?);
        remaining = &remaining[length + 1..];
    }

    Ok(messages)
}