titanite 0.2.0

Client/Server Library for Gemini protocol with Titan support
Documentation
use anyhow::{bail, Result};

pub const CODE: &[u8] = b"51";

/// [Not found](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found)
pub struct NotFound {
    pub message: Option<String>,
}

impl NotFound {
    /// Build `Self` from UTF-8 header bytes
    /// * expected buffer includes leading status code, message, CRLF
    pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
        use crate::Header;
        let h = buffer.header_bytes()?;
        if h.get(..2)
            .is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
        {
            bail!("Invalid status code")
        }
        Ok(Self {
            message: String::from_utf8((h[3..]).to_vec()).map(|m| {
                if m.is_empty() {
                    None
                } else {
                    Some(m)
                }
            })?,
        })
    }

    /// Convert `Self` into UTF-8 bytes presentation
    pub fn into_bytes(self) -> Vec<u8> {
        match self.message {
            Some(message) => {
                let mut bytes = Vec::with_capacity(message.len() + 5);
                bytes.extend(CODE);
                bytes.push(b' ');
                bytes.extend(message.into_bytes());
                bytes.extend([b'\r', b'\n']);
                bytes
            }
            None => {
                let mut bytes = Vec::with_capacity(4);
                bytes.extend(CODE);
                bytes.extend([b'\r', b'\n']);
                bytes
            }
        }
    }
}

#[test]
fn test() {
    let request = format!("51 message\r\n");
    let source = NotFound::from_bytes(request.as_bytes()).unwrap();

    assert_eq!(source.message, Some("message".to_string()));
    assert_eq!(source.into_bytes(), request.as_bytes());
}