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