azure_pim_cli/models/
scope.rs1use 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 #[arg(long)]
92 subscription: Option<Uuid>,
93
94 #[arg(long, requires = "subscription")]
98 resource_group: Option<String>,
99
100 #[arg(long, requires = "resource_group")]
104 provider: Option<String>,
105
106 #[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}