qubit-fs 0.2.2

Provider-neutral synchronous and asynchronous filesystem abstraction for Rust
Documentation
// =============================================================================
//    Copyright (c) 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================
//! Read operation options.

use crate::error::FsError;
use crate::error::FsErrorKind;
use crate::error::FsOperation;
use crate::metadata::FileSystemCapabilities;
use crate::metadata::FileSystemCapability;
use crate::metadata::ResourceVersion;
use crate::read::ChecksumPolicy;

/// Options controlling a read operation.
///
/// # Examples
///
/// ```
/// use qubit_fs::read::ReadOptions;
///
/// let options = ReadOptions::default().with_length(Some(1_024));
/// assert_eq!(Some(1_024), options.length());
/// assert!(options.validate().is_ok());
/// ```
#[non_exhaustive]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ReadOptions {
    /// Optional byte offset.
    offset: Option<u64>,
    /// Optional byte length.
    length: Option<u64>,
    /// Optional required ETag or provider version.
    if_match: Option<ResourceVersion>,
    /// Optional ETag or provider version that must not match.
    if_none_match: Option<ResourceVersion>,
    /// Checksum validation policy.
    checksum: ChecksumPolicy,
}

impl ReadOptions {
    /// Returns a copy with the byte offset replaced.
    #[inline]
    #[must_use]
    pub const fn with_offset(mut self, offset: Option<u64>) -> Self {
        self.offset = offset;
        self
    }

    /// Returns the optional byte offset.
    #[inline]
    #[must_use]
    pub const fn offset(&self) -> Option<u64> {
        self.offset
    }

    /// Returns a copy with the byte length replaced.
    #[inline]
    #[must_use]
    pub const fn with_length(mut self, length: Option<u64>) -> Self {
        self.length = length;
        self
    }

    /// Returns the optional byte length.
    #[inline]
    #[must_use]
    pub const fn length(&self) -> Option<u64> {
        self.length
    }

    /// Returns a copy with the positive version precondition replaced.
    #[inline]
    #[must_use]
    pub fn with_if_match(mut self, if_match: Option<ResourceVersion>) -> Self {
        self.if_match = if_match;
        self
    }

    /// Returns the optional positive version precondition.
    #[inline]
    #[must_use]
    pub const fn if_match(&self) -> Option<&ResourceVersion> {
        self.if_match.as_ref()
    }

    /// Returns a copy with the negative version precondition replaced.
    #[inline]
    #[must_use]
    pub fn with_if_none_match(mut self, if_none_match: Option<ResourceVersion>) -> Self {
        self.if_none_match = if_none_match;
        self
    }

    /// Returns the optional negative version precondition.
    #[inline]
    #[must_use]
    pub const fn if_none_match(&self) -> Option<&ResourceVersion> {
        self.if_none_match.as_ref()
    }

    /// Returns a copy with the checksum policy replaced.
    #[inline]
    #[must_use]
    pub const fn with_checksum(mut self, checksum: ChecksumPolicy) -> Self {
        self.checksum = checksum;
        self
    }

    /// Returns the checksum policy.
    #[inline]
    #[must_use]
    pub const fn checksum(&self) -> ChecksumPolicy {
        self.checksum
    }

    /// Validates provider-independent read options without performing I/O.
    ///
    /// A zero-length window is valid, including at the largest offset. Resource
    /// existence and access permissions are still checked when opening it.
    /// Offsets at or beyond EOF produce an empty window; lengths extending past
    /// EOF are truncated. These windows do not imply a resource snapshot.
    ///
    /// # Errors
    /// Returns `InvalidOptions` for mutually exclusive conditions or an
    /// explicit range whose exclusive end cannot be represented as `u64`.
    pub fn validate(&self) -> Result<(), FsError> {
        if self.if_match.is_some() && self.if_none_match.is_some() {
            return Err(FsError::new(
                FsErrorKind::InvalidOptions,
                FsOperation::OpenReader,
                "if_match and if_none_match cannot both be specified",
            ));
        }
        if let Some(length) = self.length
            && self.offset.unwrap_or(0).checked_add(length).is_none()
        {
            return Err(FsError::new(
                FsErrorKind::InvalidOptions,
                FsOperation::OpenReader,
                "read range exceeds the representable byte offset",
            ));
        }
        Ok(())
    }

    /// Validates required read semantics against configured capabilities.
    ///
    /// Providers should call this method before opening a reader or producing
    /// any observable side effect.
    ///
    /// # Errors
    ///
    /// Returns [`FsErrorKind::InvalidOptions`] for mutually exclusive version
    /// conditions, or [`FsErrorKind::RequirementNotMet`] with the exact
    /// missing capability for range, conditional, or required-checksum reads.
    pub fn validate_against(&self, capabilities: FileSystemCapabilities) -> Result<(), FsError> {
        self.validate()?;
        if (self.offset.is_some() || self.length.is_some()) && !capabilities.supports(FileSystemCapability::RangeRead) {
            return Err(missing_requirement(
                FileSystemCapability::RangeRead,
                "byte-range reads are required but not supported",
            ));
        }
        if (self.if_match.is_some() || self.if_none_match.is_some())
            && !capabilities.supports(FileSystemCapability::ConditionalRead)
        {
            return Err(missing_requirement(
                FileSystemCapability::ConditionalRead,
                "conditional reads are required but not supported",
            ));
        }
        if self.checksum == ChecksumPolicy::Required && !capabilities.supports(FileSystemCapability::ChecksumValidation)
        {
            return Err(missing_requirement(
                FileSystemCapability::ChecksumValidation,
                "checksum validation is required but not supported",
            ));
        }
        Ok(())
    }
}

/// Builds a typed unmet read requirement.
fn missing_requirement(capability: FileSystemCapability, message: &str) -> FsError {
    FsError::new(FsErrorKind::RequirementNotMet, FsOperation::OpenReader, message).with_required_capability(capability)
}

#[cfg(test)]
mod tests {
    use std::hint::black_box;

    use super::ReadOptions;
    use crate::metadata::ResourceVersion;

    #[test]
    fn version_accessor_is_executed_at_runtime() {
        let if_match: for<'a> fn(&'a ReadOptions) -> Option<&'a ResourceVersion> = black_box(ReadOptions::if_match);
        let options = ReadOptions::default().with_if_match(Some(ResourceVersion::new("v1")));

        assert_eq!(Some("v1"), if_match(&options).map(ResourceVersion::as_str));
    }
}