Skip to main content

agent_deploy_contract/
owners.rs

1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum OwnerKind {
8    Runtime,
9    Agent,
10    User,
11}
12
13impl OwnerKind {
14    pub fn parse(value: &str) -> Option<Self> {
15        match value {
16            "runtime" => Some(Self::Runtime),
17            "agent" => Some(Self::Agent),
18            "user" => Some(Self::User),
19            _ => None,
20        }
21    }
22
23    pub const fn as_str(self) -> &'static str {
24        match self {
25            Self::Runtime => "runtime",
26            Self::Agent => "agent",
27            Self::User => "user",
28        }
29    }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
33#[serde(deny_unknown_fields, rename_all = "camelCase")]
34pub struct OwnerRef {
35    pub kind: OwnerKind,
36    pub id: String,
37}
38
39impl OwnerRef {
40    pub fn runtime(id: impl Into<String>) -> Self {
41        Self {
42            kind: OwnerKind::Runtime,
43            id: id.into(),
44        }
45    }
46
47    pub fn agent(id: impl Into<String>) -> Self {
48        Self {
49            kind: OwnerKind::Agent,
50            id: id.into(),
51        }
52    }
53
54    pub fn user(id: impl Into<String>) -> Self {
55        Self {
56            kind: OwnerKind::User,
57            id: id.into(),
58        }
59    }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum OwnerValidationError {
64    Empty,
65    Duplicate,
66    Invalid,
67}
68
69pub fn normalize_owners(owners: Vec<OwnerRef>) -> Result<Vec<OwnerRef>, OwnerValidationError> {
70    if owners.is_empty() {
71        return Err(OwnerValidationError::Empty);
72    }
73    let mut set = BTreeSet::new();
74    for owner in owners {
75        let id = owner.id.trim();
76        if id.is_empty() || id.len() > 255 {
77            return Err(OwnerValidationError::Invalid);
78        }
79        if !set.insert(OwnerRef {
80            kind: owner.kind,
81            id: id.to_string(),
82        }) {
83            return Err(OwnerValidationError::Duplicate);
84        }
85    }
86    Ok(set.into_iter().collect())
87}
88
89pub fn owners_intersect(caller: &[OwnerRef], resource: &[OwnerRef]) -> bool {
90    let resource_set: BTreeSet<_> = resource.iter().collect();
91    caller.iter().any(|owner| resource_set.contains(owner))
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn normalize_rejects_empty_and_duplicates() {
100        assert!(matches!(
101            normalize_owners(vec![]),
102            Err(OwnerValidationError::Empty)
103        ));
104        assert!(matches!(
105            normalize_owners(vec![OwnerRef::user("u1"), OwnerRef::user("u1")]),
106            Err(OwnerValidationError::Duplicate)
107        ));
108    }
109
110    #[test]
111    fn intersect_requires_shared_identity() {
112        assert!(owners_intersect(
113            &[OwnerRef::user("u1")],
114            &[OwnerRef::user("u1"), OwnerRef::agent("agt_1")]
115        ));
116        assert!(!owners_intersect(
117            &[OwnerRef::runtime("rt-2")],
118            &[OwnerRef::runtime("rt-1")]
119        ));
120    }
121}