braid_http_rs 0.1.5

Unified Braid Protocol implementation in Rust, including Braid-HTTP, Antimatter CRDT, and BraidFS.
Documentation
//! Shared header parsing and formatting for Braid-HTTP.

use crate::core::error::{BraidError, Result};
use crate::core::types::Version;

/// Parse version header value.
pub fn parse_version_header(value: &str) -> Result<Vec<Version>> {
    use sfv::{BareItem, List, ListEntry, Parser};
    let list: List = Parser::new(value)
        .parse()
        .map_err(|e| BraidError::HeaderParse(format!("Invalid structured header: {:?}", e)))?;
    let mut versions = Vec::new();
    for member in list {
        match member {
            ListEntry::Item(item) => match item.bare_item {
                BareItem::String(s) => versions.push(Version::String(s.into())),
                BareItem::Integer(i) => versions.push(Version::Integer(i.into())),
                BareItem::Token(t) => versions.push(Version::String(t.into())),
                _ => {
                    return Err(BraidError::HeaderParse(
                        "Unsupported item type in version list".to_string(),
                    ))
                }
            },
            ListEntry::InnerList(_) => {
                return Err(BraidError::HeaderParse(
                    "Inner lists not supported for versions".to_string(),
                ))
            }
        }
    }
    Ok(versions)
}

/// Format version header value.
pub fn format_version_header(versions: &[Version]) -> String {
    versions
        .iter()
        .map(|v| match v {
            Version::String(s) => format!("\"{}\"", s.replace("\"", "\\\"")),
            Version::Integer(i) => i.to_string(),
        })
        .collect::<Vec<_>>()
        .join(", ")
}

pub fn parse_current_version_header(value: &str) -> Result<Vec<Version>> {
    parse_version_header(value)
}

pub fn parse_content_range(value: &str) -> Result<(String, String)> {
    let parts: Vec<&str> = value.splitn(2, ' ').collect();
    if parts.len() != 2 {
        return Err(BraidError::HeaderParse(format!(
            "Invalid Content-Range: expected 'unit range', got '{}'",
            value
        )));
    }
    Ok((parts[0].to_string(), parts[1].to_string()))
}

#[inline]
pub fn format_content_range(unit: &str, range: &str) -> String {
    format!("{} {}", unit, range)
}

pub fn parse_heartbeat(value: &str) -> Result<u64> {
    let trimmed = value.trim();
    if let Some(ms_str) = trimmed.strip_suffix("ms") {
        return ms_str
            .parse::<u64>()
            .map(|n| n / 1000)
            .map_err(|_| BraidError::HeaderParse(format!("Invalid heartbeat: {}", value)));
    }
    if let Some(s_str) = trimmed.strip_suffix('s') {
        return s_str
            .parse()
            .map_err(|_| BraidError::HeaderParse(format!("Invalid heartbeat: {}", value)));
    }
    trimmed
        .parse()
        .map_err(|_| BraidError::HeaderParse(format!("Invalid heartbeat: {}", value)))
}

pub fn parse_merge_type(value: &str) -> Result<String> {
    let trimmed = value.trim();
    match trimmed {
        crate::core::protocol::constants::merge_types::SYNC9
        | crate::core::protocol::constants::merge_types::DIAMOND => Ok(trimmed.to_string()),
        _ => Err(BraidError::HeaderParse(format!(
            "Unsupported merge-type: {}",
            value
        ))),
    }
}

pub fn parse_tunneled_response(
    bytes: &[u8],
) -> Result<(u16, std::collections::BTreeMap<String, String>, usize)> {
    let s = String::from_utf8_lossy(bytes);
    if let Some(end_idx) = s.find("\r\n\r\n") {
        let headers_part = &s[..end_idx];
        let mut status = 200;
        let mut headers = std::collections::BTreeMap::new();
        for line in headers_part.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            if let Some(val) = line.strip_prefix(":status:") {
                status = val.trim().parse().unwrap_or(200);
                continue;
            }
            if let Some((name, value)) = line.split_once(':') {
                headers.insert(name.trim().to_lowercase(), value.trim().to_string());
            }
        }
        Ok((status, headers, end_idx + 4))
    } else {
        Err(BraidError::HeaderParse("Incomplete headers".to_string()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_headers() {
        assert_eq!(parse_heartbeat("5s").unwrap(), 5);
        assert_eq!(format_content_range("json", ".f"), "json .f");
    }
}