rama-net 0.3.0

rama network types and utilities
Documentation
//! Fragment component types — owned [`Fragment`] and borrowed [`FragmentRef`].
//!
//! Per RFC 3986 §3.5, the fragment is opaque bytes after `#`. Unlike
//! `http::Uri`, rama preserves fragments through parse/serialize round-trips,
//! but the wire writer for HTTP request-targets *strips* the fragment per
//! RFC 9110 §7.1 — fragments are not transmitted as client request-targets.

use core::hash::Hash;

use crate::std::borrow::Cow;

use super::encode::{
    encoded_fragment, encoded_fragment_cmp, encoded_fragment_eq, hash_encoded_fragment,
    write_encoded_fragment,
};

use rama_core::bytes::BytesMut;

use percent_encoding::percent_decode;

/// Owned fragment component (the part after `#`, sans the `#` itself).
///
/// `Default` produces an empty fragment (zero bytes — distinct from
/// "no fragment"; that distinction lives on [`super::Uri::fragment`] /
/// [`super::Uri::set_fragment`]). `Display` writes the raw on-wire
/// bytes (no leading `#`). `Hash` / `PartialOrd` / `Ord` are bytewise —
/// fragments are case-sensitive and pct-encoding-preserving per RFC
/// 3986 §3.5.
#[derive(Debug, Clone, Default)]
pub struct Fragment {
    pub(crate) bytes: BytesMut,
}

impl Fragment {
    /// Percent-encoded fragment string (no leading `#`).
    #[must_use]
    pub fn as_encoded_str(&self) -> Cow<'_, str> {
        encoded_fragment(&self.bytes)
    }

    /// `true` when the fragment contains no bytes. An empty fragment is
    /// still *present* (`#` on the wire) — the present-vs-absent
    /// distinction is owned by [`super::Uri::fragment`].
    #[must_use]
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.bytes.is_empty()
    }

    /// Percent-decoded fragment. `Cow::Borrowed` when no `%XX` escapes
    /// are present; `Cow::Owned` otherwise. UTF-8 errors fall back to
    /// U+FFFD.
    #[must_use]
    pub fn as_decoded_str(&self) -> Cow<'_, str> {
        percent_decode(&self.bytes).decode_utf8_lossy()
    }

    /// Borrowed view. Named `view` (not `as_ref`) so it doesn't shadow
    /// the std `AsRef` trait — see the type-level docs.
    #[must_use]
    #[inline]
    pub fn view(&self) -> FragmentRef<'_> {
        FragmentRef { bytes: &self.bytes }
    }
}

impl PartialEq for Fragment {
    #[inline(always)]
    fn eq(&self, other: &Self) -> bool {
        self.view() == other.view()
    }
}

impl Eq for Fragment {}

impl PartialOrd for Fragment {
    #[inline(always)]
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Fragment {
    #[inline(always)]
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
        self.view().cmp(&other.view())
    }
}

impl Hash for Fragment {
    #[inline(always)]
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
        self.view().hash(state);
    }
}

/// Borrowed view of a URI fragment component (no leading `#`).
#[derive(Debug, Clone, Copy)]
pub struct FragmentRef<'a> {
    pub(crate) bytes: &'a [u8],
}

impl<'a> FragmentRef<'a> {
    #[must_use]
    #[inline]
    pub(crate) const fn new(bytes: &'a [u8]) -> Self {
        Self { bytes }
    }

    /// Borrow a fragment string as a [`FragmentRef`] — no allocation.
    ///
    /// The input is treated as component text. When rendered through
    /// [`FragmentRef::as_encoded_str`], bytes outside the fragment grammar are
    /// percent-encoded while valid existing pct triplets are preserved.
    #[must_use]
    #[inline]
    pub fn from_raw_str(fragment: &'a str) -> Self {
        Self::new(fragment.as_bytes())
    }

    /// Percent-encoded fragment string (no leading `#`).
    #[must_use]
    pub fn as_encoded_str(self) -> Cow<'a, str> {
        encoded_fragment(self.bytes)
    }

    /// `true` when the fragment contains no bytes. An empty fragment is
    /// still *present* (`#` on the wire) — the present-vs-absent
    /// distinction is owned by [`super::Uri::fragment`].
    #[must_use]
    #[inline]
    pub fn is_empty(self) -> bool {
        self.bytes.is_empty()
    }

    /// Percent-decoded fragment. `Cow::Borrowed` when no `%XX` escapes
    /// are present; `Cow::Owned` otherwise. UTF-8 errors fall back to
    /// U+FFFD.
    #[must_use]
    pub fn as_decoded_str(&self) -> Cow<'a, str> {
        percent_decode(self.bytes).decode_utf8_lossy()
    }

    /// Returns an owned copy. Named `into_owned` (matching
    /// [`crate::std::borrow::Cow::into_owned`]) so it doesn't shadow the std `ToOwned`
    /// trait method.
    #[must_use]
    pub fn into_owned(self) -> Fragment {
        Fragment {
            bytes: BytesMut::from(self.bytes),
        }
    }
}

impl PartialEq for FragmentRef<'_> {
    #[inline(always)]
    fn eq(&self, other: &Self) -> bool {
        encoded_fragment_eq(self.bytes, other.bytes)
    }
}

impl Eq for FragmentRef<'_> {}

impl PartialEq<str> for FragmentRef<'_> {
    #[inline(always)]
    fn eq(&self, other: &str) -> bool {
        self.eq(&FragmentRef::from_raw_str(other))
    }
}

impl PartialEq<&str> for FragmentRef<'_> {
    #[inline(always)]
    fn eq(&self, other: &&str) -> bool {
        self.eq(&FragmentRef::from_raw_str(other))
    }
}

impl<'a> PartialEq<FragmentRef<'a>> for str {
    #[inline(always)]
    fn eq(&self, other: &FragmentRef<'a>) -> bool {
        FragmentRef::from_raw_str(self).eq(other)
    }
}

impl<'a> PartialEq<FragmentRef<'a>> for &str {
    #[inline(always)]
    fn eq(&self, other: &FragmentRef<'a>) -> bool {
        FragmentRef::from_raw_str(self).eq(other)
    }
}

impl PartialOrd for FragmentRef<'_> {
    #[inline(always)]
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for FragmentRef<'_> {
    #[inline(always)]
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
        encoded_fragment_cmp(self.bytes, other.bytes)
    }
}

impl Hash for FragmentRef<'_> {
    #[inline(always)]
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
        hash_encoded_fragment(state, self.bytes);
    }
}

impl core::fmt::Display for Fragment {
    #[inline(always)]
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        core::fmt::Display::fmt(&self.view(), f)
    }
}

impl core::fmt::Display for FragmentRef<'_> {
    /// Renders the encoded fragment bytes (no leading `#`).
    #[inline(always)]
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write_encoded_fragment(f, self.bytes)
    }
}

impl core::str::FromStr for Fragment {
    type Err = core::convert::Infallible;

    /// Encode arbitrary input as a [`Fragment`] — bytes outside
    /// `pchar ∪ {'/', '?'}` get percent-encoded. Infallible because
    /// every input round-trips after encoding; `str::parse` users with
    /// `?`-ladder code can still use this through the `Result` shape.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self {
            bytes: super::encode::encode_fragment(s),
        })
    }
}