s2-common 0.31.1

Common stuff for client and servers for S2, the durable streams API
Documentation
use std::{marker::PhantomData, ops::Deref, str::FromStr};

use compact_str::{CompactString, ToCompactString};
use enumset::{EnumSet, EnumSetType};

use super::{
    ValidationError,
    basin::{BasinName, BasinNamePrefix},
    stream::{StreamName, StreamNamePrefix},
    strings::{IdProps, PrefixProps, StartAfterProps, StrProps},
};
use crate::{caps, types::resources::ListItemsRequest};

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
    feature = "rkyv",
    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub struct AccessTokenIdStr<T: StrProps>(CompactString, PhantomData<T>);

impl<T: StrProps> AccessTokenIdStr<T> {
    fn validate_str(id: &str) -> Result<(), ValidationError> {
        if !T::IS_PREFIX && id.is_empty() {
            return Err(format!("access token {} must not be empty", T::FIELD_NAME).into());
        }

        if !T::IS_PREFIX && (id == "." || id == "..") {
            return Err(
                format!("access token {} must not be \".\" or \"..\"", T::FIELD_NAME).into(),
            );
        }

        if id.len() > caps::MAX_ACCESS_TOKEN_ID_LEN {
            return Err(format!(
                "access token {} must not exceed {} bytes in length",
                T::FIELD_NAME,
                caps::MAX_ACCESS_TOKEN_ID_LEN
            )
            .into());
        }

        Ok(())
    }
}

#[cfg(feature = "utoipa")]
impl<T> utoipa::PartialSchema for AccessTokenIdStr<T>
where
    T: StrProps,
{
    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
        utoipa::openapi::Object::builder()
            .schema_type(utoipa::openapi::Type::String)
            .min_length((!T::IS_PREFIX).then_some(1))
            .max_length(Some(caps::MAX_ACCESS_TOKEN_ID_LEN))
            .into()
    }
}

#[cfg(feature = "utoipa")]
impl<T> utoipa::ToSchema for AccessTokenIdStr<T> where T: StrProps {}

impl<T: StrProps> serde::Serialize for AccessTokenIdStr<T> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.0)
    }
}

impl<'de, T: StrProps> serde::Deserialize<'de> for AccessTokenIdStr<T> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = CompactString::deserialize(deserializer)?;
        s.try_into().map_err(serde::de::Error::custom)
    }
}

impl<T: StrProps> AsRef<str> for AccessTokenIdStr<T> {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl<T: StrProps> Deref for AccessTokenIdStr<T> {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<T: StrProps> TryFrom<CompactString> for AccessTokenIdStr<T> {
    type Error = ValidationError;

    fn try_from(name: CompactString) -> Result<Self, Self::Error> {
        Self::validate_str(&name)?;
        Ok(Self(name, PhantomData))
    }
}

impl<T: StrProps> FromStr for AccessTokenIdStr<T> {
    type Err = ValidationError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::validate_str(s)?;
        Ok(Self(s.to_compact_string(), PhantomData))
    }
}

impl<T: StrProps> std::fmt::Debug for AccessTokenIdStr<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

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

impl<T: StrProps> From<AccessTokenIdStr<T>> for CompactString {
    fn from(value: AccessTokenIdStr<T>) -> Self {
        value.0
    }
}

pub type AccessTokenId = AccessTokenIdStr<IdProps>;

pub type AccessTokenIdPrefix = AccessTokenIdStr<PrefixProps>;

impl Default for AccessTokenIdPrefix {
    fn default() -> Self {
        AccessTokenIdStr(CompactString::default(), PhantomData)
    }
}

impl From<AccessTokenId> for AccessTokenIdPrefix {
    fn from(value: AccessTokenId) -> Self {
        Self(value.0, PhantomData)
    }
}

pub type AccessTokenIdStartAfter = AccessTokenIdStr<StartAfterProps>;

impl Default for AccessTokenIdStartAfter {
    fn default() -> Self {
        AccessTokenIdStr(CompactString::default(), PhantomData)
    }
}

impl From<AccessTokenId> for AccessTokenIdStartAfter {
    fn from(value: AccessTokenId) -> Self {
        Self(value.0, PhantomData)
    }
}

#[derive(Debug, Hash, EnumSetType, strum::EnumCount)]
pub enum Operation {
    ListBasins = 1,
    CreateBasin = 2,
    DeleteBasin = 3,
    ReconfigureBasin = 4,
    GetBasinConfig = 5,
    IssueAccessToken = 6,
    RevokeAccessToken = 7,
    ListAccessTokens = 8,
    ListStreams = 9,
    CreateStream = 10,
    DeleteStream = 11,
    GetStreamConfig = 12,
    ReconfigureStream = 13,
    CheckTail = 14,
    Append = 15,
    Read = 16,
    Trim = 17,
    Fence = 18,
    AccountMetrics = 19,
    BasinMetrics = 20,
    StreamMetrics = 21,
}

#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(
    feature = "rkyv",
    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub enum ResourceSet<E, P> {
    #[default]
    None,
    Exact(E),
    Prefix(P),
}

pub type BasinResourceSet = ResourceSet<BasinName, BasinNamePrefix>;
pub type StreamResourceSet = ResourceSet<StreamName, StreamNamePrefix>;
pub type AccessTokenResourceSet = ResourceSet<AccessTokenId, AccessTokenIdPrefix>;

#[derive(Debug, Clone, Copy, Default)]
pub struct ReadWritePermissions {
    pub read: bool,
    pub write: bool,
}

#[derive(Debug, Clone, Default)]
pub struct PermittedOperationGroups {
    pub account: ReadWritePermissions,
    pub basin: ReadWritePermissions,
    pub stream: ReadWritePermissions,
}

#[derive(Debug, Clone, Default)]
pub struct AccessTokenScope {
    pub basins: BasinResourceSet,
    pub streams: StreamResourceSet,
    pub access_tokens: AccessTokenResourceSet,
    pub op_groups: PermittedOperationGroups,
    pub ops: EnumSet<Operation>,
}

#[derive(Debug, Clone)]
pub struct AccessTokenInfo {
    pub id: AccessTokenId,
    pub expires_at: time::OffsetDateTime,
    pub auto_prefix_streams: bool,
    pub scope: AccessTokenScope,
}

#[derive(Debug, Clone)]
pub struct IssueAccessTokenRequest {
    pub id: AccessTokenId,
    pub expires_at: Option<time::OffsetDateTime>,
    pub auto_prefix_streams: bool,
    pub scope: AccessTokenScope,
}

pub type ListAccessTokensRequest = ListItemsRequest<AccessTokenIdPrefix, AccessTokenIdStartAfter>;

#[cfg(test)]
mod test {
    use rstest::rstest;

    use super::{
        super::strings::{IdProps, PrefixProps, StartAfterProps},
        AccessTokenIdStr,
    };

    #[rstest]
    #[case::normal("my-token".to_owned())]
    #[case::max_len("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN))]
    fn validate_id_ok(#[case] id: String) {
        assert_eq!(AccessTokenIdStr::<IdProps>::validate_str(&id), Ok(()));
    }

    #[rstest]
    #[case::empty("".to_owned())]
    #[case::dot(".".to_owned())]
    #[case::dot_dot("..".to_owned())]
    #[case::too_long("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN + 1))]
    fn validate_id_err(#[case] id: String) {
        AccessTokenIdStr::<IdProps>::validate_str(&id).expect_err("expected validation error");
    }

    #[rstest]
    #[case::empty("".to_owned())]
    #[case::dot(".".to_owned())]
    #[case::dot_dot("..".to_owned())]
    #[case::max_len("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN))]
    fn validate_prefix_ok(#[case] prefix: String) {
        assert_eq!(
            AccessTokenIdStr::<PrefixProps>::validate_str(&prefix),
            Ok(())
        );
    }

    #[rstest]
    #[case::too_long("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN + 1))]
    fn validate_prefix_err(#[case] prefix: String) {
        AccessTokenIdStr::<PrefixProps>::validate_str(&prefix)
            .expect_err("expected validation error");
    }

    #[rstest]
    #[case::empty("".to_owned())]
    #[case::dot(".".to_owned())]
    #[case::dot_dot("..".to_owned())]
    #[case::max_len("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN))]
    fn validate_start_after_ok(#[case] start_after: String) {
        assert_eq!(
            AccessTokenIdStr::<StartAfterProps>::validate_str(&start_after),
            Ok(())
        );
    }

    #[rstest]
    #[case::too_long("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN + 1))]
    fn validate_start_after_err(#[case] start_after: String) {
        AccessTokenIdStr::<StartAfterProps>::validate_str(&start_after)
            .expect_err("expected validation error");
    }
}