coil-storage 0.1.1

Object storage primitives for the Coil framework.
Documentation
use coil_config::StorageClass;
use thiserror::Error;

use super::{PathPolicyKind, StoragePolicy, StoragePolicyOverride};
use crate::policy::paths::{normalize_relative_path, normalize_rule_prefix};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathPolicyRule {
    pub kind: PathPolicyKind,
    pub path_prefix: String,
    pub storage_class: Option<StorageClass>,
    pub policy: StoragePolicy,
    pub object_prefix: Option<String>,
    pub local_subdir: Option<String>,
}

impl PathPolicyRule {
    pub fn new(
        path_prefix: impl Into<String>,
        storage_class: Option<StorageClass>,
        policy: StoragePolicy,
    ) -> Result<Self, StoragePolicyError> {
        Self::folder(path_prefix, storage_class, policy)
    }

    pub fn folder(
        path_prefix: impl Into<String>,
        storage_class: Option<StorageClass>,
        policy: StoragePolicy,
    ) -> Result<Self, StoragePolicyError> {
        let path_prefix = normalize_rule_prefix(&path_prefix.into())?;
        policy.validate()?;
        Ok(Self {
            kind: PathPolicyKind::Folder,
            path_prefix,
            storage_class,
            policy,
            object_prefix: None,
            local_subdir: None,
        })
    }

    pub fn upload(
        path_prefix: impl Into<String>,
        storage_class: Option<StorageClass>,
        policy: StoragePolicy,
    ) -> Result<Self, StoragePolicyError> {
        let path_prefix = normalize_relative_path(&path_prefix.into())?;
        policy.validate()?;
        Ok(Self {
            kind: PathPolicyKind::Upload,
            path_prefix,
            storage_class,
            policy,
            object_prefix: None,
            local_subdir: None,
        })
    }

    pub fn with_object_prefix(
        mut self,
        prefix: impl Into<String>,
    ) -> Result<Self, StoragePolicyError> {
        self.object_prefix = Some(normalize_rule_prefix(&prefix.into())?);
        Ok(self)
    }

    pub fn with_local_subdir(
        mut self,
        subdir: impl Into<String>,
    ) -> Result<Self, StoragePolicyError> {
        self.local_subdir = Some(normalize_rule_prefix(&subdir.into())?);
        Ok(self)
    }

    pub(crate) fn matches(&self, logical_path: &str) -> bool {
        match self.kind {
            PathPolicyKind::Folder => {
                self.path_prefix.is_empty()
                    || logical_path == self.path_prefix
                    || logical_path.starts_with(&format!("{}/", self.path_prefix))
            }
            PathPolicyKind::Upload => logical_path == self.path_prefix,
        }
    }

    pub(crate) fn specificity(&self) -> (u8, usize) {
        match self.kind {
            PathPolicyKind::Upload => (2, self.path_prefix.len()),
            PathPolicyKind::Folder => (1, self.path_prefix.len()),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoragePolicyGraph {
    rules: Vec<PathPolicyRule>,
}

impl StoragePolicyGraph {
    pub fn new() -> Self {
        Self { rules: Vec::new() }
    }

    pub fn with_rule(mut self, rule: PathPolicyRule) -> Self {
        self.rules.push(rule);
        self.rules
            .sort_by(|left, right| right.specificity().cmp(&left.specificity()));
        self
    }

    pub fn with_folder_rule(self, rule: PathPolicyRule) -> Self {
        debug_assert!(matches!(rule.kind, PathPolicyKind::Folder));
        self.with_rule(rule)
    }

    pub fn with_upload_rule(self, rule: PathPolicyRule) -> Self {
        debug_assert!(matches!(rule.kind, PathPolicyKind::Upload));
        self.with_rule(rule)
    }

    pub fn rules(&self) -> &[PathPolicyRule] {
        &self.rules
    }

    pub fn resolve(
        &self,
        storage_class: StorageClass,
        logical_path: &str,
        override_policy: Option<&StoragePolicyOverride>,
    ) -> Result<ResolvedStoragePolicy, StoragePolicyError> {
        let logical_path = normalize_relative_path(logical_path)?;
        let matched_rule = self.rules.iter().find(|rule| rule.matches(&logical_path));

        let derived_class = matched_rule
            .and_then(|rule| rule.storage_class)
            .unwrap_or(storage_class);

        let base_policy = matched_rule
            .map(|rule| rule.policy)
            .unwrap_or_else(|| derived_class.into());
        let policy = override_policy
            .map(|policy_override| policy_override.apply_to(base_policy))
            .unwrap_or(base_policy);
        policy.validate()?;

        Ok(ResolvedStoragePolicy {
            storage_class: derived_class,
            policy,
            matched_rule_prefix: matched_rule.map(|rule| rule.path_prefix.clone()),
            matched_rule_kind: matched_rule.map(|rule| rule.kind),
            object_prefix: matched_rule.and_then(|rule| rule.object_prefix.clone()),
            local_subdir: matched_rule.and_then(|rule| rule.local_subdir.clone()),
        })
    }
}

impl Default for StoragePolicyGraph {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedStoragePolicy {
    pub storage_class: StorageClass,
    pub policy: StoragePolicy,
    pub matched_rule_prefix: Option<String>,
    pub matched_rule_kind: Option<PathPolicyKind>,
    pub object_prefix: Option<String>,
    pub local_subdir: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoragePolicySet {
    graph: StoragePolicyGraph,
}

impl StoragePolicySet {
    pub fn new() -> Self {
        Self {
            graph: StoragePolicyGraph::new(),
        }
    }

    pub fn with_rule(mut self, rule: PathPolicyRule) -> Self {
        self.graph = self.graph.with_rule(rule);
        self
    }

    pub fn with_folder_rule(mut self, rule: PathPolicyRule) -> Self {
        self.graph = self.graph.with_folder_rule(rule);
        self
    }

    pub fn with_upload_rule(mut self, rule: PathPolicyRule) -> Self {
        self.graph = self.graph.with_upload_rule(rule);
        self
    }

    pub fn graph(&self) -> &StoragePolicyGraph {
        &self.graph
    }

    pub fn resolve(
        &self,
        storage_class: StorageClass,
        logical_path: &str,
        override_policy: Option<&StoragePolicyOverride>,
    ) -> Result<ResolvedStoragePolicy, StoragePolicyError> {
        self.graph
            .resolve(storage_class, logical_path, override_policy)
    }
}

impl Default for StoragePolicySet {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum StoragePolicyError {
    #[error("storage policy contains an invalid combination: {detail}")]
    InvalidCombination { detail: String },
    #[error("storage paths must be relative and non-empty, got `{path}`")]
    InvalidRelativePath { path: String },
    #[error("storage paths cannot traverse parent segments, got `{path}`")]
    ParentTraversal { path: String },
}