alien_core/
management_external_id.rs1use crate::{ErrorData, Result};
11use alien_error::AlienError;
12use serde::{Deserialize, Serialize};
13
14pub const MANAGEMENT_EXTERNAL_ID_MIN_LENGTH: usize = 32;
16pub const MANAGEMENT_EXTERNAL_ID_MAX_LENGTH: usize = 128;
18pub const MANAGEMENT_EXTERNAL_ID_PARAMETER_PATTERN: &str = "^$|^[A-Za-z0-9]{32,128}$";
22
23#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(try_from = "String", into = "String")]
30pub struct ManagementExternalId(String);
31
32impl ManagementExternalId {
33 pub fn new(value: impl Into<String>) -> Result<Self> {
35 let value = value.into();
36 let length_ok = (MANAGEMENT_EXTERNAL_ID_MIN_LENGTH..=MANAGEMENT_EXTERNAL_ID_MAX_LENGTH)
37 .contains(&value.len());
38 if !length_ok || !value.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
39 return Err(AlienError::new(ErrorData::ManagementExternalIdInvalid {
40 length: value.len(),
41 }));
42 }
43 Ok(Self(value))
44 }
45
46 pub fn expose(&self) -> &str {
48 &self.0
49 }
50}
51
52impl std::fmt::Debug for ManagementExternalId {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.write_str("ManagementExternalId([REDACTED])")
55 }
56}
57
58impl TryFrom<String> for ManagementExternalId {
59 type Error = AlienError<ErrorData>;
60
61 fn try_from(value: String) -> Result<Self> {
62 Self::new(value)
63 }
64}
65
66impl From<ManagementExternalId> for String {
67 fn from(value: ManagementExternalId) -> Self {
68 value.0
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 const VALUE: &str = "0123456789abcdef0123456789ABCDEF";
77
78 #[test]
79 fn accepts_the_values_the_template_parameter_accepts() {
80 let pattern = regex::Regex::new(MANAGEMENT_EXTERNAL_ID_PARAMETER_PATTERN).unwrap();
81 for candidate in [
82 VALUE.to_string(),
83 "a".repeat(128),
84 "a".repeat(31),
85 "a".repeat(129),
86 format!("{}-", &VALUE[..31]),
87 format!("{VALUE} "),
88 ] {
89 assert_eq!(
90 ManagementExternalId::new(candidate.clone()).is_ok(),
91 pattern.is_match(&candidate),
92 "validation and the CloudFormation pattern disagree on {} characters",
93 candidate.len()
94 );
95 }
96 }
97
98 #[test]
99 fn debug_and_errors_never_contain_the_value() {
100 let id = ManagementExternalId::new(VALUE).unwrap();
101 assert!(!format!("{id:?}").contains(VALUE));
102 assert!(!format!("{:?}", Some(&id)).contains(VALUE));
103
104 let rejected = format!("{VALUE}!");
105 let error = ManagementExternalId::new(rejected.clone()).unwrap_err();
106 assert!(!error.to_string().contains(&rejected));
107 assert!(!format!("{error:?}").contains(VALUE));
108 }
109
110 #[test]
111 fn round_trips_as_a_plain_string_and_validates_on_read() {
112 let id = ManagementExternalId::new(VALUE).unwrap();
113 assert_eq!(serde_json::to_value(&id).unwrap(), serde_json::json!(VALUE));
114 assert_eq!(
115 serde_json::from_value::<ManagementExternalId>(serde_json::json!(VALUE)).unwrap(),
116 id
117 );
118 assert!(
119 serde_json::from_value::<ManagementExternalId>(serde_json::json!("short")).is_err()
120 );
121 }
122}