use crate::{ErrorData, Result};
use alien_error::AlienError;
use serde::{Deserialize, Serialize};
pub const MANAGEMENT_EXTERNAL_ID_MIN_LENGTH: usize = 32;
pub const MANAGEMENT_EXTERNAL_ID_MAX_LENGTH: usize = 128;
pub const MANAGEMENT_EXTERNAL_ID_PARAMETER_PATTERN: &str = "^$|^[A-Za-z0-9]{32,128}$";
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ManagementExternalId(String);
impl ManagementExternalId {
pub fn new(value: impl Into<String>) -> Result<Self> {
let value = value.into();
let length_ok = (MANAGEMENT_EXTERNAL_ID_MIN_LENGTH..=MANAGEMENT_EXTERNAL_ID_MAX_LENGTH)
.contains(&value.len());
if !length_ok || !value.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
return Err(AlienError::new(ErrorData::ManagementExternalIdInvalid {
length: value.len(),
}));
}
Ok(Self(value))
}
pub fn expose(&self) -> &str {
&self.0
}
}
impl std::fmt::Debug for ManagementExternalId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("ManagementExternalId([REDACTED])")
}
}
impl TryFrom<String> for ManagementExternalId {
type Error = AlienError<ErrorData>;
fn try_from(value: String) -> Result<Self> {
Self::new(value)
}
}
impl From<ManagementExternalId> for String {
fn from(value: ManagementExternalId) -> Self {
value.0
}
}
#[cfg(test)]
mod tests {
use super::*;
const VALUE: &str = "0123456789abcdef0123456789ABCDEF";
#[test]
fn accepts_the_values_the_template_parameter_accepts() {
let pattern = regex::Regex::new(MANAGEMENT_EXTERNAL_ID_PARAMETER_PATTERN).unwrap();
for candidate in [
VALUE.to_string(),
"a".repeat(128),
"a".repeat(31),
"a".repeat(129),
format!("{}-", &VALUE[..31]),
format!("{VALUE} "),
] {
assert_eq!(
ManagementExternalId::new(candidate.clone()).is_ok(),
pattern.is_match(&candidate),
"validation and the CloudFormation pattern disagree on {} characters",
candidate.len()
);
}
}
#[test]
fn debug_and_errors_never_contain_the_value() {
let id = ManagementExternalId::new(VALUE).unwrap();
assert!(!format!("{id:?}").contains(VALUE));
assert!(!format!("{:?}", Some(&id)).contains(VALUE));
let rejected = format!("{VALUE}!");
let error = ManagementExternalId::new(rejected.clone()).unwrap_err();
assert!(!error.to_string().contains(&rejected));
assert!(!format!("{error:?}").contains(VALUE));
}
#[test]
fn round_trips_as_a_plain_string_and_validates_on_read() {
let id = ManagementExternalId::new(VALUE).unwrap();
assert_eq!(serde_json::to_value(&id).unwrap(), serde_json::json!(VALUE));
assert_eq!(
serde_json::from_value::<ManagementExternalId>(serde_json::json!(VALUE)).unwrap(),
id
);
assert!(
serde_json::from_value::<ManagementExternalId>(serde_json::json!("short")).is_err()
);
}
}