ugi 0.2.1

Runtime-agnostic Rust request client with HTTP/1.1, HTTP/2, HTTP/3, H2C, WebSocket, SSE, and gRPC support
Documentation
use std::fmt;

use crate::error::{Error, ErrorKind, Result};

/// A validated, lowercase HTTP header name.
///
/// Header names are ASCII-only, case-insensitive, and stored in lowercase.
/// Only alphanumeric characters and hyphens (`-`) are permitted.
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct HeaderName(String);

/// A validated HTTP header value.
///
/// The value may contain any UTF-8 characters except carriage return (`\r`)
/// and newline (`\n`), which are rejected to prevent header-injection attacks.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HeaderValue(String);

/// An ordered, multi-valued HTTP header map.
///
/// Insertion order is preserved.  The same header name may appear multiple
/// times (e.g. `Set-Cookie`).  [`HeaderMap::insert`] replaces any existing
/// entry with the same name; [`HeaderMap::append`] adds a second entry.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct HeaderMap(Vec<(HeaderName, HeaderValue)>);

impl HeaderName {
    /// Parse and validate a header name.
    ///
    /// Returns an error if `name` is empty or contains characters outside the
    /// set `[A-Za-z0-9-]`.  The stored name is lowercased.
    pub fn new(name: impl AsRef<str>) -> Result<Self> {
        let name = name.as_ref().trim();
        if name.is_empty() {
            return Err(Error::new(
                ErrorKind::InvalidHeaderName,
                "header name is empty",
            ));
        }
        if !name
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-'))
        {
            return Err(Error::new(
                ErrorKind::InvalidHeaderName,
                format!("invalid header name: {name}"),
            ));
        }
        Ok(Self(name.to_ascii_lowercase()))
    }

    /// Return the header name as a lowercase `&str`.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl HeaderValue {
    /// Parse and validate a header value.
    ///
    /// Returns an error if `value` contains `\r` or `\n`.
    pub fn new(value: impl AsRef<str>) -> Result<Self> {
        let value = value.as_ref();
        if value.contains(['\r', '\n']) {
            return Err(Error::new(
                ErrorKind::InvalidHeaderValue,
                "header value contains a newline",
            ));
        }
        Ok(Self(value.to_owned()))
    }

    /// Return the header value as a `&str`.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl HeaderMap {
    /// Create an empty header map.
    pub fn new() -> Self {
        Self::default()
    }

    /// Insert or replace a header.
    ///
    /// Any existing entries with the same name are removed before inserting
    /// the new value.
    pub fn insert(&mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Result<()> {
        let name = HeaderName::new(name)?;
        let value = HeaderValue::new(value)?;
        self.0.retain(|(existing, _)| existing != &name);
        self.0.push((name, value));
        Ok(())
    }

    /// Append a header without removing existing entries with the same name.
    pub fn append(&mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Result<()> {
        self.0
            .push((HeaderName::new(name)?, HeaderValue::new(value)?));
        Ok(())
    }

    /// Return the value of the first header with the given name, if any.
    pub fn get(&self, name: &str) -> Option<&str> {
        let name = HeaderName::new(name).ok()?;
        self.0
            .iter()
            .find(|(existing, _)| existing == &name)
            .map(|(_, value)| value.as_str())
    }

    /// Return all values for the given header name.
    pub fn get_all(&self, name: &str) -> Vec<&str> {
        let Ok(name) = HeaderName::new(name) else {
            return Vec::new();
        };
        self.0
            .iter()
            .filter(|(existing, _)| existing == &name)
            .map(|(_, value)| value.as_str())
            .collect()
    }

    /// Iterate over all `(name, value)` pairs in insertion order.
    pub fn iter(&self) -> impl Iterator<Item = (&HeaderName, &HeaderValue)> {
        self.0.iter().map(|(name, value)| (name, value))
    }

    /// Remove all entries with the given name.
    pub fn remove(&mut self, name: &str) {
        if let Ok(name) = HeaderName::new(name) {
            self.0.retain(|(existing, _)| existing != &name);
        }
    }
}

impl fmt::Display for HeaderName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl fmt::Display for HeaderValue {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}