aliyun-oss 0.2.0

aliyun oss sdk
Documentation
//! Object key and ETag newtype wrappers.

use std::fmt;

use crate::error::{ErrorContext, OssError, OssErrorKind, Result};

const MAX_OBJECT_KEY_LENGTH: usize = 1024;

/// A validated OSS object key (non-empty, max 1024 characters).
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct ObjectKey(String);

impl ObjectKey {
    /// Creates a new `ObjectKey` after validating the input.
    pub fn new(key: impl Into<String>) -> Result<Self> {
        let key = key.into();
        validate_object_key(&key)?;
        Ok(Self(key))
    }

    /// Returns the object key as a string slice.
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Returns the parent directory path, if any.
    pub fn parent(&self) -> Option<&str> {
        let path = &self.0;
        if let Some(pos) = path.rfind('/') {
            Some(&path[..=pos])
        } else {
            None
        }
    }

    /// Returns the file name component of the object key.
    pub fn file_name(&self) -> &str {
        let path = &self.0;
        if let Some(pos) = path.rfind('/') {
            &path[pos + 1..]
        } else {
            path
        }
    }
}

impl fmt::Debug for ObjectKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("ObjectKey").field(&self.0).finish()
    }
}

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

fn validate_object_key(key: &str) -> Result<()> {
    if key.is_empty() {
        return Err(OssError {
            kind: OssErrorKind::ValidationError,
            context: Box::new(ErrorContext {
                operation: Some("validate ObjectKey: empty".into()),
                object_key: Some(key.to_string()),
                ..Default::default()
            }),
            source: None,
        });
    }

    if key.len() > MAX_OBJECT_KEY_LENGTH {
        return Err(OssError {
            kind: OssErrorKind::ValidationError,
            context: Box::new(ErrorContext {
                operation: Some(format!(
                    "validate ObjectKey length: {} (max {MAX_OBJECT_KEY_LENGTH})",
                    key.len()
                )),
                object_key: Some(key.to_string()),
                ..Default::default()
            }),
            source: None,
        });
    }

    Ok(())
}

/// An entity tag (ETag) for object versioning, returned from OSS responses.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ETag(String);

impl ETag {
    /// Creates a new `ETag` from a raw string value.
    pub fn new(tag: impl Into<String>) -> Self {
        Self(tag.into())
    }

    /// Parses an ETag from an HTTP response header value, stripping surrounding quotes.
    pub fn from_header(value: &str) -> Option<Self> {
        let trimmed = value.trim();
        if trimmed.len() < 2 {
            return None;
        }
        if trimmed.starts_with('"') && trimmed.ends_with('"') {
            Some(Self(trimmed[1..trimmed.len() - 1].to_string()))
        } else {
            Some(Self(trimmed.to_string()))
        }
    }

    /// Returns the ETag as a string slice (without quotes).
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for ETag {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "\"{}\"", self.0)
    }
}

/// Represents an OSS object owner identity.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Owner {
    pub id: String,
    pub display_name: Option<String>,
}

impl Owner {
    /// Creates a new `Owner` with the given ID.
    pub fn new(id: impl Into<String>) -> Self {
        Self {
            id: id.into(),
            display_name: None,
        }
    }

    /// Sets the display name for the owner.
    pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
        self.display_name = Some(name.into());
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn object_key_new_valid() {
        assert!(ObjectKey::new("hello.txt").is_ok());
        assert!(ObjectKey::new("a/b/c/d.jpg").is_ok());
        assert!(ObjectKey::new("中文/文件名.jpg").is_ok());
    }

    #[test]
    fn object_key_rejects_empty() {
        assert!(ObjectKey::new("").is_err());
    }

    #[test]
    fn object_key_rejects_over_max_length() {
        assert!(ObjectKey::new("a".repeat(1025)).is_err());
    }

    #[test]
    fn object_key_accepts_exact_max_length() {
        assert!(ObjectKey::new("a".repeat(1024)).is_ok());
    }

    #[test]
    fn object_key_parent_returns_directory() {
        let key = ObjectKey::new("a/b/c.txt").unwrap();
        assert_eq!(key.parent(), Some("a/b/"));
    }

    #[test]
    fn object_key_parent_no_directory() {
        let key = ObjectKey::new("file.txt").unwrap();
        assert_eq!(key.parent(), None);
    }

    #[test]
    fn object_key_file_name_extracts_name() {
        let key = ObjectKey::new("a/b/c.txt").unwrap();
        assert_eq!(key.file_name(), "c.txt");
        let key2 = ObjectKey::new("noext").unwrap();
        assert_eq!(key2.file_name(), "noext");
    }

    #[test]
    fn etag_from_response_header_with_quotes() {
        let etag = ETag::from_header("\"abc123\"").unwrap();
        assert_eq!(etag.as_str(), "abc123");
    }

    #[test]
    fn etag_from_response_header_without_quotes() {
        let etag = ETag::from_header("abc123").unwrap();
        assert_eq!(etag.as_str(), "abc123");
    }

    #[test]
    fn etag_from_response_header_empty() {
        assert!(ETag::from_header("").is_none());
    }

    #[test]
    fn etag_display_includes_quotes() {
        let etag = ETag::new("abc123");
        assert_eq!(etag.to_string(), "\"abc123\"");
    }

    #[test]
    fn owner_new() {
        let owner = Owner::new("owner-id");
        assert_eq!(owner.id, "owner-id");
        assert!(owner.display_name.is_none());
    }

    #[test]
    fn owner_with_display_name() {
        let owner = Owner::new("owner-id").with_display_name("Owner Name");
        assert_eq!(owner.id, "owner-id");
        assert_eq!(owner.display_name.as_deref(), Some("Owner Name"));
    }

    #[test]
    fn object_key_send_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<ObjectKey>();
        assert_send_sync::<ETag>();
        assert_send_sync::<Owner>();
    }
}