jotta-fs 0.1.3

Third-party low-level Jottacloud client for Rust.
Documentation
//! Ranges of bytes.
use std::{
    fmt::Debug,
    ops::{Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive},
};

use reqwest::header::HeaderValue;

/// An optionally half-open range of bytes.
#[allow(clippy::module_name_repetitions)]
pub trait ByteRange: Debug {
    /// The first byte of the range (inclusive).
    fn start(&self) -> u64;

    /// The last byte of the range (inclusive).
    fn end(&self) -> Option<u64> {
        self.len().map(|len| len + self.start() - 1)
    }

    /// Get the length.
    ///
    /// ```
    /// use jotta_fs::range::{ByteRange, OpenByteRange, ClosedByteRange};
    ///
    /// assert_eq!(ByteRange::len(&ClosedByteRange::new_to_including(3)), Some(4));
    /// assert_eq!(ByteRange::len(&ClosedByteRange::try_from_bounds(3, 7).unwrap()), Some(5));
    /// assert_eq!(OpenByteRange::full().len(), None);
    /// ```
    fn len(&self) -> Option<u64>;

    /// Is the range empty?
    ///
    /// ```
    /// use jotta_fs::range::{ByteRange, OpenByteRange, ClosedByteRange};
    ///
    /// assert!(!ClosedByteRange::new_to_including(3).is_empty());
    /// assert!(!OpenByteRange::new(10).is_empty());
    /// assert!(!OpenByteRange::full().is_empty());
    /// assert!(ClosedByteRange::new(5, 0).is_empty());
    /// ```
    fn is_empty(&self) -> bool {
        self.len().map_or(false, |len| len == 0)
    }

    /// Format a single "segment" of a HTTP `Range` header.
    ///
    /// ```
    /// use jotta_fs::range::{ByteRange, OpenByteRange, ClosedByteRange};
    ///
    /// assert_eq!(ClosedByteRange::try_from_bounds(5, 50).unwrap().to_http_range(), "5-50");
    ///
    /// assert_eq!(OpenByteRange::new(100).to_http_range(), "100-");
    /// assert_eq!(OpenByteRange::full().to_http_range(), "0-");
    /// ```
    fn to_http_range(&self) -> String {
        format!(
            "{}-{}",
            self.start(),
            self.end().map(|n| n.to_string()).unwrap_or_default(),
        )
    }

    /// Format a [HTTP `Range` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range).
    ///
    fn to_http(&self) -> HeaderValue {
        let s = format!("bytes={}", self.to_http_range());
        HeaderValue::from_str(&s).unwrap()
    }
}

impl ByteRange for OpenByteRange {
    fn start(&self) -> u64 {
        self.start
    }

    fn len(&self) -> Option<u64> {
        None
    }
}

/// An open byte range.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[allow(clippy::module_name_repetitions)]
pub struct OpenByteRange {
    start: u64,
}

impl OpenByteRange {
    /// Construct a new half-open byte range.
    #[must_use]
    pub fn new(start: u64) -> Self {
        Self { start }
    }

    /// Construct a full byte range.
    #[must_use]
    pub fn full() -> Self {
        Self::new(0)
    }
}

impl From<RangeFrom<u64>> for OpenByteRange {
    fn from(r: RangeFrom<u64>) -> Self {
        Self { start: r.start }
    }
}

impl From<RangeFull> for OpenByteRange {
    fn from(_: RangeFull) -> Self {
        Self { start: 0 }
    }
}

/// A closed byte range.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[allow(clippy::module_name_repetitions)]
pub struct ClosedByteRange {
    start: u64,
    len: u64,
}

/// Invalid range.
#[derive(Debug, thiserror::Error)]
pub enum InvalidRangeError {
    /// Byte ranges must not be backwards.
    #[error("range is backwards")]
    Backwards,
}

impl ClosedByteRange {
    /// Construct a new closed byte range.
    #[must_use]
    pub fn new(start: u64, len: u64) -> Self {
        Self { start, len }
    }

    /// Attempt to construct a new closed byte range from some specified bounds.
    ///
    /// ```
    /// use jotta_fs::range::ClosedByteRange;
    ///
    /// let range = ClosedByteRange::try_from_bounds(3, 8).unwrap();
    ///
    /// assert_eq!(range.start(), 3);
    /// assert_eq!(range.end(), 8);
    /// assert_eq!(range.len(), 6);
    /// ```
    ///
    /// # Errors
    ///
    /// ```
    /// # use jotta_fs::range::ClosedByteRange;
    /// assert!(ClosedByteRange::try_from_bounds(100, 0).is_err()); // reversed
    /// ```
    pub fn try_from_bounds(first: u64, last: u64) -> Result<Self, InvalidRangeError> {
        if first > last {
            Err(InvalidRangeError::Backwards)
        } else {
            Ok(Self::new(first, last - first + 1))
        }
    }

    /// Construct a new closed byte range with a specified last byte.
    /// First byte will be 0.
    ///
    /// ```
    /// use jotta_fs::range::ClosedByteRange;
    ///
    /// assert_eq!(ClosedByteRange::new_to_including(10), ClosedByteRange::try_from_bounds(0, 10).unwrap())
    /// ```
    #[must_use]
    pub fn new_to_including(end: u64) -> Self {
        Self::new(0, end + 1)
    }

    /// How many bytes this range includes.
    #[must_use]
    pub fn len(&self) -> u64 {
        self.len
    }

    /// Is the range empty?
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.len == 0
    }

    /// Start of the range.
    #[must_use]
    pub fn start(&self) -> u64 {
        self.start
    }

    /// Get the end byte.
    ///
    /// ```
    /// use jotta_fs::range::ClosedByteRange;
    ///
    /// assert_eq!(ClosedByteRange::try_from_bounds(5, 20).unwrap().end(), 20);
    /// ```
    #[must_use]
    pub fn end(&self) -> u64 {
        self.start + self.len - 1
    }
}

impl ByteRange for ClosedByteRange {
    fn start(&self) -> u64 {
        self.start
    }

    fn len(&self) -> Option<u64> {
        Some(self.len)
    }
}

impl TryFrom<Range<u64>> for ClosedByteRange {
    type Error = InvalidRangeError;

    fn try_from(r: Range<u64>) -> Result<Self, Self::Error> {
        Self::try_from_bounds(r.start, r.end - 1)
    }
}

impl TryFrom<RangeInclusive<u64>> for ClosedByteRange {
    type Error = InvalidRangeError;

    fn try_from(r: RangeInclusive<u64>) -> Result<Self, Self::Error> {
        Self::try_from_bounds(*r.start(), *r.end())
    }
}

impl From<RangeTo<u64>> for ClosedByteRange {
    fn from(r: RangeTo<u64>) -> Self {
        Self::new_to_including(r.end - 1)
    }
}

impl From<RangeToInclusive<u64>> for ClosedByteRange {
    fn from(r: RangeToInclusive<u64>) -> Self {
        Self::new_to_including(r.end)
    }
}