1use aws_smithy_types::config_bag::{Storable, StoreReplace};
9use std::borrow::Cow;
10use std::error::Error;
11use std::fmt;
12use std::sync::atomic::{AtomicBool, Ordering};
13
14static APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED: AtomicBool = AtomicBool::new(false);
15
16#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct AppName(Cow<'static, str>);
29
30impl AsRef<str> for AppName {
31 fn as_ref(&self) -> &str {
32 &self.0
33 }
34}
35
36impl fmt::Display for AppName {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 write!(f, "{}", self.0)
39 }
40}
41
42impl Storable for AppName {
43 type Storer = StoreReplace<AppName>;
44}
45
46impl AppName {
47 pub fn new(app_name: impl Into<Cow<'static, str>>) -> Result<Self, InvalidAppName> {
52 let app_name = app_name.into();
53
54 if app_name.is_empty() {
55 return Err(InvalidAppName);
56 }
57 fn valid_character(c: char) -> bool {
58 match c {
59 _ if c.is_ascii_alphanumeric() => true,
60 '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`'
61 | '|' | '~' => true,
62 _ => false,
63 }
64 }
65 if !app_name.chars().all(valid_character) {
66 return Err(InvalidAppName);
67 }
68 if app_name.len() > 50 {
69 if let Ok(false) = APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED.compare_exchange(
70 false,
71 true,
72 Ordering::Acquire,
73 Ordering::Relaxed,
74 ) {
75 tracing::warn!(
76 "The `app_name` set when configuring the SDK client is recommended \
77 to have no more than 50 characters."
78 )
79 }
80 }
81 Ok(Self(app_name))
82 }
83}
84
85#[derive(Debug)]
89#[non_exhaustive]
90pub struct InvalidAppName;
91
92impl Error for InvalidAppName {}
93
94impl fmt::Display for InvalidAppName {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 write!(
97 f,
98 "The app name can only have alphanumeric characters, or any of \
99 '!' | '#' | '$' | '%' | '&' | '\\'' | '*' | '+' | '-' | \
100 '.' | '^' | '_' | '`' | '|' | '~'"
101 )
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::AppName;
108 use crate::app_name::APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED;
109 use std::sync::atomic::Ordering;
110
111 #[test]
112 fn validation() {
113 assert!(AppName::new("asdf1234ASDF!#$%&'*+-.^_`|~").is_ok());
114 assert!(AppName::new("foo bar").is_err());
115 assert!(AppName::new("🚀").is_err());
116 assert!(AppName::new("").is_err());
117 }
118
119 #[tracing_test::traced_test]
120 #[test]
121 fn log_warn_once() {
122 assert!(!APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED.load(Ordering::Relaxed));
124
125 AppName::new("not-long").unwrap();
127 assert!(!logs_contain(
128 "is recommended to have no more than 50 characters"
129 ));
130 assert!(!APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED.load(Ordering::Relaxed));
131
132 AppName::new("greaterthanfiftycharactersgreaterthanfiftycharacters").unwrap();
134 assert!(logs_contain(
135 "is recommended to have no more than 50 characters"
136 ));
137 assert!(APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED.load(Ordering::Relaxed));
138
139 tracing_test::internal::global_buf().lock().unwrap().clear();
144
145 AppName::new("greaterthanfiftycharactersgreaterthanfiftycharacters").unwrap();
146 assert!(!logs_contain(
147 "is recommended to have no more than 50 characters"
148 ));
149 }
150}