1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
56//! New-type for a configurable app name.
78use aws_smithy_types::config_bag::{Storable, StoreReplace};
9use std::borrow::Cow;
10use std::error::Error;
11use std::fmt;
12use std::sync::atomic::{AtomicBool, Ordering};
1314static APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED: AtomicBool = AtomicBool::new(false);
1516/// App name that can be configured with an AWS SDK client to become part of the user agent string.
17///
18/// This name is used to identify the application in the user agent that gets sent along with requests.
19///
20/// The name may only have alphanumeric characters and any of these characters:
21/// ```text
22/// !#$%&'*+-.^_`|~
23/// ```
24/// Spaces are not allowed.
25///
26/// App names are recommended to be no more than 50 characters.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct AppName(Cow<'static, str>);
2930impl AsRef<str> for AppName {
31fn as_ref(&self) -> &str {
32&self.0
33}
34}
3536impl fmt::Display for AppName {
37fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38write!(f, "{}", self.0)
39 }
40}
4142impl Storable for AppName {
43type Storer = StoreReplace<AppName>;
44}
4546impl AppName {
47/// Creates a new app name.
48 ///
49 /// This will return an `InvalidAppName` error if the given name doesn't meet the
50 /// character requirements. See [`AppName`] for details on these requirements.
51pub fn new(app_name: impl Into<Cow<'static, str>>) -> Result<Self, InvalidAppName> {
52let app_name = app_name.into();
5354if app_name.is_empty() {
55return Err(InvalidAppName);
56 }
57fn valid_character(c: char) -> bool {
58match c {
59_ if c.is_ascii_alphanumeric() => true,
60'!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`'
61| '|' | '~' => true,
62_ => false,
63 }
64 }
65if !app_name.chars().all(valid_character) {
66return Err(InvalidAppName);
67 }
68if app_name.len() > 50 {
69if let Ok(false) = APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED.compare_exchange(
70false,
71true,
72 Ordering::Acquire,
73 Ordering::Relaxed,
74 ) {
75tracing::warn!(
76"The `app_name` set when configuring the SDK client is recommended \
77 to have no more than 50 characters."
78)
79 }
80 }
81Ok(Self(app_name))
82 }
83}
8485/// Error for when an app name doesn't meet character requirements.
86///
87/// See [`AppName`] for details on these requirements.
88#[derive(Debug)]
89#[non_exhaustive]
90pub struct InvalidAppName;
9192impl Error for InvalidAppName {}
9394impl fmt::Display for InvalidAppName {
95fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96write!(
97 f,
98"The app name can only have alphanumeric characters, or any of \
99 '!' | '#' | '$' | '%' | '&' | '\\'' | '*' | '+' | '-' | \
100 '.' | '^' | '_' | '`' | '|' | '~'"
101)
102 }
103}
104105#[cfg(test)]
106mod tests {
107use super::AppName;
108use crate::app_name::APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED;
109use std::sync::atomic::Ordering;
110111#[test]
112fn validation() {
113assert!(AppName::new("asdf1234ASDF!#$%&'*+-.^_`|~").is_ok());
114assert!(AppName::new("foo bar").is_err());
115assert!(AppName::new("🚀").is_err());
116assert!(AppName::new("").is_err());
117 }
118119#[tracing_test::traced_test]
120 #[test]
121fn log_warn_once() {
122// Pre-condition: make sure we start in the expected state of having never logged this
123assert!(!APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED.load(Ordering::Relaxed));
124125// Verify a short app name doesn't log
126AppName::new("not-long").unwrap();
127assert!(!logs_contain(
128"is recommended to have no more than 50 characters"
129));
130assert!(!APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED.load(Ordering::Relaxed));
131132// Verify a long app name logs
133AppName::new("greaterthanfiftycharactersgreaterthanfiftycharacters").unwrap();
134assert!(logs_contain(
135"is recommended to have no more than 50 characters"
136));
137assert!(APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED.load(Ordering::Relaxed));
138139// Now verify it only logs once ever
140141 // HACK: there's no way to reset tracing-test, so just
142 // reach into its internals and clear it manually
143tracing_test::internal::global_buf().lock().unwrap().clear();
144145 AppName::new("greaterthanfiftycharactersgreaterthanfiftycharacters").unwrap();
146assert!(!logs_contain(
147"is recommended to have no more than 50 characters"
148));
149 }
150}