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#[doc(hidden)]
515/// Metadata about a software framework that is being used with the SDK.
516#[derive(Clone, Debug)]
517#[non_exhaustive]
518pub struct FrameworkMetadata {
519    name: Cow<'static, str>,
520    version: Option<Cow<'static, str>>,
521    additional: AdditionalMetadataList,
522}
523
524impl FrameworkMetadata {
525    /// Creates `FrameworkMetadata`.
526    ///
527    /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or
528    /// has characters other than the following:
529    /// ```text
530    /// !#$%&'*+-.^_`|~
531    /// ```
532    pub fn new(
533        name: impl Into<Cow<'static, str>>,
534        version: Option<Cow<'static, str>>,
535    ) -> Result<Self, InvalidMetadataValue> {
536        Ok(Self {
537            name: validate_metadata(name.into())?,
538            version: version.map(validate_metadata).transpose()?,
539            additional: Default::default(),
540        })
541    }
542
543    /// Bundles additional arbitrary metadata with this framework metadata.
544    pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self {
545        self.additional.push(metadata);
546        self
547    }
548}
549
550impl fmt::Display for FrameworkMetadata {
551    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
552        // framework-metadata = "lib/" name ["/" version] *(RWS additional-metadata)
553        if let Some(version) = &self.version {
554            write!(f, "lib/{}/{}{}", self.name, version, self.additional)
555        } else {
556            write!(f, "lib/{}{}", self.name, self.additional)
557        }
558    }
559}
560
561#[derive(Clone, Debug)]
562struct OsMetadata {
563    os_family: &'static OsFamily,
564    version: Option<String>,
565}
566
567impl fmt::Display for OsMetadata {
568    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569        let os_family = match self.os_family {
570            OsFamily::Windows => "windows",
571            OsFamily::Linux => "linux",
572            OsFamily::Macos => "macos",
573            OsFamily::Android => "android",
574            OsFamily::Ios => "ios",
575            OsFamily::Other => "other",
576        };
577        write!(f, "os/{os_family}")?;
578        if let Some(ref version) = self.version {
579            write!(f, "/{version}")?;
580        }
581        Ok(())
582    }
583}
584
585#[derive(Clone, Debug)]
586struct LanguageMetadata {
587    lang: &'static str,
588    version: &'static str,
589    extras: AdditionalMetadataList,
590}
591impl fmt::Display for LanguageMetadata {
592    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593        // language-metadata = "lang/" language "/" version *(RWS additional-metadata)
594        write!(f, "lang/{}/{}{}", self.lang, self.version, self.extras)
595    }
596}
597
598#[derive(Clone, Debug)]
599struct ExecEnvMetadata {
600    name: String,
601}
602impl fmt::Display for ExecEnvMetadata {
603    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
604        write!(f, "exec-env/{}", &self.name)
605    }
606}
607
608#[cfg(test)]
609mod test {
610    use super::*;
611    use aws_types::app_name::AppName;
612    use aws_types::build_metadata::OsFamily;
613    use aws_types::os_shim_internal::Env;
614    use std::borrow::Cow;
615
616    fn make_deterministic(ua: &mut AwsUserAgent) {
617        // hard code some variable things for a deterministic test
618        ua.sdk_metadata.version = "0.1";
619        ua.ua_metadata.version = "0.1";
620        ua.language_metadata.version = "1.50.0";
621        ua.os_metadata.os_family = &OsFamily::Macos;
622        ua.os_metadata.version = Some("1.15".to_string());
623    }
624
625    #[test]
626    fn generate_a_valid_ua() {
627        let api_metadata = ApiMetadata {
628            service_id: "dynamodb".into(),
629            version: "123",
630        };
631        let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata);
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"
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"
640        );
641    }
642
643    #[test]
644    fn generate_a_valid_ua_with_execution_env() {
645        let api_metadata = ApiMetadata {
646            service_id: "dynamodb".into(),
647            version: "123",
648        };
649        let mut ua = AwsUserAgent::new_from_environment(
650            Env::from_slice(&[("AWS_EXECUTION_ENV", "lambda")]),
651            api_metadata,
652        );
653        make_deterministic(&mut ua);
654        assert_eq!(
655            ua.aws_ua_header(),
656            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda"
657        );
658        assert_eq!(
659            ua.ua_header(),
660            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda"
661        );
662    }
663
664    #[test]
665    fn generate_a_valid_ua_with_frameworks() {
666        let api_metadata = ApiMetadata {
667            service_id: "dynamodb".into(),
668            version: "123",
669        };
670        let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
671            .with_framework_metadata(
672                FrameworkMetadata::new("some-framework", Some(Cow::Borrowed("1.3")))
673                    .unwrap()
674                    .with_additional(AdditionalMetadata::new("something").unwrap()),
675            )
676            .with_framework_metadata(FrameworkMetadata::new("other", None).unwrap());
677        make_deterministic(&mut ua);
678        assert_eq!(
679            ua.aws_ua_header(),
680            "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 md/something lib/other"
681        );
682        assert_eq!(
683            ua.ua_header(),
684            "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 md/something lib/other"
685        );
686    }
687
688    #[test]
689    fn generate_a_valid_ua_with_app_name() {
690        let api_metadata = ApiMetadata {
691            service_id: "dynamodb".into(),
692            version: "123",
693        };
694        let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
695            .with_app_name(AppName::new("my_app").unwrap());
696        make_deterministic(&mut ua);
697        assert_eq!(
698            ua.aws_ua_header(),
699            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app"
700        );
701        assert_eq!(
702            ua.ua_header(),
703            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app"
704        );
705    }
706
707    #[test]
708    fn generate_a_valid_ua_with_build_env_additional_metadata() {
709        let mut ua = AwsUserAgent::for_tests();
710        ua.build_env_additional_metadata = Some(AdditionalMetadata::new("asdf").unwrap());
711        assert_eq!(
712            ua.aws_ua_header(),
713            "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf"
714        );
715        assert_eq!(
716            ua.ua_header(),
717            "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf"
718        );
719    }
720
721    #[test]
722    fn generate_a_valid_ua_with_business_metrics() {
723        // single metric ID
724        {
725            let ua = AwsUserAgent::for_tests().with_business_metric(BusinessMetric::ResourceModel);
726            assert_eq!(
727                ua.aws_ua_header(),
728                "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
729            );
730            assert_eq!(
731                ua.ua_header(),
732                "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
733            );
734        }
735        // multiple metric IDs
736        {
737            let ua = AwsUserAgent::for_tests()
738                .with_business_metric(BusinessMetric::RetryModeAdaptive)
739                .with_business_metric(BusinessMetric::S3Transfer)
740                .with_business_metric(BusinessMetric::S3ExpressBucket);
741            assert_eq!(
742                ua.aws_ua_header(),
743                "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"
744            );
745            assert_eq!(
746                ua.ua_header(),
747                "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"
748            );
749        }
750    }
751}
752
753/*
754Appendix: User Agent ABNF
755sdk-ua-header                 = "x-amz-user-agent:" OWS ua-string OWS
756ua-pair                       = ua-name ["/" ua-value]
757ua-name                       = token
758ua-value                      = token
759version                       = token
760name                          = token
761service-id                    = token
762sdk-name                      = java / ruby / php / dotnet / python / cli / kotlin / rust / js / cpp / go / go-v2
763os-family                     = windows / linux / macos / android / ios / other
764config                        = retry-mode
765additional-metadata           = "md/" ua-pair
766sdk-metadata                  = "aws-sdk-" sdk-name "/" version
767api-metadata                  = "api/" service-id "/" version
768os-metadata                   = "os/" os-family ["/" version]
769language-metadata             = "lang/" language "/" version *(RWS additional-metadata)
770env-metadata                  = "exec-env/" name
771framework-metadata            = "lib/" name ["/" version] *(RWS additional-metadata)
772app-id                        = "app/" name
773build-env-additional-metadata = "md/" value
774ua-metadata                   = "ua/2.1"
775business-metrics              = "m/" metric_id *(comma metric_id)
776metric_id                     = 1*m_char
777m_char                        = DIGIT / ALPHA / "+" / "-"
778comma                         = ","
779ua-string                     = sdk-metadata RWS
780                                ua-metadata RWS
781                                [api-metadata RWS]
782                                os-metadata RWS
783                                language-metadata RWS
784                                [env-metadata RWS]
785                                       ; ordering is not strictly required in the following section
786                                [business-metrics]
787                                [app-id]
788                                [build-env-additional-metadata]
789                                *(framework-metadata RWS)
790
791# New metadata field might be added in the future and they must follow this format
792prefix               = token
793metadata             = prefix "/" ua-pair
794
795# token, RWS and OWS are defined in [RFC 7230](https://tools.ietf.org/html/rfc7230)
796OWS            = *( SP / HTAB )
797               ; optional whitespace
798RWS            = 1*( SP / HTAB )
799               ; required whitespace
800token          = 1*tchar
801tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
802                 "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
803*/