1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::{
    fmt::{Display, Formatter, Result as FmtResult},
    str::FromStr,
};
use uuid::Uuid;

#[derive(thiserror::Error, Debug)]
pub enum ScopeError {
    #[error("scope must start with a /")]
    LeadingSlash,
}

#[derive(Serialize, PartialOrd, Ord, PartialEq, Eq, Debug, Clone, Deserialize, Hash)]
pub struct Scope(pub(crate) String);
impl Scope {
    pub fn new<S: Into<String>>(value: S) -> Result<Self, ScopeError> {
        let value = value.into();
        if !value.starts_with('/') {
            return Err(ScopeError::LeadingSlash);
        }
        Ok(Self(value))
    }

    #[must_use]
    pub fn from_subscription(subscription_id: &Uuid) -> Self {
        Self(format!("/subscriptions/{subscription_id}"))
    }

    #[must_use]
    pub fn from_resource_group(subscription_id: &Uuid, resource_group: &str) -> Self {
        Self(format!(
            "/subscriptions/{subscription_id}/resourceGroups/{resource_group}"
        ))
    }

    #[must_use]
    pub fn from_provider(subscription_id: &Uuid, resource_group: &str, provider: &str) -> Self {
        Self(format!(
            "/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/{provider}"
        ))
    }

    #[must_use]
    pub fn is_subscription(&self) -> bool {
        self.0.starts_with("/subscriptions/") && !self.0.contains("/resourceGroups/")
    }

    #[must_use]
    pub fn subscription(&self) -> Option<Uuid> {
        let entries = self.0.split('/').collect::<Vec<_>>();
        let first = entries.get(1)?;
        if first != &"subscriptions" {
            return None;
        }
        let id = entries.get(2)?;
        Uuid::parse_str(id).ok()
    }

    #[must_use]
    pub fn contains(&self, other: &Self) -> bool {
        let first = self.0.split('/').collect::<Vec<_>>();
        let second = other.0.split('/').collect::<Vec<_>>();

        let left = Some(&first[..]);
        let right = second.get(0..first.len());

        left == right
    }
}

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

impl FromStr for Scope {
    type Err = ScopeError;
    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        Self::new(s.to_string())
    }
}

#[cfg(test)]
mod tests {
    use crate::models::scope::Scope;

    #[test]
    fn test_contains() {
        let with_provider = Scope("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/provider".to_string());
        let with_rg1 = Scope(
            "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg".to_string(),
        );
        let with_rg2 = Scope(
            "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/r".to_string(),
        );
        let with_sub1 = Scope("/subscriptions/00000000-0000-0000-0000-000000000000".to_string());
        let with_sub2 = Scope("/subscriptions/00000000-0000-0000-0000-000000000001".to_string());

        assert!(with_rg1.contains(&with_provider));
        assert!(with_rg1.contains(&with_rg1));

        assert!(!with_provider.contains(&with_rg1));
        assert!(!with_rg2.contains(&with_provider));

        assert!(with_sub1.contains(&with_provider));
        assert!(with_sub1.contains(&with_rg1));
        assert!(with_sub1.contains(&with_rg2));
        assert!(with_sub1.contains(&with_sub1));
        assert!(!with_sub1.contains(&with_sub2));
    }
}