Skip to main content

wami_core/arn/
builder.rs

1//! Fluent builder for constructing WAMI ARNs.
2
3use super::types::{CloudMapping, Resource, Service, TenantPath, WamiArn};
4use crate::error::{AmiError, Result};
5
6/// A fluent builder for constructing WAMI ARNs.
7///
8/// # Examples
9///
10/// ## Building a WAMI-native ARN
11///
12/// ```
13/// use wami_core::arn::{WamiArn, Service};
14///
15/// let arn = WamiArn::builder()
16///     .service(Service::Iam)
17///     .tenant_hierarchy(vec![12345678, 87654321, 99999999])
18///     .wami_instance("999888777")
19///     .resource("user", "77557755")
20///     .build()
21///     .unwrap();
22///
23/// assert_eq!(
24///     arn.to_string(),
25///     "arn:wami:iam:12345678/87654321/99999999:wami:999888777:user/77557755"
26/// );
27/// ```
28///
29/// ## Building a cloud-synced ARN
30///
31/// ```
32/// use wami_core::arn::{WamiArn, Service};
33///
34/// let arn = WamiArn::builder()
35///     .service(Service::Iam)
36///     .tenant_hierarchy(vec![12345678, 87654321, 99999999])
37///     .wami_instance("999888777")
38///     .cloud_provider("aws", "223344556677")
39///     .resource("user", "77557755")
40///     .build()
41///     .unwrap();
42///
43/// assert_eq!(
44///     arn.to_string(),
45///     "arn:wami:iam:12345678/87654321/99999999:wami:999888777:aws:223344556677:global:user/77557755"
46/// );
47/// ```
48#[derive(Debug, Default)]
49pub struct ArnBuilder {
50    service: Option<Service>,
51    tenant_path: Option<TenantPath>,
52    wami_instance_id: Option<String>,
53    cloud_mapping: Option<CloudMapping>,
54    resource: Option<Resource>,
55}
56
57impl ArnBuilder {
58    /// Creates a new ARN builder.
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Sets the service.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use wami_core::arn::{WamiArn, Service};
69    ///
70    /// let builder = WamiArn::builder().service(Service::Iam);
71    /// ```
72    pub fn service(mut self, service: Service) -> Self {
73        self.service = Some(service);
74        self
75    }
76
77    /// Sets the service from a string.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use wami_core::arn::WamiArn;
83    ///
84    /// let builder = WamiArn::builder().service_str("iam");
85    /// ```
86    pub fn service_str(mut self, service: impl Into<String>) -> Self {
87        self.service = Some(Service::from(service.into().as_str()));
88        self
89    }
90
91    /// Sets the tenant path.
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use wami_core::arn::{WamiArn, TenantPath};
97    ///
98    /// let path = TenantPath::new(vec![12345678, 87654321]);
99    /// let builder = WamiArn::builder().tenant_path(path);
100    /// ```
101    pub fn tenant_path(mut self, path: TenantPath) -> Self {
102        self.tenant_path = Some(path);
103        self
104    }
105
106    /// Sets the tenant hierarchy from a vector of numeric tenant ID segments.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use wami_core::arn::WamiArn;
112    ///
113    /// let builder = WamiArn::builder()
114    ///     .tenant_hierarchy(vec![12345678, 87654321, 99999999]);
115    /// ```
116    pub fn tenant_hierarchy(mut self, segments: Vec<u64>) -> Self {
117        self.tenant_path = Some(TenantPath::new(segments));
118        self
119    }
120
121    /// Sets a single tenant (non-hierarchical) using a numeric tenant ID.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use wami_core::arn::WamiArn;
127    ///
128    /// let builder = WamiArn::builder().tenant(12345678);
129    /// ```
130    pub fn tenant(mut self, tenant_id: u64) -> Self {
131        self.tenant_path = Some(TenantPath::single(tenant_id));
132        self
133    }
134
135    /// Sets the WAMI instance ID.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use wami_core::arn::WamiArn;
141    ///
142    /// let builder = WamiArn::builder().wami_instance("999888777");
143    /// ```
144    pub fn wami_instance(mut self, instance_id: impl Into<String>) -> Self {
145        self.wami_instance_id = Some(instance_id.into());
146        self
147    }
148
149    /// Sets the cloud provider mapping without a region (global resource).
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use wami_core::arn::WamiArn;
155    ///
156    /// let builder = WamiArn::builder()
157    ///     .cloud_provider("aws", "223344556677");
158    /// ```
159    pub fn cloud_provider(
160        mut self,
161        provider: impl Into<String>,
162        account_id: impl Into<String>,
163    ) -> Self {
164        self.cloud_mapping = Some(CloudMapping::new(provider, account_id));
165        self
166    }
167
168    /// Sets the cloud provider mapping with a specific region.
169    ///
170    /// # Examples
171    ///
172    /// ```
173    /// use wami_core::arn::WamiArn;
174    ///
175    /// let builder = WamiArn::builder()
176    ///     .cloud_provider_with_region("aws", "223344556677", "us-east-1");
177    /// ```
178    pub fn cloud_provider_with_region(
179        mut self,
180        provider: impl Into<String>,
181        account_id: impl Into<String>,
182        region: impl Into<String>,
183    ) -> Self {
184        self.cloud_mapping = Some(CloudMapping::with_region(provider, account_id, region));
185        self
186    }
187
188    /// Sets the region for the current cloud mapping.
189    /// If no cloud mapping exists, this does nothing.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// use wami_core::arn::WamiArn;
195    ///
196    /// let builder = WamiArn::builder()
197    ///     .cloud_provider("aws", "223344556677")
198    ///     .region("us-east-1");
199    /// ```
200    pub fn region(mut self, region: impl Into<String>) -> Self {
201        if let Some(ref mut mapping) = self.cloud_mapping {
202            mapping.region = Some(region.into());
203        }
204        self
205    }
206
207    /// Sets the cloud mapping directly.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use wami_core::arn::{WamiArn, CloudMapping};
213    ///
214    /// let mapping = CloudMapping::new("gcp", "554433221");
215    /// let builder = WamiArn::builder().cloud_mapping(mapping);
216    /// ```
217    pub fn cloud_mapping(mut self, mapping: CloudMapping) -> Self {
218        self.cloud_mapping = Some(mapping);
219        self
220    }
221
222    /// Removes any cloud mapping (creates a WAMI-native ARN).
223    ///
224    /// # Examples
225    ///
226    /// ```
227    /// use wami_core::arn::WamiArn;
228    ///
229    /// let builder = WamiArn::builder()
230    ///     .cloud_provider("aws", "123456")
231    ///     .no_cloud_mapping();
232    /// ```
233    pub fn no_cloud_mapping(mut self) -> Self {
234        self.cloud_mapping = None;
235        self
236    }
237
238    /// Sets the resource.
239    ///
240    /// # Examples
241    ///
242    /// ```
243    /// use wami_core::arn::{WamiArn, Resource};
244    ///
245    /// let resource = Resource::new("user", "77557755");
246    /// let builder = WamiArn::builder().resource_obj(resource);
247    /// ```
248    pub fn resource_obj(mut self, resource: Resource) -> Self {
249        self.resource = Some(resource);
250        self
251    }
252
253    /// Sets the resource from type and ID.
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use wami_core::arn::WamiArn;
259    ///
260    /// let builder = WamiArn::builder().resource("user", "77557755");
261    /// ```
262    pub fn resource(
263        mut self,
264        resource_type: impl Into<String>,
265        resource_id: impl Into<String>,
266    ) -> Self {
267        self.resource = Some(Resource::new(resource_type, resource_id));
268        self
269    }
270
271    /// Builds the ARN, returning an error if any required fields are missing.
272    ///
273    /// # Errors
274    ///
275    /// Returns an error if any of the following fields are not set:
276    /// - service
277    /// - tenant_path
278    /// - wami_instance_id
279    /// - resource
280    ///
281    /// # Examples
282    ///
283    /// ```
284    /// use wami_core::arn::{WamiArn, Service};
285    ///
286    /// let result = WamiArn::builder()
287    ///     .service(Service::Iam)
288    ///     .tenant(12345678)
289    ///     .wami_instance("999888777")
290    ///     .resource("user", "77557755")
291    ///     .build();
292    ///
293    /// assert!(result.is_ok());
294    /// ```
295    #[allow(clippy::result_large_err)]
296    pub fn build(self) -> Result<WamiArn> {
297        let service = self.service.ok_or_else(|| AmiError::InvalidParameter {
298            message: "ARN builder: service is required".to_string(),
299        })?;
300
301        let tenant_path = self.tenant_path.ok_or_else(|| AmiError::InvalidParameter {
302            message: "ARN builder: tenant_path is required".to_string(),
303        })?;
304
305        let wami_instance_id = self
306            .wami_instance_id
307            .ok_or_else(|| AmiError::InvalidParameter {
308                message: "ARN builder: wami_instance_id is required".to_string(),
309            })?;
310
311        let resource = self.resource.ok_or_else(|| AmiError::InvalidParameter {
312            message: "ARN builder: resource is required".to_string(),
313        })?;
314
315        // Validate tenant path is not empty
316        if tenant_path.segments.is_empty() {
317            return Err(AmiError::InvalidParameter {
318                message: "ARN builder: tenant_path cannot be empty".to_string(),
319            });
320        }
321
322        // Validate wami_instance_id is not empty
323        if wami_instance_id.is_empty() {
324            return Err(AmiError::InvalidParameter {
325                message: "ARN builder: wami_instance_id cannot be empty".to_string(),
326            });
327        }
328
329        // Validate resource type and ID are not empty
330        if resource.resource_type.is_empty() {
331            return Err(AmiError::InvalidParameter {
332                message: "ARN builder: resource_type cannot be empty".to_string(),
333            });
334        }
335
336        if resource.resource_id.is_empty() {
337            return Err(AmiError::InvalidParameter {
338                message: "ARN builder: resource_id cannot be empty".to_string(),
339            });
340        }
341
342        Ok(WamiArn {
343            service,
344            tenant_path,
345            wami_instance_id,
346            cloud_mapping: self.cloud_mapping,
347            resource,
348        })
349    }
350}
351
352impl WamiArn {
353    /// Creates a new ARN builder.
354    ///
355    /// # Examples
356    ///
357    /// ```
358    /// use wami_core::arn::{WamiArn, Service};
359    ///
360    /// let arn = WamiArn::builder()
361    ///     .service(Service::Iam)
362    ///     .tenant(12345678)
363    ///     .wami_instance("999888777")
364    ///     .resource("user", "77557755")
365    ///     .build()
366    ///     .unwrap();
367    /// ```
368    pub fn builder() -> ArnBuilder {
369        ArnBuilder::new()
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn test_builder_wami_native() {
379        let arn = WamiArn::builder()
380            .service(Service::Iam)
381            .tenant_hierarchy(vec![12345678, 87654321, 99999999])
382            .wami_instance("999888777")
383            .resource("user", "77557755")
384            .build()
385            .unwrap();
386
387        assert_eq!(arn.service, Service::Iam);
388        assert_eq!(arn.tenant_path.segments, vec![12345678, 87654321, 99999999]);
389        assert_eq!(arn.wami_instance_id, "999888777");
390        assert_eq!(arn.cloud_mapping, None);
391        assert_eq!(arn.resource.resource_type, "user");
392        assert_eq!(arn.resource.resource_id, "77557755");
393        assert_eq!(
394            arn.to_string(),
395            "arn:wami:iam:12345678/87654321/99999999:wami:999888777:user/77557755"
396        );
397    }
398
399    #[test]
400    fn test_builder_cloud_synced() {
401        let arn = WamiArn::builder()
402            .service(Service::Iam)
403            .tenant_hierarchy(vec![12345678, 87654321, 99999999])
404            .wami_instance("999888777")
405            .cloud_provider("aws", "223344556677")
406            .resource("user", "77557755")
407            .build()
408            .unwrap();
409
410        assert_eq!(arn.service, Service::Iam);
411        assert_eq!(arn.cloud_mapping.as_ref().unwrap().provider, "aws");
412        assert_eq!(
413            arn.cloud_mapping.as_ref().unwrap().account_id,
414            "223344556677"
415        );
416        assert_eq!(arn.cloud_mapping.as_ref().unwrap().region, None);
417        assert_eq!(
418            arn.to_string(),
419            "arn:wami:iam:12345678/87654321/99999999:wami:999888777:aws:223344556677:global:user/77557755"
420        );
421    }
422
423    #[test]
424    fn test_builder_cloud_synced_with_region() {
425        let arn = WamiArn::builder()
426            .service(Service::Iam)
427            .tenant_hierarchy(vec![12345678, 87654321, 99999999])
428            .wami_instance("999888777")
429            .cloud_provider_with_region("aws", "223344556677", "us-east-1")
430            .resource("user", "77557755")
431            .build()
432            .unwrap();
433
434        assert_eq!(
435            arn.cloud_mapping.as_ref().unwrap().region,
436            Some("us-east-1".to_string())
437        );
438        assert_eq!(
439            arn.to_string(),
440            "arn:wami:iam:12345678/87654321/99999999:wami:999888777:aws:223344556677:us-east-1:user/77557755"
441        );
442    }
443
444    #[test]
445    fn test_builder_region_method() {
446        let arn = WamiArn::builder()
447            .service(Service::Iam)
448            .tenant(12345678)
449            .wami_instance("999888777")
450            .cloud_provider("aws", "223344556677")
451            .region("eu-west-1")
452            .resource("user", "77557755")
453            .build()
454            .unwrap();
455
456        assert_eq!(
457            arn.cloud_mapping.as_ref().unwrap().region,
458            Some("eu-west-1".to_string())
459        );
460    }
461
462    #[test]
463    fn test_builder_single_tenant() {
464        let arn = WamiArn::builder()
465            .service(Service::Sts)
466            .tenant(12345678)
467            .wami_instance("111222333")
468            .resource("session", "sess123")
469            .build()
470            .unwrap();
471
472        assert_eq!(arn.tenant_path.segments, vec![12345678]);
473        assert_eq!(
474            arn.to_string(),
475            "arn:wami:sts:12345678:wami:111222333:session/sess123"
476        );
477    }
478
479    #[test]
480    fn test_builder_service_str() {
481        let arn = WamiArn::builder()
482            .service_str("iam")
483            .tenant(12345678)
484            .wami_instance("999888777")
485            .resource("policy", "pol123")
486            .build()
487            .unwrap();
488
489        assert_eq!(arn.service, Service::Iam);
490    }
491
492    #[test]
493    fn test_builder_custom_service() {
494        let arn = WamiArn::builder()
495            .service_str("custom-service")
496            .tenant(12345678)
497            .wami_instance("999888777")
498            .resource("resource", "res123")
499            .build()
500            .unwrap();
501
502        assert_eq!(arn.service, Service::Custom("custom-service".to_string()));
503        assert_eq!(
504            arn.to_string(),
505            "arn:wami:custom-service:12345678:wami:999888777:resource/res123"
506        );
507    }
508
509    #[test]
510    fn test_builder_no_cloud_mapping() {
511        let arn = WamiArn::builder()
512            .service(Service::Iam)
513            .tenant(12345678)
514            .wami_instance("999888777")
515            .cloud_provider("aws", "123456")
516            .no_cloud_mapping()
517            .resource("user", "77557755")
518            .build()
519            .unwrap();
520
521        assert_eq!(arn.cloud_mapping, None);
522    }
523
524    #[test]
525    fn test_builder_missing_service() {
526        let result = WamiArn::builder()
527            .tenant(12345678)
528            .wami_instance("999888777")
529            .resource("user", "77557755")
530            .build();
531
532        assert!(result.is_err());
533        assert!(result
534            .unwrap_err()
535            .to_string()
536            .contains("service is required"));
537    }
538
539    #[test]
540    fn test_builder_missing_tenant() {
541        let result = WamiArn::builder()
542            .service(Service::Iam)
543            .wami_instance("999888777")
544            .resource("user", "77557755")
545            .build();
546
547        assert!(result.is_err());
548        assert!(result
549            .unwrap_err()
550            .to_string()
551            .contains("tenant_path is required"));
552    }
553
554    #[test]
555    fn test_builder_missing_instance() {
556        let result = WamiArn::builder()
557            .service(Service::Iam)
558            .tenant(12345678)
559            .resource("user", "77557755")
560            .build();
561
562        assert!(result.is_err());
563        assert!(result
564            .unwrap_err()
565            .to_string()
566            .contains("wami_instance_id is required"));
567    }
568
569    #[test]
570    fn test_builder_missing_resource() {
571        let result = WamiArn::builder()
572            .service(Service::Iam)
573            .tenant(12345678)
574            .wami_instance("999888777")
575            .build();
576
577        assert!(result.is_err());
578        assert!(result
579            .unwrap_err()
580            .to_string()
581            .contains("resource is required"));
582    }
583
584    #[test]
585    fn test_builder_empty_tenant_path() {
586        let result = WamiArn::builder()
587            .service(Service::Iam)
588            .tenant_path(TenantPath::new(vec![]))
589            .wami_instance("999888777")
590            .resource("user", "77557755")
591            .build();
592
593        assert!(result.is_err());
594        assert!(result
595            .unwrap_err()
596            .to_string()
597            .contains("tenant_path cannot be empty"));
598    }
599
600    #[test]
601    fn test_builder_resource_obj() {
602        let resource = Resource::new("role", "role123");
603        let arn = WamiArn::builder()
604            .service(Service::Iam)
605            .tenant(12345678)
606            .wami_instance("999888777")
607            .resource_obj(resource)
608            .build()
609            .unwrap();
610
611        assert_eq!(arn.resource.resource_type, "role");
612        assert_eq!(arn.resource.resource_id, "role123");
613    }
614
615    #[test]
616    fn test_builder_cloud_mapping_obj() {
617        let mapping = CloudMapping::new("gcp", "554433221");
618        let arn = WamiArn::builder()
619            .service(Service::Iam)
620            .tenant(12345678)
621            .wami_instance("999888777")
622            .cloud_mapping(mapping)
623            .resource("user", "77557755")
624            .build()
625            .unwrap();
626
627        assert_eq!(arn.cloud_mapping.as_ref().unwrap().provider, "gcp");
628        assert_eq!(arn.cloud_mapping.as_ref().unwrap().account_id, "554433221");
629    }
630}