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 for backward compatibility
274    ///
275    /// This header is intended to be set at `User-Agent`
276    pub fn ua_header(&self) -> String {
277        let mut ua_value = String::new();
278        use std::fmt::Write;
279        write!(ua_value, "{} ", &self.sdk_metadata).unwrap();
280        write!(ua_value, "{} ", &self.os_metadata).unwrap();
281        write!(ua_value, "{}", &self.language_metadata).unwrap();
282        ua_value
283    }
284}
285
286impl Storable for AwsUserAgent {
287    type Storer = StoreReplace<Self>;
288}
289
290#[derive(Clone, Copy, Debug)]
291struct SdkMetadata {
292    name: &'static str,
293    version: &'static str,
294}
295
296impl fmt::Display for SdkMetadata {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        write!(f, "aws-sdk-{}/{}", self.name, self.version)
299    }
300}
301
302#[derive(Clone, Copy, Debug)]
303struct UaMetadata {
304    version: &'static str,
305}
306
307impl fmt::Display for UaMetadata {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        write!(f, "ua/{}", self.version)
310    }
311}
312
313/// Metadata about the client that's making the call.
314#[derive(Clone, Debug)]
315pub struct ApiMetadata {
316    service_id: Cow<'static, str>,
317    version: &'static str,
318}
319
320impl ApiMetadata {
321    /// Creates new `ApiMetadata`.
322    pub const fn new(service_id: &'static str, version: &'static str) -> Self {
323        Self {
324            service_id: Cow::Borrowed(service_id),
325            version,
326        }
327    }
328}
329
330impl fmt::Display for ApiMetadata {
331    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332        write!(f, "api/{}/{}", self.service_id, self.version)
333    }
334}
335
336impl Storable for ApiMetadata {
337    type Storer = StoreReplace<Self>;
338}
339
340/// Error for when an user agent metadata doesn't meet character requirements.
341///
342/// Metadata may only have alphanumeric characters and any of these characters:
343/// ```text
344/// !#$%&'*+-.^_`|~
345/// ```
346/// Spaces are not allowed.
347#[derive(Debug)]
348#[non_exhaustive]
349pub struct InvalidMetadataValue;
350
351impl Error for InvalidMetadataValue {}
352
353impl fmt::Display for InvalidMetadataValue {
354    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355        write!(
356            f,
357            "User agent metadata can only have alphanumeric characters, or any of \
358             '!' |  '#' |  '$' |  '%' |  '&' |  '\\'' |  '*' |  '+' |  '-' | \
359             '.' |  '^' |  '_' |  '`' |  '|' |  '~'"
360        )
361    }
362}
363
364fn validate_metadata(value: Cow<'static, str>) -> Result<Cow<'static, str>, InvalidMetadataValue> {
365    fn valid_character(c: char) -> bool {
366        match c {
367            _ if c.is_ascii_alphanumeric() => true,
368            '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|'
369            | '~' => true,
370            _ => false,
371        }
372    }
373    if !value.chars().all(valid_character) {
374        return Err(InvalidMetadataValue);
375    }
376    Ok(value)
377}
378
379#[doc(hidden)]
380/// Additional metadata that can be bundled with framework or feature metadata.
381#[derive(Clone, Debug)]
382#[non_exhaustive]
383pub struct AdditionalMetadata {
384    value: Cow<'static, str>,
385}
386
387impl AdditionalMetadata {
388    /// Creates `AdditionalMetadata`.
389    ///
390    /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or
391    /// has characters other than the following:
392    /// ```text
393    /// !#$%&'*+-.^_`|~
394    /// ```
395    pub fn new(value: impl Into<Cow<'static, str>>) -> Result<Self, InvalidMetadataValue> {
396        Ok(Self {
397            value: validate_metadata(value.into())?,
398        })
399    }
400}
401
402impl fmt::Display for AdditionalMetadata {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        // additional-metadata = "md/" ua-pair
405        write!(f, "md/{}", self.value)
406    }
407}
408
409#[derive(Clone, Debug, Default)]
410struct AdditionalMetadataList(Vec<AdditionalMetadata>);
411
412impl AdditionalMetadataList {
413    fn push(&mut self, metadata: AdditionalMetadata) {
414        self.0.push(metadata);
415    }
416}
417
418impl fmt::Display for AdditionalMetadataList {
419    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420        for metadata in &self.0 {
421            write!(f, " {}", metadata)?;
422        }
423        Ok(())
424    }
425}
426
427#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
428#[doc(hidden)]
429/// Metadata about a feature that is being used in the SDK.
430#[derive(Clone, Debug)]
431#[non_exhaustive]
432pub struct FeatureMetadata {
433    name: Cow<'static, str>,
434    version: Option<Cow<'static, str>>,
435    additional: AdditionalMetadataList,
436}
437
438#[allow(deprecated)]
439impl FeatureMetadata {
440    /// Creates `FeatureMetadata`.
441    ///
442    /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or
443    /// has characters other than the following:
444    /// ```text
445    /// !#$%&'*+-.^_`|~
446    /// ```
447    pub fn new(
448        name: impl Into<Cow<'static, str>>,
449        version: Option<Cow<'static, str>>,
450    ) -> Result<Self, InvalidMetadataValue> {
451        Ok(Self {
452            name: validate_metadata(name.into())?,
453            version: version.map(validate_metadata).transpose()?,
454            additional: Default::default(),
455        })
456    }
457
458    /// Bundles additional arbitrary metadata with this feature metadata.
459    pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self {
460        self.additional.push(metadata);
461        self
462    }
463}
464
465#[allow(deprecated)]
466impl fmt::Display for FeatureMetadata {
467    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
468        // feat-metadata = "ft/" name ["/" version] *(RWS additional-metadata)
469        if let Some(version) = &self.version {
470            write!(f, "ft/{}/{}{}", self.name, version, self.additional)
471        } else {
472            write!(f, "ft/{}{}", self.name, self.additional)
473        }
474    }
475}
476
477#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
478#[doc(hidden)]
479/// Metadata about a config value that is being used in the SDK.
480#[derive(Clone, Debug)]
481#[non_exhaustive]
482pub struct ConfigMetadata {
483    config: Cow<'static, str>,
484    value: Option<Cow<'static, str>>,
485}
486
487#[allow(deprecated)]
488impl ConfigMetadata {
489    /// Creates `ConfigMetadata`.
490    ///
491    /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or
492    /// has characters other than the following:
493    /// ```text
494    /// !#$%&'*+-.^_`|~
495    /// ```
496    pub fn new(
497        config: impl Into<Cow<'static, str>>,
498        value: Option<Cow<'static, str>>,
499    ) -> Result<Self, InvalidMetadataValue> {
500        Ok(Self {
501            config: validate_metadata(config.into())?,
502            value: value.map(validate_metadata).transpose()?,
503        })
504    }
505}
506
507#[allow(deprecated)]
508impl fmt::Display for ConfigMetadata {
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510        // config-metadata = "cfg/" config ["/" value]
511        if let Some(value) = &self.value {
512            write!(f, "cfg/{}/{}", self.config, value)
513        } else {
514            write!(f, "cfg/{}", self.config)
515        }
516    }
517}
518
519#[doc(hidden)]
520/// Metadata about a software framework that is being used with the SDK.
521#[derive(Clone, Debug)]
522#[non_exhaustive]
523pub struct FrameworkMetadata {
524    name: Cow<'static, str>,
525    version: Option<Cow<'static, str>>,
526    additional: AdditionalMetadataList,
527}
528
529impl FrameworkMetadata {
530    /// Creates `FrameworkMetadata`.
531    ///
532    /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or
533    /// has characters other than the following:
534    /// ```text
535    /// !#$%&'*+-.^_`|~
536    /// ```
537    pub fn new(
538        name: impl Into<Cow<'static, str>>,
539        version: Option<Cow<'static, str>>,
540    ) -> Result<Self, InvalidMetadataValue> {
541        Ok(Self {
542            name: validate_metadata(name.into())?,
543            version: version.map(validate_metadata).transpose()?,
544            additional: Default::default(),
545        })
546    }
547
548    /// Bundles additional arbitrary metadata with this framework metadata.
549    pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self {
550        self.additional.push(metadata);
551        self
552    }
553}
554
555impl fmt::Display for FrameworkMetadata {
556    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
557        // framework-metadata = "lib/" name ["/" version] *(RWS additional-metadata)
558        if let Some(version) = &self.version {
559            write!(f, "lib/{}/{}{}", self.name, version, self.additional)
560        } else {
561            write!(f, "lib/{}{}", self.name, self.additional)
562        }
563    }
564}
565
566#[derive(Clone, Debug)]
567struct OsMetadata {
568    os_family: &'static OsFamily,
569    version: Option<String>,
570}
571
572impl fmt::Display for OsMetadata {
573    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
574        let os_family = match self.os_family {
575            OsFamily::Windows => "windows",
576            OsFamily::Linux => "linux",
577            OsFamily::Macos => "macos",
578            OsFamily::Android => "android",
579            OsFamily::Ios => "ios",
580            OsFamily::Other => "other",
581        };
582        write!(f, "os/{}", os_family)?;
583        if let Some(ref version) = self.version {
584            write!(f, "/{}", version)?;
585        }
586        Ok(())
587    }
588}
589
590#[derive(Clone, Debug)]
591struct LanguageMetadata {
592    lang: &'static str,
593    version: &'static str,
594    extras: AdditionalMetadataList,
595}
596impl fmt::Display for LanguageMetadata {
597    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598        // language-metadata = "lang/" language "/" version *(RWS additional-metadata)
599        write!(f, "lang/{}/{}{}", self.lang, self.version, self.extras)
600    }
601}
602
603#[derive(Clone, Debug)]
604struct ExecEnvMetadata {
605    name: String,
606}
607impl fmt::Display for ExecEnvMetadata {
608    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
609        write!(f, "exec-env/{}", &self.name)
610    }
611}
612
613#[cfg(test)]
614mod test {
615    use super::*;
616    use aws_types::app_name::AppName;
617    use aws_types::build_metadata::OsFamily;
618    use aws_types::os_shim_internal::Env;
619    use std::borrow::Cow;
620
621    fn make_deterministic(ua: &mut AwsUserAgent) {
622        // hard code some variable things for a deterministic test
623        ua.sdk_metadata.version = "0.1";
624        ua.ua_metadata.version = "0.1";
625        ua.language_metadata.version = "1.50.0";
626        ua.os_metadata.os_family = &OsFamily::Macos;
627        ua.os_metadata.version = Some("1.15".to_string());
628    }
629
630    #[test]
631    fn generate_a_valid_ua() {
632        let api_metadata = ApiMetadata {
633            service_id: "dynamodb".into(),
634            version: "123",
635        };
636        let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata);
637        make_deterministic(&mut ua);
638        assert_eq!(
639            ua.aws_ua_header(),
640            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0"
641        );
642        assert_eq!(
643            ua.ua_header(),
644            "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
645        );
646    }
647
648    #[test]
649    fn generate_a_valid_ua_with_execution_env() {
650        let api_metadata = ApiMetadata {
651            service_id: "dynamodb".into(),
652            version: "123",
653        };
654        let mut ua = AwsUserAgent::new_from_environment(
655            Env::from_slice(&[("AWS_EXECUTION_ENV", "lambda")]),
656            api_metadata,
657        );
658        make_deterministic(&mut ua);
659        assert_eq!(
660            ua.aws_ua_header(),
661            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda"
662        );
663        assert_eq!(
664            ua.ua_header(),
665            "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
666        );
667    }
668
669    #[test]
670    fn generate_a_valid_ua_with_frameworks() {
671        let api_metadata = ApiMetadata {
672            service_id: "dynamodb".into(),
673            version: "123",
674        };
675        let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
676            .with_framework_metadata(
677                FrameworkMetadata::new("some-framework", Some(Cow::Borrowed("1.3")))
678                    .unwrap()
679                    .with_additional(AdditionalMetadata::new("something").unwrap()),
680            )
681            .with_framework_metadata(FrameworkMetadata::new("other", None).unwrap());
682        make_deterministic(&mut ua);
683        assert_eq!(
684            ua.aws_ua_header(),
685            "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"
686        );
687        assert_eq!(
688            ua.ua_header(),
689            "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
690        );
691    }
692
693    #[test]
694    fn generate_a_valid_ua_with_app_name() {
695        let api_metadata = ApiMetadata {
696            service_id: "dynamodb".into(),
697            version: "123",
698        };
699        let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
700            .with_app_name(AppName::new("my_app").unwrap());
701        make_deterministic(&mut ua);
702        assert_eq!(
703            ua.aws_ua_header(),
704            "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app"
705        );
706        assert_eq!(
707            ua.ua_header(),
708            "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
709        );
710    }
711
712    #[test]
713    fn generate_a_valid_ua_with_build_env_additional_metadata() {
714        let mut ua = AwsUserAgent::for_tests();
715        ua.build_env_additional_metadata = Some(AdditionalMetadata::new("asdf").unwrap());
716        assert_eq!(
717            ua.aws_ua_header(),
718            "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf"
719        );
720        assert_eq!(
721            ua.ua_header(),
722            "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
723        );
724    }
725
726    #[test]
727    fn generate_a_valid_ua_with_business_metrics() {
728        // single metric ID
729        {
730            let ua = AwsUserAgent::for_tests().with_business_metric(BusinessMetric::ResourceModel);
731            assert_eq!(
732                ua.aws_ua_header(),
733                "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
734            );
735            assert_eq!(
736                ua.ua_header(),
737                "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
738            );
739        }
740        // multiple metric IDs
741        {
742            let ua = AwsUserAgent::for_tests()
743                .with_business_metric(BusinessMetric::RetryModeAdaptive)
744                .with_business_metric(BusinessMetric::S3Transfer)
745                .with_business_metric(BusinessMetric::S3ExpressBucket);
746            assert_eq!(
747                ua.aws_ua_header(),
748                "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"
749            );
750            assert_eq!(
751                ua.ua_header(),
752                "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
753            );
754        }
755    }
756}
757
758/*
759Appendix: User Agent ABNF
760sdk-ua-header                 = "x-amz-user-agent:" OWS ua-string OWS
761ua-pair                       = ua-name ["/" ua-value]
762ua-name                       = token
763ua-value                      = token
764version                       = token
765name                          = token
766service-id                    = token
767sdk-name                      = java / ruby / php / dotnet / python / cli / kotlin / rust / js / cpp / go / go-v2
768os-family                     = windows / linux / macos / android / ios / other
769config                        = retry-mode
770additional-metadata           = "md/" ua-pair
771sdk-metadata                  = "aws-sdk-" sdk-name "/" version
772api-metadata                  = "api/" service-id "/" version
773os-metadata                   = "os/" os-family ["/" version]
774language-metadata             = "lang/" language "/" version *(RWS additional-metadata)
775env-metadata                  = "exec-env/" name
776framework-metadata            = "lib/" name ["/" version] *(RWS additional-metadata)
777app-id                        = "app/" name
778build-env-additional-metadata = "md/" value
779ua-metadata                   = "ua/2.1"
780business-metrics              = "m/" metric_id *(comma metric_id)
781metric_id                     = 1*m_char
782m_char                        = DIGIT / ALPHA / "+" / "-"
783comma                         = ","
784ua-string                     = sdk-metadata RWS
785                                ua-metadata RWS
786                                [api-metadata RWS]
787                                os-metadata RWS
788                                language-metadata RWS
789                                [env-metadata RWS]
790                                       ; ordering is not strictly required in the following section
791                                [business-metrics]
792                                [app-id]
793                                [build-env-additional-metadata]
794                                *(framework-metadata RWS)
795
796# New metadata field might be added in the future and they must follow this format
797prefix               = token
798metadata             = prefix "/" ua-pair
799
800# token, RWS and OWS are defined in [RFC 7230](https://tools.ietf.org/html/rfc7230)
801OWS            = *( SP / HTAB )
802               ; optional whitespace
803RWS            = 1*( SP / HTAB )
804               ; required whitespace
805token          = 1*tchar
806tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
807                 "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
808*/