Skip to main content

aws_runtime/
user_agent.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use aws_smithy_types::config_bag::{Storable, StoreReplace};
7use aws_types::app_name::AppName;
8use aws_types::build_metadata::{OsFamily, BUILD_METADATA};
9use aws_types::os_shim_internal::Env;
10use std::borrow::Cow;
11use std::error::Error;
12use std::fmt;
13
14mod interceptor;
15mod metrics;
16#[cfg(feature = "test-util")]
17pub mod test_util;
18
19const USER_AGENT_VERSION: &str = "2.1";
20
21use crate::user_agent::metrics::BusinessMetrics;
22pub use interceptor::UserAgentInterceptor;
23pub use metrics::BusinessMetric;
24
25/// AWS User Agent
26///
27/// Ths struct should be inserted into the [`ConfigBag`](aws_smithy_types::config_bag::ConfigBag)
28/// during operation construction. The `UserAgentInterceptor` reads `AwsUserAgent`
29/// from the config bag and sets the `User-Agent` and `x-amz-user-agent` headers.
30#[derive(Clone, Debug)]
31pub struct AwsUserAgent {
32    sdk_metadata: SdkMetadata,
33    ua_metadata: UaMetadata,
34    api_metadata: ApiMetadata,
35    os_metadata: OsMetadata,
36    language_metadata: LanguageMetadata,
37    exec_env_metadata: Option<ExecEnvMetadata>,
38    business_metrics: BusinessMetrics,
39    framework_metadata: Vec<FrameworkMetadata>,
40    app_name: Option<AppName>,
41    build_env_additional_metadata: Option<AdditionalMetadata>,
42    additional_metadata: Vec<AdditionalMetadata>,
43}
44
45impl AwsUserAgent {
46    /// Load a User Agent configuration from the environment
47    ///
48    /// This utilizes [`BUILD_METADATA`](const@aws_types::build_metadata::BUILD_METADATA) from `aws_types`
49    /// to capture the Rust version & target platform. `ApiMetadata` provides
50    /// the version & name of the specific service.
51    pub fn new_from_environment(env: Env, api_metadata: ApiMetadata) -> Self {
52        let build_metadata = &BUILD_METADATA;
53        let sdk_metadata = SdkMetadata {
54            name: "rust",
55            version: build_metadata.core_pkg_version,
56        };
57        let ua_metadata = UaMetadata {
58            version: USER_AGENT_VERSION,
59        };
60        let os_metadata = OsMetadata {
61            os_family: &build_metadata.os_family,
62            version: None,
63        };
64        let exec_env_metadata = env
65            .get("AWS_EXECUTION_ENV")
66            .ok()
67            .map(|name| ExecEnvMetadata { name });
68
69        // Retrieve additional metadata at compile-time from the AWS_SDK_RUST_BUILD_UA_METADATA env var
70        let build_env_additional_metadata = option_env!("AWS_SDK_RUST_BUILD_UA_METADATA")
71            .and_then(|value| AdditionalMetadata::new(value).ok());
72
73        AwsUserAgent {
74            sdk_metadata,
75            ua_metadata,
76            api_metadata,
77            os_metadata,
78            language_metadata: LanguageMetadata {
79                lang: "rust",
80                version: BUILD_METADATA.rust_version,
81                extras: Default::default(),
82            },
83            exec_env_metadata,
84            framework_metadata: Default::default(),
85            business_metrics: Default::default(),
86            app_name: Default::default(),
87            build_env_additional_metadata,
88            additional_metadata: Default::default(),
89        }
90    }
91
92    /// For test purposes, construct an environment-independent User Agent
93    ///
94    /// Without this, running CI on a different platform would produce different user agent strings
95    pub fn for_tests() -> Self {
96        Self {
97            sdk_metadata: SdkMetadata {
98                name: "rust",
99                version: "0.123.test",
100            },
101            ua_metadata: UaMetadata { version: "0.1" },
102            api_metadata: ApiMetadata {
103                service_id: "test-service".into(),
104                version: "0.123",
105            },
106            os_metadata: OsMetadata {
107                os_family: &OsFamily::Windows,
108                version: Some("XPSP3".to_string()),
109            },
110            language_metadata: LanguageMetadata {
111                lang: "rust",
112                version: "1.50.0",
113                extras: Default::default(),
114            },
115            exec_env_metadata: None,
116            business_metrics: Default::default(),
117            framework_metadata: Vec::new(),
118            app_name: None,
119            build_env_additional_metadata: None,
120            additional_metadata: Vec::new(),
121        }
122    }
123
124    #[deprecated(
125        since = "1.4.0",
126        note = "This is a no-op; use `with_business_metric` instead."
127    )]
128    #[allow(unused_mut)]
129    #[allow(deprecated)]
130    #[doc(hidden)]
131    /// Adds feature metadata to the user agent.
132    pub fn with_feature_metadata(mut self, _metadata: FeatureMetadata) -> Self {
133        self
134    }
135
136    #[deprecated(
137        since = "1.4.0",
138        note = "This is a no-op; use `add_business_metric` instead."
139    )]
140    #[allow(deprecated)]
141    #[allow(unused_mut)]
142    #[doc(hidden)]
143    /// Adds feature metadata to the user agent.
144    pub fn add_feature_metadata(&mut self, _metadata: FeatureMetadata) -> &mut Self {
145        self
146    }
147
148    #[deprecated(
149        since = "1.4.0",
150        note = "This is a no-op; use `with_business_metric` instead."
151    )]
152    #[allow(deprecated)]
153    #[allow(unused_mut)]
154    #[doc(hidden)]
155    /// Adds config metadata to the user agent.
156    pub fn with_config_metadata(mut self, _metadata: ConfigMetadata) -> Self {
157        self
158    }
159
160    #[deprecated(
161        since = "1.4.0",
162        note = "This is a no-op; use `add_business_metric` instead."
163    )]
164    #[allow(deprecated)]
165    #[allow(unused_mut)]
166    #[doc(hidden)]
167    /// Adds config metadata to the user agent.
168    pub fn add_config_metadata(&mut self, _metadata: ConfigMetadata) -> &mut Self {
169        self
170    }
171
172    #[doc(hidden)]
173    /// Adds business metric to the user agent.
174    pub fn with_business_metric(mut self, metric: BusinessMetric) -> Self {
175        self.business_metrics.push(metric);
176        self
177    }
178
179    #[doc(hidden)]
180    /// Adds business metric to the user agent.
181    pub fn add_business_metric(&mut self, metric: BusinessMetric) -> &mut Self {
182        self.business_metrics.push(metric);
183        self
184    }
185
186    #[doc(hidden)]
187    /// Adds framework metadata to the user agent.
188    pub fn with_framework_metadata(mut self, metadata: FrameworkMetadata) -> Self {
189        self.framework_metadata.push(metadata);
190        self
191    }
192
193    #[doc(hidden)]
194    /// Adds framework metadata to the user agent.
195    pub fn add_framework_metadata(&mut self, metadata: FrameworkMetadata) -> &mut Self {
196        self.framework_metadata.push(metadata);
197        self
198    }
199
200    /// Adds additional metadata to the user agent.
201    pub fn with_additional_metadata(mut self, metadata: AdditionalMetadata) -> Self {
202        self.additional_metadata.push(metadata);
203        self
204    }
205
206    /// Adds additional metadata to the user agent.
207    pub fn add_additional_metadata(&mut self, metadata: AdditionalMetadata) -> &mut Self {
208        self.additional_metadata.push(metadata);
209        self
210    }
211
212    /// Sets the app name for the user agent.
213    pub fn with_app_name(mut self, app_name: AppName) -> Self {
214        self.app_name = Some(app_name);
215        self
216    }
217
218    /// Sets the app name for the user agent.
219    pub fn set_app_name(&mut self, app_name: AppName) -> &mut Self {
220        self.app_name = Some(app_name);
221        self
222    }
223
224    /// Generate a new-style user agent style header
225    ///
226    /// This header should be set at `x-amz-user-agent`
227    pub fn aws_ua_header(&self) -> String {
228        /*
229        ABNF for the user agent (see the bottom of the file for complete ABNF):
230        ua-string = sdk-metadata RWS
231                    ua-metadata RWS
232                    [api-metadata RWS]
233                    os-metadata RWS
234                    language-metadata RWS
235                    [env-metadata RWS]
236                        ; ordering is not strictly required in the following section
237                    [business-metrics]
238                    [appId]
239                    *(framework-metadata RWS)
240        */
241        let mut ua_value = String::new();
242        use std::fmt::Write;
243        // unwrap calls should never fail because string formatting will always succeed.
244        write!(ua_value, "{} ", &self.sdk_metadata).unwrap();
245        write!(ua_value, "{} ", &self.ua_metadata).unwrap();
246        write!(ua_value, "{} ", &self.api_metadata).unwrap();
247        write!(ua_value, "{} ", &self.os_metadata).unwrap();
248        write!(ua_value, "{} ", &self.language_metadata).unwrap();
249        if let Some(ref env_meta) = self.exec_env_metadata {
250            write!(ua_value, "{env_meta} ").unwrap();
251        }
252        if !self.business_metrics.is_empty() {
253            write!(ua_value, "{} ", &self.business_metrics).unwrap()
254        }
255        for framework in &self.framework_metadata {
256            write!(ua_value, "{framework} ").unwrap();
257        }
258        for additional_metadata in &self.additional_metadata {
259            write!(ua_value, "{additional_metadata} ").unwrap();
260        }
261        if let Some(app_name) = &self.app_name {
262            write!(ua_value, "app/{app_name}").unwrap();
263        }
264        if let Some(additional_metadata) = &self.build_env_additional_metadata {
265            write!(ua_value, "{additional_metadata}").unwrap();
266        }
267        if ua_value.ends_with(' ') {
268            ua_value.truncate(ua_value.len() - 1);
269        }
270        ua_value
271    }
272
273    /// Generate an old-style User-Agent header
274    ///
275    /// This header is intended to be set at `User-Agent`
276    pub fn ua_header(&self) -> String {
277        self.aws_ua_header()
278    }
279}
280
281impl Storable for AwsUserAgent {
282    type Storer = StoreReplace<Self>;
283}
284
285#[derive(Clone, Copy, Debug)]
286struct SdkMetadata {
287    name: &'static str,
288    version: &'static str,
289}
290
291impl fmt::Display for SdkMetadata {
292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        write!(f, "aws-sdk-{}/{}", self.name, self.version)
294    }
295}
296
297#[derive(Clone, Copy, Debug)]
298struct UaMetadata {
299    version: &'static str,
300}
301
302impl fmt::Display for UaMetadata {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        write!(f, "ua/{}", self.version)
305    }
306}
307
308/// Metadata about the client that's making the call.
309#[derive(Clone, Debug)]
310pub struct ApiMetadata {
311    service_id: Cow<'static, str>,
312    version: &'static str,
313}
314
315impl ApiMetadata {
316    /// Creates new `ApiMetadata`.
317    pub const fn new(service_id: &'static str, version: &'static str) -> Self {
318        Self {
319            service_id: Cow::Borrowed(service_id),
320            version,
321        }
322    }
323}
324
325impl fmt::Display for ApiMetadata {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        write!(f, "api/{}/{}", self.service_id, self.version)
328    }
329}
330
331impl Storable for ApiMetadata {
332    type Storer = StoreReplace<Self>;
333}
334
335/// Error for when an user agent metadata doesn't meet character requirements.
336///
337/// Metadata may only have alphanumeric characters and any of these characters:
338/// ```text
339/// !#$%&'*+-.^_`|~
340/// ```
341/// Spaces are not allowed.
342#[derive(Debug)]
343#[non_exhaustive]
344pub struct InvalidMetadataValue;
345
346impl Error for InvalidMetadataValue {}
347
348impl fmt::Display for InvalidMetadataValue {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        write!(
351            f,
352            "User agent metadata can only have alphanumeric characters, or any of \
353             '!' |  '#' |  '$' |  '%' |  '&' |  '\\'' |  '*' |  '+' |  '-' | \
354             '.' |  '^' |  '_' |  '`' |  '|' |  '~'"
355        )
356    }
357}
358
359fn validate_metadata(value: Cow<'static, str>) -> Result<Cow<'static, str>, InvalidMetadataValue> {
360    fn valid_character(c: char) -> bool {
361        match c {
362            _ if c.is_ascii_alphanumeric() => true,
363            '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|'
364            | '~' => true,
365            _ => false,
366        }
367    }
368    if !value.chars().all(valid_character) {
369        return Err(InvalidMetadataValue);
370    }
371    Ok(value)
372}
373
374#[doc(hidden)]
375/// Additional metadata that can be bundled with framework or feature metadata.
376#[derive(Clone, Debug)]
377#[non_exhaustive]
378pub struct AdditionalMetadata {
379    value: Cow<'static, str>,
380}
381
382impl AdditionalMetadata {
383    /// Creates `AdditionalMetadata`.
384    ///
385    /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or
386    /// has characters other than the following:
387    /// ```text
388    /// !#$%&'*+-.^_`|~
389    /// ```
390    pub fn new(value: impl Into<Cow<'static, str>>) -> Result<Self, InvalidMetadataValue> {
391        Ok(Self {
392            value: validate_metadata(value.into())?,
393        })
394    }
395}
396
397impl fmt::Display for AdditionalMetadata {
398    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399        // additional-metadata = "md/" ua-pair
400        write!(f, "md/{}", self.value)
401    }
402}
403
404#[derive(Clone, Debug, Default)]
405struct AdditionalMetadataList(Vec<AdditionalMetadata>);
406
407impl AdditionalMetadataList {
408    fn push(&mut self, metadata: AdditionalMetadata) {
409        self.0.push(metadata);
410    }
411}
412
413impl fmt::Display for AdditionalMetadataList {
414    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415        for metadata in &self.0 {
416            write!(f, " {metadata}")?;
417        }
418        Ok(())
419    }
420}
421
422#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
423#[doc(hidden)]
424/// Metadata about a feature that is being used in the SDK.
425#[derive(Clone, Debug)]
426#[non_exhaustive]
427pub struct FeatureMetadata {
428    name: Cow<'static, str>,
429    version: Option<Cow<'static, str>>,
430    additional: AdditionalMetadataList,
431}
432
433#[allow(deprecated)]
434impl FeatureMetadata {
435    /// Creates `FeatureMetadata`.
436    ///
437    /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or
438    /// has characters other than the following:
439    /// ```text
440    /// !#$%&'*+-.^_`|~
441    /// ```
442    pub fn new(
443        name: impl Into<Cow<'static, str>>,
444        version: Option<Cow<'static, str>>,
445    ) -> Result<Self, InvalidMetadataValue> {
446        Ok(Self {
447            name: validate_metadata(name.into())?,
448            version: version.map(validate_metadata).transpose()?,
449            additional: Default::default(),
450        })
451    }
452
453    /// Bundles additional arbitrary metadata with this feature metadata.
454    pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self {
455        self.additional.push(metadata);
456        self
457    }
458}
459
460#[allow(deprecated)]
461impl fmt::Display for FeatureMetadata {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        // feat-metadata = "ft/" name ["/" version] *(RWS additional-metadata)
464        if let Some(version) = &self.version {
465            write!(f, "ft/{}/{}{}", self.name, version, self.additional)
466        } else {
467            write!(f, "ft/{}{}", self.name, self.additional)
468        }
469    }
470}
471
472#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
473#[doc(hidden)]
474/// Metadata about a config value that is being used in the SDK.
475#[derive(Clone, Debug)]
476#[non_exhaustive]
477pub struct ConfigMetadata {
478    config: Cow<'static, str>,
479    value: Option<Cow<'static, str>>,
480}
481
482#[allow(deprecated)]
483impl ConfigMetadata {
484    /// Creates `ConfigMetadata`.
485    ///
486    /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or
487    /// has characters other than the following:
488    /// ```text
489    /// !#$%&'*+-.^_`|~
490    /// ```
491    pub fn new(
492        config: impl Into<Cow<'static, str>>,
493        value: Option<Cow<'static, str>>,
494    ) -> Result<Self, InvalidMetadataValue> {
495        Ok(Self {
496            config: validate_metadata(config.into())?,
497            value: value.map(validate_metadata).transpose()?,
498        })
499    }
500}
501
502#[allow(deprecated)]
503impl fmt::Display for ConfigMetadata {
504    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505        // config-metadata = "cfg/" config ["/" value]
506        if let Some(value) = &self.value {
507            write!(f, "cfg/{}/{}", self.config, value)
508        } else {
509            write!(f, "cfg/{}", self.config)
510        }
511    }
512}
513
514/// Metadata about a software framework that is being used with the SDK.
515///
516/// This is a type alias for [`aws_types::sdk_ua_metadata::FrameworkMetadata`]. The canonical
517/// definition now lives in `aws-types`; this alias is kept as a breadcrumb so existing code that
518/// referred to `aws_runtime::user_agent::FrameworkMetadata` continues to compile.
519pub type FrameworkMetadata = aws_types::sdk_ua_metadata::FrameworkMetadata;
520
521#[derive(Clone, Debug)]
522struct OsMetadata {
523    os_family: &'static OsFamily,
524    version: Option<String>,
525}
526
527impl fmt::Display for OsMetadata {
528    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
529        let os_family = match self.os_family {
530            OsFamily::Windows => "windows",
531            OsFamily::Linux => "linux",
532            OsFamily::Macos => "macos",
533            OsFamily::Android => "android",
534            OsFamily::Ios => "ios",
535            OsFamily::Other => "other",
536        };
537        write!(f, "os/{os_family}")?;
538        if let Some(ref version) = self.version {
539            write!(f, "/{version}")?;
540        }
541        Ok(())
542    }
543}
544
545#[derive(Clone, Debug)]
546struct LanguageMetadata {
547    lang: &'static str,
548    version: &'static str,
549    extras: AdditionalMetadataList,
550}
551impl fmt::Display for LanguageMetadata {
552    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553        // language-metadata = "lang/" language "/" version *(RWS additional-metadata)
554        write!(f, "lang/{}/{}{}", self.lang, self.version, self.extras)
555    }
556}
557
558#[derive(Clone, Debug)]
559struct ExecEnvMetadata {
560    name: String,
561}
562impl fmt::Display for ExecEnvMetadata {
563    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
564        write!(f, "exec-env/{}", &self.name)
565    }
566}
567
568#[cfg(test)]
569mod test {
570    use super::*;
571    use aws_types::app_name::AppName;
572    use aws_types::build_metadata::OsFamily;
573    use aws_types::os_shim_internal::Env;
574
575    fn make_deterministic(ua: &mut AwsUserAgent) {
576        // hard code some variable things for a deterministic test
577        ua.sdk_metadata.version = "0.1";
578        ua.ua_metadata.version = "0.1";
579        ua.language_metadata.version = "1.50.0";
580        ua.os_metadata.os_family = &OsFamily::Macos;
581        ua.os_metadata.version = Some("1.15".to_string());
582    }
583
584    #[test]
585    fn generate_a_valid_ua() {
586        let api_metadata = ApiMetadata {
587            service_id: "dynamodb".into(),
588            version: "123",
589        };
590        let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata);
591        make_deterministic(&mut ua);
592        assert_eq!(
593            ua.aws_ua_header(),
594            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0"
595        );
596        assert_eq!(
597            ua.ua_header(),
598            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0"
599        );
600    }
601
602    #[test]
603    fn generate_a_valid_ua_with_execution_env() {
604        let api_metadata = ApiMetadata {
605            service_id: "dynamodb".into(),
606            version: "123",
607        };
608        let mut ua = AwsUserAgent::new_from_environment(
609            Env::from_slice(&[("AWS_EXECUTION_ENV", "lambda")]),
610            api_metadata,
611        );
612        make_deterministic(&mut ua);
613        assert_eq!(
614            ua.aws_ua_header(),
615            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda"
616        );
617        assert_eq!(
618            ua.ua_header(),
619            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda"
620        );
621    }
622
623    #[test]
624    fn generate_a_valid_ua_with_frameworks() {
625        let api_metadata = ApiMetadata {
626            service_id: "dynamodb".into(),
627            version: "123",
628        };
629        let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
630            .with_framework_metadata(FrameworkMetadata::new("some-framework", Some("1.3")).unwrap())
631            .with_framework_metadata(FrameworkMetadata::new("other", None::<&str>).unwrap());
632        make_deterministic(&mut ua);
633        assert_eq!(
634            ua.aws_ua_header(),
635            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 lib/some-framework/1.3 lib/other"
636        );
637        assert_eq!(
638            ua.ua_header(),
639            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 lib/some-framework/1.3 lib/other"
640        );
641    }
642
643    #[test]
644    fn generate_a_valid_ua_with_app_name() {
645        let api_metadata = ApiMetadata {
646            service_id: "dynamodb".into(),
647            version: "123",
648        };
649        let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
650            .with_app_name(AppName::new("my_app").unwrap());
651        make_deterministic(&mut ua);
652        assert_eq!(
653            ua.aws_ua_header(),
654            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app"
655        );
656        assert_eq!(
657            ua.ua_header(),
658            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app"
659        );
660    }
661
662    #[test]
663    fn generate_a_valid_ua_with_build_env_additional_metadata() {
664        let mut ua = AwsUserAgent::for_tests();
665        ua.build_env_additional_metadata = Some(AdditionalMetadata::new("asdf").unwrap());
666        assert_eq!(
667            ua.aws_ua_header(),
668            "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf"
669        );
670        assert_eq!(
671            ua.ua_header(),
672            "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf"
673        );
674    }
675
676    #[test]
677    fn generate_a_valid_ua_with_business_metrics() {
678        // single metric ID
679        {
680            let ua = AwsUserAgent::for_tests().with_business_metric(BusinessMetric::ResourceModel);
681            assert_eq!(
682                ua.aws_ua_header(),
683                "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
684            );
685            assert_eq!(
686                ua.ua_header(),
687                "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
688            );
689        }
690        // multiple metric IDs
691        {
692            let ua = AwsUserAgent::for_tests()
693                .with_business_metric(BusinessMetric::RetryModeAdaptive)
694                .with_business_metric(BusinessMetric::S3Transfer)
695                .with_business_metric(BusinessMetric::S3ExpressBucket);
696            assert_eq!(
697                ua.aws_ua_header(),
698                "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/F,G,J"
699            );
700            assert_eq!(
701                ua.ua_header(),
702                "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/F,G,J"
703            );
704        }
705    }
706}
707
708/*
709Appendix: User Agent ABNF
710sdk-ua-header                 = "x-amz-user-agent:" OWS ua-string OWS
711ua-pair                       = ua-name ["/" ua-value]
712ua-name                       = token
713ua-value                      = token
714version                       = token
715name                          = token
716service-id                    = token
717sdk-name                      = java / ruby / php / dotnet / python / cli / kotlin / rust / js / cpp / go / go-v2
718os-family                     = windows / linux / macos / android / ios / other
719config                        = retry-mode
720additional-metadata           = "md/" ua-pair
721sdk-metadata                  = "aws-sdk-" sdk-name "/" version
722api-metadata                  = "api/" service-id "/" version
723os-metadata                   = "os/" os-family ["/" version]
724language-metadata             = "lang/" language "/" version *(RWS additional-metadata)
725env-metadata                  = "exec-env/" name
726framework-metadata            = "lib/" name ["/" version] *(RWS additional-metadata)
727app-id                        = "app/" name
728build-env-additional-metadata = "md/" value
729ua-metadata                   = "ua/2.1"
730business-metrics              = "m/" metric_id *(comma metric_id)
731metric_id                     = 1*m_char
732m_char                        = DIGIT / ALPHA / "+" / "-"
733comma                         = ","
734ua-string                     = sdk-metadata RWS
735                                ua-metadata RWS
736                                [api-metadata RWS]
737                                os-metadata RWS
738                                language-metadata RWS
739                                [env-metadata RWS]
740                                       ; ordering is not strictly required in the following section
741                                [business-metrics]
742                                [app-id]
743                                [build-env-additional-metadata]
744                                *(framework-metadata RWS)
745
746# New metadata field might be added in the future and they must follow this format
747prefix               = token
748metadata             = prefix "/" ua-pair
749
750# token, RWS and OWS are defined in [RFC 7230](https://tools.ietf.org/html/rfc7230)
751OWS            = *( SP / HTAB )
752               ; optional whitespace
753RWS            = 1*( SP / HTAB )
754               ; required whitespace
755token          = 1*tchar
756tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
757                 "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
758*/