agent-deploy-contract 0.3.1

Transport-neutral deploy contracts for Agent Infra
Documentation
use std::collections::BTreeSet;

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OwnerKind {
    Runtime,
    Agent,
    User,
}

impl OwnerKind {
    pub fn parse(value: &str) -> Option<Self> {
        match value {
            "runtime" => Some(Self::Runtime),
            "agent" => Some(Self::Agent),
            "user" => Some(Self::User),
            _ => None,
        }
    }

    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Runtime => "runtime",
            Self::Agent => "agent",
            Self::User => "user",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct OwnerRef {
    pub kind: OwnerKind,
    pub id: String,
}

impl OwnerRef {
    pub fn runtime(id: impl Into<String>) -> Self {
        Self {
            kind: OwnerKind::Runtime,
            id: id.into(),
        }
    }

    pub fn agent(id: impl Into<String>) -> Self {
        Self {
            kind: OwnerKind::Agent,
            id: id.into(),
        }
    }

    pub fn user(id: impl Into<String>) -> Self {
        Self {
            kind: OwnerKind::User,
            id: id.into(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OwnerValidationError {
    Empty,
    Duplicate,
    Invalid,
}

pub fn normalize_owners(owners: Vec<OwnerRef>) -> Result<Vec<OwnerRef>, OwnerValidationError> {
    if owners.is_empty() {
        return Err(OwnerValidationError::Empty);
    }
    let mut set = BTreeSet::new();
    for owner in owners {
        let id = owner.id.trim();
        if id.is_empty() || id.len() > 255 {
            return Err(OwnerValidationError::Invalid);
        }
        if !set.insert(OwnerRef {
            kind: owner.kind,
            id: id.to_string(),
        }) {
            return Err(OwnerValidationError::Duplicate);
        }
    }
    Ok(set.into_iter().collect())
}

pub fn owners_intersect(caller: &[OwnerRef], resource: &[OwnerRef]) -> bool {
    let resource_set: BTreeSet<_> = resource.iter().collect();
    caller.iter().any(|owner| resource_set.contains(owner))
}

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

    #[test]
    fn normalize_rejects_empty_and_duplicates() {
        assert!(matches!(
            normalize_owners(vec![]),
            Err(OwnerValidationError::Empty)
        ));
        assert!(matches!(
            normalize_owners(vec![OwnerRef::user("u1"), OwnerRef::user("u1")]),
            Err(OwnerValidationError::Duplicate)
        ));
    }

    #[test]
    fn intersect_requires_shared_identity() {
        assert!(owners_intersect(
            &[OwnerRef::user("u1")],
            &[OwnerRef::user("u1"), OwnerRef::agent("agt_1")]
        ));
        assert!(!owners_intersect(
            &[OwnerRef::runtime("rt-2")],
            &[OwnerRef::runtime("rt-1")]
        ));
    }
}