Skip to main content

azure_pim_cli/models/
scope.rs

1use anyhow::Result;
2use clap::Args;
3use serde::{Deserialize, Serialize};
4use std::{
5    fmt::{Display, Formatter, Result as FmtResult},
6    str::FromStr,
7};
8use uuid::Uuid;
9
10#[derive(thiserror::Error, Debug)]
11pub enum ScopeError {
12    #[error("scope must start with a /")]
13    LeadingSlash,
14}
15
16#[derive(Serialize, PartialOrd, Ord, PartialEq, Eq, Debug, Clone, Deserialize, Hash)]
17pub struct Scope(pub(crate) String);
18impl Scope {
19    pub fn new<S: Into<String>>(value: S) -> Result<Self, ScopeError> {
20        let value = value.into();
21        if !value.starts_with('/') {
22            return Err(ScopeError::LeadingSlash);
23        }
24        Ok(Self(value))
25    }
26
27    #[must_use]
28    pub fn from_subscription(subscription_id: &Uuid) -> Self {
29        Self(format!("/subscriptions/{subscription_id}"))
30    }
31
32    #[must_use]
33    pub fn from_resource_group(subscription_id: &Uuid, resource_group: &str) -> Self {
34        Self(format!(
35            "/subscriptions/{subscription_id}/resourceGroups/{resource_group}"
36        ))
37    }
38
39    #[must_use]
40    pub fn from_provider(subscription_id: &Uuid, resource_group: &str, provider: &str) -> Self {
41        Self(format!(
42            "/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/{provider}"
43        ))
44    }
45
46    #[must_use]
47    pub fn is_subscription(&self) -> bool {
48        self.0.starts_with("/subscriptions/") && !self.0.contains("/resourceGroups/")
49    }
50
51    #[must_use]
52    pub fn subscription(&self) -> Option<Uuid> {
53        let entries = self.0.split('/').collect::<Vec<_>>();
54        let first = entries.get(1)?;
55        if first != &"subscriptions" {
56            return None;
57        }
58        let id = entries.get(2)?;
59        Uuid::parse_str(id).ok()
60    }
61
62    #[must_use]
63    pub fn contains(&self, other: &Self) -> bool {
64        let first = self.0.split('/').collect::<Vec<_>>();
65        let second = other.0.split('/').collect::<Vec<_>>();
66
67        let left = Some(&first[..]);
68        let right = second.get(0..first.len());
69
70        left == right
71    }
72}
73
74impl Display for Scope {
75    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
76        write!(f, "{}", self.0)
77    }
78}
79
80impl FromStr for Scope {
81    type Err = ScopeError;
82    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
83        Self::new(s.to_string())
84    }
85}
86
87#[derive(Args)]
88#[command(about = None)]
89pub struct ScopeBuilder {
90    /// Specify scope at the subscription level
91    #[arg(long)]
92    subscription: Option<Uuid>,
93
94    /// Specify scope at the Resource Group level
95    ///
96    /// This argument requires `subscription` to be set.
97    #[arg(long, requires = "subscription")]
98    resource_group: Option<String>,
99
100    /// Specify scope at the Resource Provider level
101    ///
102    /// This argument requires `subscription` and `resource_group` to be set.
103    #[arg(long, requires = "resource_group")]
104    provider: Option<String>,
105
106    /// Specify the full scope directly
107    #[arg(long, conflicts_with = "subscription")]
108    scope: Option<Scope>,
109}
110
111impl ScopeBuilder {
112    #[must_use]
113    pub fn build(self) -> Option<Scope> {
114        let Self {
115            subscription,
116            resource_group,
117            provider,
118            scope,
119        } = self;
120
121        match (subscription, resource_group, provider, scope) {
122            (Some(subscription), Some(group), Some(provider), None) => {
123                Some(Scope::from_provider(&subscription, &group, &provider))
124            }
125            (Some(subscription), Some(group), None, None) => {
126                Some(Scope::from_resource_group(&subscription, &group))
127            }
128            (Some(subscription), None, None, None) => Some(Scope::from_subscription(&subscription)),
129            (None, None, None, Some(scope)) => Some(scope),
130            (None, None, None, None) => None,
131            _ => {
132                unreachable!("invalid combination of arguments provided");
133            }
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use crate::models::scope::Scope;
141
142    #[test]
143    fn test_contains() {
144        let with_provider = Scope("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/provider".to_string());
145        let with_rg1 = Scope(
146            "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg".to_string(),
147        );
148        let with_rg2 = Scope(
149            "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/r".to_string(),
150        );
151        let with_sub1 = Scope("/subscriptions/00000000-0000-0000-0000-000000000000".to_string());
152        let with_sub2 = Scope("/subscriptions/00000000-0000-0000-0000-000000000001".to_string());
153
154        assert!(with_rg1.contains(&with_provider));
155        assert!(with_rg1.contains(&with_rg1));
156
157        assert!(!with_provider.contains(&with_rg1));
158        assert!(!with_rg2.contains(&with_provider));
159
160        assert!(with_sub1.contains(&with_provider));
161        assert!(with_sub1.contains(&with_rg1));
162        assert!(with_sub1.contains(&with_rg2));
163        assert!(with_sub1.contains(&with_sub1));
164        assert!(!with_sub1.contains(&with_sub2));
165    }
166}