knafeh 1.0.0

QUIC-based RPC library with Python bindings
Documentation
use std::collections::HashMap;

use crate::error::KnafehError;
use crate::rpc::message::Metadata;

// ---------------------------------------------------------------------------
// HTTP/3 ↔ RPC header mapping
// ---------------------------------------------------------------------------

/// The HTTP header prefix for RPC metadata.
pub const RPC_HEADER_PREFIX: &str = "x-rpc-";

/// The header used to carry the RPC status code.
pub const RPC_STATUS_HEADER: &str = "x-rpc-status";

/// The header used to carry the RPC status message.
pub const RPC_STATUS_MESSAGE_HEADER: &str = "x-rpc-status-message";

/// The header indicating the RPC method kind for streaming dispatch.
pub const RPC_METHOD_KIND_HEADER: &str = "x-rpc-method-kind";

/// Convert RPC metadata to HTTP/3 header list.
///
/// User metadata keys are prefixed with `x-rpc-` to avoid clashes with
/// standard HTTP headers.
pub fn metadata_to_headers(metadata: &Metadata) -> Vec<(String, String)> {
    metadata
        .iter()
        .map(|(k, v)| {
            if k.starts_with("x-rpc-") || k.starts_with(":") {
                (k.clone(), v.clone())
            } else {
                (format!("{RPC_HEADER_PREFIX}{k}"), v.clone())
            }
        })
        .collect()
}

/// Extract RPC metadata from HTTP/3 headers.
///
/// Strips the `x-rpc-` prefix from user metadata keys.
pub fn headers_to_metadata(headers: &[(String, String)]) -> Metadata {
    let mut metadata = HashMap::new();
    for (key, value) in headers {
        if key == RPC_STATUS_HEADER || key == RPC_STATUS_MESSAGE_HEADER {
            metadata.insert(key.clone(), value.clone());
        } else if let Some(stripped) = key.strip_prefix(RPC_HEADER_PREFIX) {
            metadata.insert(stripped.to_string(), value.clone());
        }
        // Ignore standard HTTP headers (non x-rpc- prefixed).
    }
    metadata
}

/// Build the HTTP/3 request headers for an RPC call.
pub fn build_request_headers(
    method_path: &str,
    content_type: &str,
    metadata: &Metadata,
    method_kind: &str,
) -> Vec<(Vec<u8>, Vec<u8>)> {
    let mut headers = vec![
        (b":method".to_vec(), b"POST".to_vec()),
        (b":path".to_vec(), format!("/{method_path}").into_bytes()),
        (b":scheme".to_vec(), b"https".to_vec()),
        (b"content-type".to_vec(), content_type.as_bytes().to_vec()),
        (
            RPC_METHOD_KIND_HEADER.as_bytes().to_vec(),
            method_kind.as_bytes().to_vec(),
        ),
    ];

    for (key, value) in metadata {
        let header_key = if key.starts_with("x-rpc-") || key.starts_with(":") {
            key.clone()
        } else {
            format!("{RPC_HEADER_PREFIX}{key}")
        };
        headers.push((header_key.into_bytes(), value.clone().into_bytes()));
    }

    headers
}

/// Build the HTTP/3 response headers for an RPC response.
pub fn build_response_headers(
    status_code: u8,
    status_message: &str,
    content_type: &str,
    metadata: &Metadata,
) -> Vec<(Vec<u8>, Vec<u8>)> {
    let mut headers = vec![
        (b":status".to_vec(), b"200".to_vec()),
        (b"content-type".to_vec(), content_type.as_bytes().to_vec()),
        (
            RPC_STATUS_HEADER.as_bytes().to_vec(),
            status_code.to_string().into_bytes(),
        ),
        (
            RPC_STATUS_MESSAGE_HEADER.as_bytes().to_vec(),
            status_message.as_bytes().to_vec(),
        ),
    ];

    for (key, value) in metadata {
        let header_key = if key.starts_with("x-rpc-") || key.starts_with(":") {
            key.clone()
        } else {
            format!("{RPC_HEADER_PREFIX}{key}")
        };
        headers.push((header_key.into_bytes(), value.clone().into_bytes()));
    }

    headers
}

/// Extract the method path from HTTP/3 pseudo-header `:path`.
///
/// Strips the leading `/` — e.g., `/greeter/say_hello` → `greeter/say_hello`.
pub fn extract_method_path(path: &[u8]) -> Result<String, KnafehError> {
    let path_str = std::str::from_utf8(path)
        .map_err(|e| KnafehError::InvalidMessage(format!("invalid UTF-8 in :path: {e}")))?;

    let trimmed = path_str.strip_prefix('/').unwrap_or(path_str);

    if !trimmed.contains('/') {
        return Err(KnafehError::InvalidMessage(format!(
            "invalid RPC path (expected 'service/method'): {trimmed}"
        )));
    }

    Ok(trimmed.to_string())
}