From 72eae556ae9253c0fa718e12e174d3ee28d556ee Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Thu, 11 Nov 2021 16:01:30 -0800 Subject: [PATCH] Relegate `chrono` to an optional feature in a new conversion crate (#849) * Refactor Instant to use `time` instead of `chrono` * Rename methods on Instant to have a consistent naming scheme. * Remove built-in Instant conversions. * Remove `chrono` from `aws-sigv4` * Re-export `Instant` from service crates * Implement `aws-smithy-types-convert` * Rename `Instant` to `DateTime` * Make date-time formatting operations fallible * Add initial changelog entries * Update changelog * Make DateTime to SystemTime conversion fallible * Incorporate review feedback * Fix merge issues * Fix examples * Fix doc comments * Fix unused import warning when using `convert-chrono` feature exclusively --- CHANGELOG.md | 53 ++ aws/SDK_CHANGELOG.md | 50 ++ .../aws-config/src/json_credentials.rs | 26 +- aws/rust-runtime/aws-config/src/sts.rs | 18 +- .../aws-sig-auth/src/event_stream.rs | 2 +- aws/rust-runtime/aws-sig-auth/src/signer.rs | 2 +- aws/rust-runtime/aws-sigv4/Cargo.toml | 3 +- aws/rust-runtime/aws-sigv4/src/date_fmt.rs | 54 -- aws/rust-runtime/aws-sigv4/src/date_time.rs | 144 +++++ .../aws-sigv4/src/event_stream.rs | 43 +- .../src/http_request/canonical_request.rs | 78 +-- .../aws-sigv4/src/http_request/mod.rs | 4 +- .../aws-sigv4/src/http_request/sign.rs | 18 +- aws/rust-runtime/aws-sigv4/src/lib.rs | 30 +- aws/rust-runtime/aws-sigv4/src/sign.rs | 14 +- aws/sdk/build.gradle.kts | 1 + aws/sdk/examples/apigateway/Cargo.toml | 1 + .../apigateway/src/bin/get_rest_apis.rs | 6 +- aws/sdk/examples/cognitoidentity/Cargo.toml | 3 +- .../src/bin/describe-identity-pool.rs | 28 +- .../src/bin/list-identity-pools.rs | 10 +- .../src/bin/list-pool-identities.rs | 15 +- .../cognitoidentityprovider/Cargo.toml | 1 + .../src/bin/list-user-pools.rs | 17 +- aws/sdk/examples/cognitosync/Cargo.toml | 1 + .../src/bin/list-identity-pool-usage.rs | 13 +- aws/sdk/examples/sagemaker/Cargo.toml | 3 +- .../sagemaker/src/bin/list-training-jobs.rs | 14 +- .../sagemaker/src/bin/sagemaker-helloworld.rs | 11 +- .../protocols/ServerHttpProtocolGenerator.kt | 14 +- .../smithy/rust/codegen/rustlang/RustTypes.kt | 6 +- .../rust/codegen/smithy/RuntimeTypes.kt | 6 +- .../rust/codegen/smithy/SymbolVisitor.kt | 2 +- .../SmithyTypesPubUseGenerator.kt | 2 + .../codegen/smithy/generators/Instantiator.kt | 4 +- .../http/RequestBindingGenerator.kt | 14 +- .../http/ResponseBindingGenerator.kt | 4 +- .../parse/XmlBindingTraitParserGenerator.kt | 2 +- .../serialize/JsonSerializerGenerator.kt | 2 +- .../serialize/QuerySerializerGenerator.kt | 2 +- .../XmlBindingTraitSerializerGenerator.kt | 2 +- .../smithy/rust/codegen/SymbolBuilderTest.kt | 4 +- .../generators/StructureGeneratorTest.kt | 2 +- .../http/RequestBindingGeneratorTest.kt | 23 +- .../EventStreamUnmarshallerGeneratorTest.kt | 40 +- .../EventStreamMarshallerGeneratorTest.kt | 6 +- design/src/smithy/simple_shapes.md | 8 +- rust-runtime/Cargo.toml | 1 + .../fuzz/fuzz_targets/mutated_headers.rs | 4 +- .../aws-smithy-eventstream/src/error.rs | 4 +- .../aws-smithy-eventstream/src/frame.rs | 20 +- .../aws-smithy-eventstream/src/smithy.rs | 4 +- rust-runtime/aws-smithy-http/src/header.rs | 10 +- rust-runtime/aws-smithy-http/src/label.rs | 7 +- rust-runtime/aws-smithy-http/src/operation.rs | 27 +- rust-runtime/aws-smithy-http/src/query.rs | 14 +- .../aws-smithy-json/src/deserialize/token.rs | 18 +- rust-runtime/aws-smithy-json/src/serialize.rs | 73 ++- rust-runtime/aws-smithy-query/src/lib.rs | 44 +- .../aws-smithy-types-convert/Cargo.toml | 18 + rust-runtime/aws-smithy-types-convert/LICENSE | 175 ++++++ .../aws-smithy-types-convert/src/date_time.rs | 237 ++++++++ .../aws-smithy-types-convert/src/lib.rs | 17 + rust-runtime/aws-smithy-types/Cargo.toml | 7 +- rust-runtime/aws-smithy-types/fuzz/Cargo.toml | 12 + .../fuzz/fuzz_targets/parse_date_time.rs | 4 +- .../fuzz/fuzz_targets/parse_epoch_seconds.rs | 4 +- .../fuzz/fuzz_targets/parse_http_date.rs | 4 +- .../fuzz/fuzz_targets/read_date_time.rs | 18 + .../fuzz/fuzz_targets/read_http_date.rs | 18 + .../proptest-regressions/instant/format.txt | 7 + .../src/{instant => date_time}/format.rs | 425 +++++++++----- .../aws-smithy-types/src/date_time/mod.rs | 546 ++++++++++++++++++ .../aws-smithy-types/src/instant/mod.rs | 428 -------------- rust-runtime/aws-smithy-types/src/lib.rs | 4 +- 75 files changed, 1955 insertions(+), 1001 deletions(-) delete mode 100644 aws/rust-runtime/aws-sigv4/src/date_fmt.rs create mode 100644 aws/rust-runtime/aws-sigv4/src/date_time.rs create mode 100644 rust-runtime/aws-smithy-types-convert/Cargo.toml create mode 100644 rust-runtime/aws-smithy-types-convert/LICENSE create mode 100644 rust-runtime/aws-smithy-types-convert/src/date_time.rs create mode 100644 rust-runtime/aws-smithy-types-convert/src/lib.rs create mode 100644 rust-runtime/aws-smithy-types/fuzz/fuzz_targets/read_date_time.rs create mode 100644 rust-runtime/aws-smithy-types/fuzz/fuzz_targets/read_http_date.rs create mode 100644 rust-runtime/aws-smithy-types/proptest-regressions/instant/format.txt rename rust-runtime/aws-smithy-types/src/{instant => date_time}/format.rs (54%) create mode 100644 rust-runtime/aws-smithy-types/src/date_time/mod.rs delete mode 100644 rust-runtime/aws-smithy-types/src/instant/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b70827881..ec4d3a85a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,60 @@ vNext (Month Day, Year) ======================= + +**TODO Next Release:** - Update README & aws-sdk-rust CI for MSRV upgrade to 1.54 +**Breaking Changes** + +Several breaking changes around `aws_smithy_types::Instant` were introduced by smithy-rs#849: +- `aws_smithy_types::Instant` from was renamed to `DateTime` to avoid confusion with the standard library's monotonically nondecreasing `Instant` type. +- `DateParseError` in `aws_smithy_types` has been renamed to `DateTimeParseError` to match the type that's being parsed. +- The `chrono-conversions` feature and associated functions have been moved to the `aws-smithy-types-convert` crate. + - Calls to `Instant::from_chrono` should be changed to: + ```rust + use aws_smithy_types::DateTime; + use aws_smithy_types_convert::date_time::DateTimeExt; + + // For chrono::DateTime + let date_time = DateTime::from_chrono_utc(chrono_date_time); + // For chrono::DateTime + let date_time = DateTime::from_chrono_offset(chrono_date_time); + ``` + - Calls to `instant.to_chrono()` should be changed to: + ```rust + use aws_smithy_types_convert::date_time::DateTimeExt; + + date_time.to_chrono_utc(); + ``` +- `Instant::from_system_time` and `Instant::to_system_time` have been changed to `From` trait implementations. + - Calls to `from_system_time` should be changed to: + ```rust + DateTime::from(system_time); + // or + let date_time: DateTime = system_time.into(); + ``` + - Calls to `to_system_time` should be changed to: + ```rust + SystemTime::from(date_time); + // or + let system_time: SystemTime = date_time.into(); + ``` +- Several functions in `Instant`/`DateTime` were renamed: + - `Instant::from_f64` -> `DateTime::from_secs_f64` + - `Instant::from_fractional_seconds` -> `DateTime::from_fractional_secs` + - `Instant::from_epoch_seconds` -> `DateTime::from_secs` + - `Instant::from_epoch_millis` -> `DateTime::from_millis` + - `Instant::epoch_fractional_seconds` -> `DateTime::as_secs_f64` + - `Instant::has_nanos` -> `DateTime::has_subsec_nanos` + - `Instant::epoch_seconds` -> `DateTime::secs` + - `Instant::epoch_subsecond_nanos` -> `DateTime::subsec_nanos` + - `Instant::to_epoch_millis` -> `DateTime::to_millis` +- The `DateTime::fmt` method is now fallible and fails when a `DateTime`'s value is outside what can be represented by the desired date format. +- In `aws-sigv4`, the `SigningParams` builder's `date_time` setter was renamed to `time` and changed to take a `std::time::SystemTime` instead of a chrono's `DateTime`. + +**New this week** +- Conversions from `aws_smithy_types::DateTime` to `OffsetDateTime` from the `time` crate are now available from the `aws-smithy-types-convert` crate. (smithy-rs#849) + v0.28.0-alpha (November 11th, 2021) =================================== diff --git a/aws/SDK_CHANGELOG.md b/aws/SDK_CHANGELOG.md index 29ae4b2f8..d45ba06ea 100644 --- a/aws/SDK_CHANGELOG.md +++ b/aws/SDK_CHANGELOG.md @@ -2,6 +2,56 @@ vNext (Month Day, Year) ======================= - Update README & aws-sdk-rust CI for MSRV upgrade to 1.54 +**Breaking Changes** + +Several breaking changes around `aws_smithy_types::Instant` were introduced by smithy-rs#849: +- `aws_smithy_types::Instant` from was renamed to `DateTime` to avoid confusion with the standard library's monotonically nondecreasing `Instant` type. +- `DateParseError` in `aws_smithy_types` has been renamed to `DateTimeParseError` to match the type that's being parsed. +- The `chrono-conversions` feature and associated functions have been moved to the `aws-smithy-types-convert` crate. + - Calls to `Instant::from_chrono` should be changed to: + ```rust + use aws_smithy_types::DateTime; + use aws_smithy_types_convert::date_time::DateTimeExt; + + // For chrono::DateTime + let date_time = DateTime::from_chrono_utc(chrono_date_time); + // For chrono::DateTime + let date_time = DateTime::from_chrono_offset(chrono_date_time); + ``` + - Calls to `instant.to_chrono()` should be changed to: + ```rust + use aws_smithy_types_convert::date_time::DateTimeExt; + + date_time.to_chrono_utc(); + ``` +- `Instant::from_system_time` and `Instant::to_system_time` have been changed to `From` trait implementations. + - Calls to `from_system_time` should be changed to: + ```rust + DateTime::from(system_time); + // or + let date_time: DateTime = system_time.into(); + ``` + - Calls to `to_system_time` should be changed to: + ```rust + SystemTime::from(date_time); + // or + let system_time: SystemTime = date_time.into(); + ``` +- Several functions in `Instant`/`DateTime` were renamed: + - `Instant::from_f64` -> `DateTime::from_secs_f64` + - `Instant::from_fractional_seconds` -> `DateTime::from_fractional_secs` + - `Instant::from_epoch_seconds` -> `DateTime::from_secs` + - `Instant::from_epoch_millis` -> `DateTime::from_millis` + - `Instant::epoch_fractional_seconds` -> `DateTime::as_secs_f64` + - `Instant::has_nanos` -> `DateTime::has_subsec_nanos` + - `Instant::epoch_seconds` -> `DateTime::secs` + - `Instant::epoch_subsecond_nanos` -> `DateTime::subsec_nanos` + - `Instant::to_epoch_millis` -> `DateTime::to_millis` +- The `DateTime::fmt` method is now fallible and fails when a `DateTime`'s value is outside what can be represented by the desired date format. + +**New this week** +- Conversions from `aws_smithy_types::DateTime` to `OffsetDateTime` from the `time` crate are now available from the `aws-smithy-types-convert` crate. (smithy-rs#849) + v0.0.25-alpha (November 11th, 2021) =================================== diff --git a/aws/rust-runtime/aws-config/src/json_credentials.rs b/aws/rust-runtime/aws-config/src/json_credentials.rs index 7008f1cff..5d123d968 100644 --- a/aws/rust-runtime/aws-config/src/json_credentials.rs +++ b/aws/rust-runtime/aws-config/src/json_credentials.rs @@ -1,8 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + use aws_smithy_json::deserialize::token::skip_value; use aws_smithy_json::deserialize::{json_token_iter, EscapeError, Token}; -use aws_smithy_types::instant::Format; -use aws_smithy_types::Instant; +use aws_smithy_types::date_time::Format; +use aws_smithy_types::DateTime; use std::borrow::Cow; +use std::convert::TryFrom; use std::error::Error; use std::fmt::{Display, Formatter}; use std::time::SystemTime; @@ -167,14 +173,16 @@ pub(crate) fn parse_json_credentials( session_token.ok_or(InvalidJsonCredentials::MissingField("Token"))?; let expiration = expiration.ok_or(InvalidJsonCredentials::MissingField("Expiration"))?; - let expiration = Instant::from_str(expiration.as_ref(), Format::DateTime) - .map_err(|err| { + let expiration = SystemTime::try_from( + DateTime::from_str(expiration.as_ref(), Format::DateTime).map_err(|err| { InvalidJsonCredentials::Other(format!("invalid date: {}", err).into()) - })? - .to_system_time() - .ok_or_else(|| { - InvalidJsonCredentials::Other("invalid expiration (prior to unix epoch)".into()) - })?; + })?, + ) + .map_err(|_| { + InvalidJsonCredentials::Other( + "credential expiration time cannot be represented by a SystemTime".into(), + ) + })?; Ok(JsonCredentials::RefreshableCredentials { access_key_id, secret_access_key, diff --git a/aws/rust-runtime/aws-config/src/sts.rs b/aws/rust-runtime/aws-config/src/sts.rs index 1c527c019..9055681a6 100644 --- a/aws/rust-runtime/aws-config/src/sts.rs +++ b/aws/rust-runtime/aws-config/src/sts.rs @@ -12,6 +12,7 @@ pub(crate) mod util { use aws_sdk_sts::model::Credentials as StsCredentials; use aws_types::credentials::{self, CredentialsError}; use aws_types::Credentials as AwsCredentials; + use std::convert::TryFrom; use std::time::{SystemTime, UNIX_EPOCH}; /// Convert STS credentials to aws_auth::Credentials @@ -21,14 +22,15 @@ pub(crate) mod util { ) -> credentials::Result { let sts_credentials = sts_credentials .ok_or_else(|| CredentialsError::unhandled("STS credentials must be defined"))?; - let expiration = sts_credentials - .expiration - .ok_or_else(|| CredentialsError::unhandled("missing expiration"))?; - let expiration = expiration.to_system_time().ok_or_else(|| { - CredentialsError::unhandled(format!( - "expiration is before unix epoch: {:?}", - &expiration - )) + let expiration = SystemTime::try_from( + sts_credentials + .expiration + .ok_or_else(|| CredentialsError::unhandled("missing expiration"))?, + ) + .map_err(|_| { + CredentialsError::unhandled( + "credential expiration time cannot be represented by a SystemTime", + ) })?; Ok(AwsCredentials::new( sts_credentials diff --git a/aws/rust-runtime/aws-sig-auth/src/event_stream.rs b/aws/rust-runtime/aws-sig-auth/src/event_stream.rs index c5401853e..dd859d4cb 100644 --- a/aws/rust-runtime/aws-sig-auth/src/event_stream.rs +++ b/aws/rust-runtime/aws-sig-auth/src/event_stream.rs @@ -43,7 +43,7 @@ impl SigV4Signer { .secret_key(credentials.secret_access_key()) .region(region.as_ref()) .service_name(signing_service.as_ref()) - .date_time(time.into()) + .time(time) .settings(()); builder.set_security_token(credentials.session_token()); builder.build().unwrap() diff --git a/aws/rust-runtime/aws-sig-auth/src/signer.rs b/aws/rust-runtime/aws-sig-auth/src/signer.rs index e88419bcd..4c91af3ca 100644 --- a/aws/rust-runtime/aws-sig-auth/src/signer.rs +++ b/aws/rust-runtime/aws-sig-auth/src/signer.rs @@ -167,7 +167,7 @@ impl SigV4Signer { .secret_key(credentials.secret_access_key()) .region(request_config.region.as_ref()) .service_name(request_config.service.as_ref()) - .date_time(request_config.request_ts.into()) + .time(request_config.request_ts) .settings(settings); builder.set_security_token(credentials.session_token()); builder.build().expect("all required fields set") diff --git a/aws/rust-runtime/aws-sigv4/Cargo.toml b/aws/rust-runtime/aws-sigv4/Cargo.toml index d8bd59746..9dc2afbf1 100644 --- a/aws/rust-runtime/aws-sigv4/Cargo.toml +++ b/aws/rust-runtime/aws-sigv4/Cargo.toml @@ -16,7 +16,6 @@ default = ["sign-http"] [dependencies] aws-smithy-eventstream = { path = "../../../rust-runtime/aws-smithy-eventstream", optional = true } bytes = { version = "1", optional = true } -chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } form_urlencoded = { version = "1.0", optional = true } hex = "0.4" http = { version = "0.2", optional = true } @@ -24,6 +23,7 @@ once_cell = "1.8" percent-encoding = { version = "2.1", optional = true } regex = "1.5" ring = "0.16" +time = "0.3.4" tracing = "0.1" [dev-dependencies] @@ -31,3 +31,4 @@ bytes = "1" httparse = "1.5" pretty_assertions = "1.0" proptest = "1" +time = { version = "0.3.4", features = ["parsing"] } diff --git a/aws/rust-runtime/aws-sigv4/src/date_fmt.rs b/aws/rust-runtime/aws-sigv4/src/date_fmt.rs deleted file mode 100644 index a714fca20..000000000 --- a/aws/rust-runtime/aws-sigv4/src/date_fmt.rs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -// Some of the functions in this file are unused when disabling certain features -#![allow(dead_code)] -use chrono::{Date, DateTime, NaiveDate, NaiveDateTime, ParseError, Utc}; - -const DATE_TIME_FORMAT: &str = "%Y%m%dT%H%M%SZ"; -const DATE_FORMAT: &str = "%Y%m%d"; - -/// Formats a chrono `Date` in `YYYYMMDD` format. -pub(crate) fn format_date(date: &Date) -> String { - date.format(DATE_FORMAT).to_string() -} - -/// Parses `YYYYMMDD` formatted dates into a chrono `Date`. -pub(crate) fn parse_date(date_str: &str) -> Result, ParseError> { - Ok(Date::::from_utc( - NaiveDate::parse_from_str(date_str, "%Y%m%d")?, - Utc, - )) -} - -/// Formats a chrono `DateTime` in `YYYYMMDD'T'HHMMSS'Z'` format. -pub(crate) fn format_date_time(date_time: &DateTime) -> String { - date_time.format(DATE_TIME_FORMAT).to_string() -} - -/// Parses `YYYYMMDD'T'HHMMSS'Z'` formatted dates into a chrono `DateTime`. -pub(crate) fn parse_date_time(date_time_str: &str) -> Result, ParseError> { - Ok(DateTime::::from_utc( - NaiveDateTime::parse_from_str(date_time_str, DATE_TIME_FORMAT)?, - Utc, - )) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn date_time_roundtrip() { - let date = parse_date_time("20150830T123600Z").unwrap(); - assert_eq!("20150830T123600Z", format_date_time(&date)); - } - - #[test] - fn date_roundtrip() { - let date = parse_date("20150830").unwrap(); - assert_eq!("20150830", format_date(&date)); - } -} diff --git a/aws/rust-runtime/aws-sigv4/src/date_time.rs b/aws/rust-runtime/aws-sigv4/src/date_time.rs new file mode 100644 index 000000000..bf05278ef --- /dev/null +++ b/aws/rust-runtime/aws-sigv4/src/date_time.rs @@ -0,0 +1,144 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +// Some of the functions in this file are unused when disabling certain features +#![allow(dead_code)] + +use std::time::SystemTime; +use time::{OffsetDateTime, Time}; + +/// Truncates the subseconds from the given `SystemTime` to zero. +pub(crate) fn truncate_subsecs(time: SystemTime) -> SystemTime { + let date_time = OffsetDateTime::from(time); + let time = date_time.time(); + date_time + .replace_time( + Time::from_hms(time.hour(), time.minute(), time.second()).expect("was already a time"), + ) + .into() +} + +/// Formats a `SystemTime` in `YYYYMMDD` format. +pub(crate) fn format_date(time: SystemTime) -> String { + let time = OffsetDateTime::from(time); + format!( + "{:04}{:02}{:02}", + time.year(), + u8::from(time.month()), + time.day() + ) +} + +/// Formats a `SystemTime` in `YYYYMMDD'T'HHMMSS'Z'` format. +pub(crate) fn format_date_time(time: SystemTime) -> String { + let time = OffsetDateTime::from(time); + format!( + "{:04}{:02}{:02}T{:02}{:02}{:02}Z", + time.year(), + u8::from(time.month()), + time.day(), + time.hour(), + time.minute(), + time.second() + ) +} + +/// Parse functions that are only needed for unit tests. +#[cfg(test)] +pub(crate) mod test_parsers { + use std::{borrow::Cow, error::Error, fmt, time::SystemTime}; + use time::format_description; + use time::{Date, PrimitiveDateTime, Time}; + + const DATE_TIME_FORMAT: &str = "[year][month][day]T[hour][minute][second]Z"; + const DATE_FORMAT: &str = "[year][month][day]"; + + /// Parses `YYYYMMDD'T'HHMMSS'Z'` formatted dates into a `SystemTime`. + pub(crate) fn parse_date_time(date_time_str: &str) -> Result { + let date_time = PrimitiveDateTime::parse( + date_time_str, + &format_description::parse(DATE_TIME_FORMAT).unwrap(), + ) + .map_err(|err| ParseError(err.to_string().into()))? + .assume_utc(); + Ok(date_time.into()) + } + + /// Parses `YYYYMMDD` formatted dates into a `SystemTime`. + pub(crate) fn parse_date(date_str: &str) -> Result { + let date_time = PrimitiveDateTime::new( + Date::parse(date_str, &format_description::parse(DATE_FORMAT).unwrap()) + .map_err(|err| ParseError(err.to_string().into()))?, + Time::from_hms(0, 0, 0).unwrap(), + ) + .assume_utc(); + Ok(date_time.into()) + } + + #[derive(Debug)] + pub(crate) struct ParseError(Cow<'static, str>); + + impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "failed to parse time: {}", self.0) + } + } + + impl Error for ParseError {} +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::date_time::test_parsers::{parse_date, parse_date_time}; + use time::format_description::well_known::Rfc3339; + + #[test] + fn date_format() { + let time: SystemTime = OffsetDateTime::parse("2039-02-04T23:01:09.104Z", &Rfc3339) + .unwrap() + .into(); + assert_eq!("20390204", format_date(time)); + let time: SystemTime = OffsetDateTime::parse("0100-01-02T00:00:00.000Z", &Rfc3339) + .unwrap() + .into(); + assert_eq!("01000102", format_date(time)); + } + + #[test] + fn date_time_format() { + let time: SystemTime = OffsetDateTime::parse("2039-02-04T23:01:09.104Z", &Rfc3339) + .unwrap() + .into(); + assert_eq!("20390204T230109Z", format_date_time(time)); + let time: SystemTime = OffsetDateTime::parse("0100-01-02T00:00:00.000Z", &Rfc3339) + .unwrap() + .into(); + assert_eq!("01000102T000000Z", format_date_time(time)); + } + + #[test] + fn date_time_roundtrip() { + let time = parse_date_time("20150830T123600Z").unwrap(); + assert_eq!("20150830T123600Z", format_date_time(time)); + } + + #[test] + fn date_roundtrip() { + let time = parse_date("20150830").unwrap(); + assert_eq!("20150830", format_date(time)); + } + + #[test] + fn test_truncate_subsecs() { + let time: SystemTime = OffsetDateTime::parse("2039-02-04T23:01:09.104Z", &Rfc3339) + .unwrap() + .into(); + let expected: SystemTime = OffsetDateTime::parse("2039-02-04T23:01:09.000Z", &Rfc3339) + .unwrap() + .into(); + assert_eq!(expected, truncate_subsecs(time)); + } +} diff --git a/aws/rust-runtime/aws-sigv4/src/event_stream.rs b/aws/rust-runtime/aws-sigv4/src/event_stream.rs index 938c1de51..48f1249d1 100644 --- a/aws/rust-runtime/aws-sigv4/src/event_stream.rs +++ b/aws/rust-runtime/aws-sigv4/src/event_stream.rs @@ -9,8 +9,8 @@ //! //! ```rust //! use aws_sigv4::event_stream::{sign_message, SigningParams}; -//! use chrono::Utc; //! use aws_smithy_eventstream::frame::{Header, HeaderValue, Message}; +//! use std::time::SystemTime; //! //! // The `last_signature` argument is the previous message's signature, or //! // the signature of the initial HTTP request if a message hasn't been signed yet. @@ -26,7 +26,7 @@ //! .secret_key("example secret key") //! .region("us-east-1") //! .service_name("exampleservice") -//! .date_time(Utc::now()) +//! .time(SystemTime::now()) //! .settings(()) //! .build() //! .unwrap(); @@ -36,13 +36,13 @@ //! sign_message(&message_to_sign, &last_signature, ¶ms).into_parts(); //! ``` -use crate::date_fmt::{format_date, format_date_time}; +use crate::date_time::{format_date, format_date_time, truncate_subsecs}; use crate::sign::{calculate_signature, generate_signing_key, sha256_hex_string}; use crate::SigningOutput; use aws_smithy_eventstream::frame::{write_headers_to, Header, HeaderValue, Message}; use bytes::Bytes; -use chrono::{DateTime, SubsecRound, Utc}; use std::io::Write; +use std::time::SystemTime; /// Event stream signing parameters pub type SigningParams<'a> = super::SigningParams<'a, ()>; @@ -51,13 +51,13 @@ pub type SigningParams<'a> = super::SigningParams<'a, ()>; fn calculate_string_to_sign( message_payload: &[u8], last_signature: &str, - date_time: &DateTime, + time: SystemTime, params: &SigningParams<'_>, ) -> Vec { // Event Stream string to sign format is documented here: // https://docs.aws.amazon.com/transcribe/latest/dg/how-streaming.html - let date_time_str = format_date_time(date_time); - let date_str = format_date(&date_time.date()); + let date_time_str = format_date_time(time); + let date_str = format_date(time); let mut sts: Vec = Vec::new(); writeln!(sts, "AWS4-HMAC-SHA256-PAYLOAD").unwrap(); @@ -70,7 +70,7 @@ fn calculate_string_to_sign( .unwrap(); writeln!(sts, "{}", last_signature).unwrap(); - let date_header = Header::new(":date", HeaderValue::Timestamp((*date_time).into())); + let date_header = Header::new(":date", HeaderValue::Timestamp(time.into())); let mut date_buffer = Vec::new(); write_headers_to(&[date_header], &mut date_buffer).unwrap(); writeln!(sts, "{}", sha256_hex_string(&date_buffer)).unwrap(); @@ -115,18 +115,14 @@ fn sign_payload<'a>( ) -> SigningOutput { // Truncate the sub-seconds up front since the timestamp written to the signed message header // needs to exactly match the string formatted timestamp, which doesn't include sub-seconds. - let date_time = params.date_time.trunc_subsecs(0); + let time = truncate_subsecs(params.time); - let signing_key = generate_signing_key( - params.secret_key, - date_time.date(), - params.region, - params.service_name, - ); + let signing_key = + generate_signing_key(params.secret_key, time, params.region, params.service_name); let string_to_sign = calculate_string_to_sign( message_payload.as_ref().map(|v| &v[..]).unwrap_or(&[]), last_signature, - &date_time, + time, params, ); let signature = calculate_signature(signing_key, &string_to_sign); @@ -138,10 +134,7 @@ fn sign_payload<'a>( ":chunk-signature", HeaderValue::ByteArray(hex::decode(&signature).unwrap().into()), )) - .add_header(Header::new( - ":date", - HeaderValue::Timestamp(date_time.into()), - )), + .add_header(Header::new(":date", HeaderValue::Timestamp(time.into()))), signature, ) } @@ -166,7 +159,7 @@ mod tests { security_token: None, region: "us-east-1", service_name: "testservice", - date_time: (UNIX_EPOCH + Duration::new(123_456_789_u64, 1234u32)).into(), + time: (UNIX_EPOCH + Duration::new(123_456_789_u64, 1234u32)).into(), settings: (), }; @@ -185,7 +178,7 @@ mod tests { std::str::from_utf8(&calculate_string_to_sign( &message_payload, &last_signature, - ¶ms.date_time, + params.time, ¶ms )) .unwrap() @@ -204,7 +197,7 @@ mod tests { security_token: None, region: "us-east-1", service_name: "testservice", - date_time: (UNIX_EPOCH + Duration::new(123_456_789_u64, 1234u32)).into(), + time: (UNIX_EPOCH + Duration::new(123_456_789_u64, 1234u32)).into(), settings: (), }; @@ -219,9 +212,9 @@ mod tests { } assert_eq!(":date", signed.headers()[1].name().as_str()); if let HeaderValue::Timestamp(value) = signed.headers()[1].value() { - assert_eq!(123_456_789_i64, value.epoch_seconds()); + assert_eq!(123_456_789_i64, value.secs()); // The subseconds should have been truncated off - assert_eq!(0, value.epoch_subsecond_nanos()); + assert_eq!(0, value.subsec_nanos()); } else { panic!("expected timestamp for :date header"); } diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs b/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs index 4f5e33fee..22b35fa8f 100644 --- a/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs +++ b/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs @@ -5,11 +5,10 @@ use super::query_writer::QueryWriter; use super::{Error, PayloadChecksumKind, SignableBody, SignatureLocation, SigningParams}; -use crate::date_fmt::{format_date, format_date_time, parse_date, parse_date_time}; +use crate::date_time::{format_date, format_date_time}; use crate::http_request::sign::SignableRequest; use crate::http_request::PercentEncodingMode; use crate::sign::sha256_hex_string; -use chrono::{Date, DateTime, Utc}; use http::header::{HeaderName, CONTENT_LENGTH, CONTENT_TYPE, HOST, USER_AGENT}; use http::{HeaderMap, HeaderValue, Method, Uri}; use std::borrow::Cow; @@ -18,6 +17,7 @@ use std::convert::TryFrom; use std::fmt; use std::fmt::Formatter; use std::str::FromStr; +use std::time::SystemTime; pub(crate) mod header { pub(crate) const X_AMZ_CONTENT_SHA_256: &str = "x-amz-content-sha256"; @@ -133,7 +133,7 @@ impl<'a> CanonicalRequest<'a> { }; let payload_hash = Self::payload_hash(req.body()); - let date_time = format_date_time(¶ms.date_time); + let date_time = format_date_time(params.time); let (signed_headers, canonical_headers) = Self::headers(req, params, &payload_hash, &date_time)?; let signed_headers = SignedHeaders::new(signed_headers); @@ -150,7 +150,7 @@ impl<'a> CanonicalRequest<'a> { credential: format!( "{}/{}/{}/{}/aws4_request", params.access_key, - format_date(¶ms.date_time.date()), + format_date(params.time), params.region, params.service_name, ), @@ -429,7 +429,7 @@ impl Ord for CanonicalHeaderName { #[derive(PartialEq, Debug, Clone)] pub(super) struct SigningScope<'a> { - pub(super) date: Date, + pub(super) time: SystemTime, pub(super) region: &'a str, pub(super) service: &'a str, } @@ -439,75 +439,37 @@ impl<'a> fmt::Display for SigningScope<'a> { write!( f, "{}/{}/{}/aws4_request", - format_date(&self.date), + format_date(self.time), self.region, self.service ) } } -impl<'a> TryFrom<&'a str> for SigningScope<'a> { - type Error = Error; - fn try_from(s: &'a str) -> Result, Self::Error> { - let mut scopes = s.split('/'); - let date = parse_date(scopes.next().expect("missing date"))?; - let region = scopes.next().expect("missing region"); - let service = scopes.next().expect("missing service"); - - let scope = SigningScope { - date, - region, - service, - }; - - Ok(scope) - } -} - #[derive(PartialEq, Debug)] pub(super) struct StringToSign<'a> { pub(super) scope: SigningScope<'a>, - pub(super) date: DateTime, + pub(super) time: SystemTime, pub(super) region: &'a str, pub(super) service: &'a str, pub(super) hashed_creq: &'a str, } -impl<'a> TryFrom<&'a str> for StringToSign<'a> { - type Error = Error; - fn try_from(s: &'a str) -> Result { - let lines = s.lines().collect::>(); - let date = parse_date_time(lines[1])?; - let scope: SigningScope<'_> = TryFrom::try_from(lines[2])?; - let hashed_creq = &lines[3]; - - let sts = StringToSign { - date, - region: scope.region, - service: scope.service, - scope, - hashed_creq, - }; - - Ok(sts) - } -} - impl<'a> StringToSign<'a> { pub(crate) fn new( - date: DateTime, + time: SystemTime, region: &'a str, service: &'a str, hashed_creq: &'a str, ) -> Self { let scope = SigningScope { - date: date.date(), + time, region, service, }; Self { scope, - date, + time, region, service, hashed_creq, @@ -521,7 +483,7 @@ impl<'a> fmt::Display for StringToSign<'a> { f, "{}\n{}\n{}\n{}", HMAC_256, - format_date_time(&self.date), + format_date_time(self.time), self.scope.to_string(), self.hashed_creq ) @@ -530,7 +492,7 @@ impl<'a> fmt::Display for StringToSign<'a> { #[cfg(test)] mod tests { - use crate::date_fmt::parse_date_time; + use crate::date_time::test_parsers::parse_date_time; use crate::http_request::canonical_request::{ normalize_header_value, trim_all, CanonicalRequest, SigningScope, StringToSign, }; @@ -542,7 +504,6 @@ mod tests { use crate::sign::sha256_hex_string; use pretty_assertions::assert_eq; use proptest::{proptest, strategy::Strategy}; - use std::convert::TryFrom; use std::time::Duration; fn signing_params(settings: SigningSettings) -> SigningParams<'static> { @@ -552,7 +513,7 @@ mod tests { security_token: None, region: "test-region", service_name: "testservicename", - date_time: parse_date_time("20210511T154045Z").unwrap(), + time: parse_date_time("20210511T154045Z").unwrap(), settings, } } @@ -624,9 +585,8 @@ mod tests { #[test] fn test_generate_scope() { let expected = "20150830/us-east-1/iam/aws4_request\n"; - let date = parse_date_time("20150830T123600Z").unwrap(); let scope = SigningScope { - date: date.date(), + time: parse_date_time("20150830T123600Z").unwrap(), region: "us-east-1", service: "iam", }; @@ -635,21 +595,15 @@ mod tests { #[test] fn test_string_to_sign() { - let date = parse_date_time("20150830T123600Z").unwrap(); + let time = parse_date_time("20150830T123600Z").unwrap(); let creq = test_canonical_request("get-vanilla-query-order-key-case"); let expected_sts = test_sts("get-vanilla-query-order-key-case"); let encoded = sha256_hex_string(creq.as_bytes()); - let actual = StringToSign::new(date, "us-east-1", "service", &encoded); + let actual = StringToSign::new(time, "us-east-1", "service", &encoded); assert_eq!(expected_sts, actual.to_string()); } - #[test] - fn read_sts() { - let sts = test_sts("get-vanilla-query-order-key-case"); - StringToSign::try_from(sts.as_ref()).unwrap(); - } - #[test] fn test_digest_of_canonical_request() { let creq = test_canonical_request("get-vanilla-query-order-key-case"); diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/mod.rs b/aws/rust-runtime/aws-sigv4/src/http_request/mod.rs index 25dd7fd35..0b80e7113 100644 --- a/aws/rust-runtime/aws-sigv4/src/http_request/mod.rs +++ b/aws/rust-runtime/aws-sigv4/src/http_request/mod.rs @@ -10,8 +10,8 @@ //! ```rust //! # fn test() -> Result<(), aws_sigv4::http_request::Error> { //! use aws_sigv4::http_request::{sign, SigningSettings, SigningParams, SignableRequest}; -//! use chrono::Utc; //! use http; +//! use std::time::SystemTime; //! //! // Create the request to sign //! let mut request = http::Request::builder() @@ -26,7 +26,7 @@ //! .secret_key("example secret key") //! .region("us-east-1") //! .service_name("exampleservice") -//! .date_time(Utc::now()) +//! .time(SystemTime::now()) //! .settings(signing_settings) //! .build() //! .unwrap(); diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/sign.rs b/aws/rust-runtime/aws-sigv4/src/http_request/sign.rs index 46d7b54b5..d8182a609 100644 --- a/aws/rust-runtime/aws-sigv4/src/http_request/sign.rs +++ b/aws/rust-runtime/aws-sigv4/src/http_request/sign.rs @@ -184,14 +184,14 @@ fn calculate_signing_params<'a>( let encoded_creq = &sha256_hex_string(creq.to_string().as_bytes()); let sts = StringToSign::new( - params.date_time, + params.time, params.region, params.service_name, encoded_creq, ); let signing_key = generate_signing_key( params.secret_key, - params.date_time.date(), + params.time, params.region, params.service_name, ); @@ -235,7 +235,7 @@ fn calculate_signing_headers<'a>( // Step 2: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-create-string-to-sign.html. let encoded_creq = &sha256_hex_string(creq.to_string().as_bytes()); let sts = StringToSign::new( - params.date_time, + params.time, params.region, params.service_name, encoded_creq, @@ -244,7 +244,7 @@ fn calculate_signing_headers<'a>( // Step 3: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-calculate-signature.html let signing_key = generate_signing_key( params.secret_key, - params.date_time.date(), + params.time, params.region, params.service_name, ); @@ -299,7 +299,7 @@ fn build_authorization_header( #[cfg(test)] mod tests { use super::{sign, SigningInstructions}; - use crate::date_fmt::parse_date_time; + use crate::date_time::test_parsers::parse_date_time; use crate::http_request::sign::SignableRequest; use crate::http_request::test::{ make_headers_comparable, test_request, test_signed_request, @@ -328,7 +328,7 @@ mod tests { security_token: None, region: "us-east-1", service_name: "service", - date_time: parse_date_time("20150830T123600Z").unwrap(), + time: parse_date_time("20150830T123600Z").unwrap(), settings, }; @@ -358,7 +358,7 @@ mod tests { security_token: None, region: "us-east-1", service_name: "service", - date_time: parse_date_time("20150830T123600Z").unwrap(), + time: parse_date_time("20150830T123600Z").unwrap(), settings, }; @@ -386,7 +386,7 @@ mod tests { security_token: None, region: "us-east-1", service_name: "service", - date_time: parse_date_time("20150830T123600Z").unwrap(), + time: parse_date_time("20150830T123600Z").unwrap(), settings, }; @@ -436,7 +436,7 @@ mod tests { security_token: None, region: "us-east-1", service_name: "service", - date_time: parse_date_time("20150830T123600Z").unwrap(), + time: parse_date_time("20150830T123600Z").unwrap(), settings, }; diff --git a/aws/rust-runtime/aws-sigv4/src/lib.rs b/aws/rust-runtime/aws-sigv4/src/lib.rs index ddfe5560a..75fc46135 100644 --- a/aws/rust-runtime/aws-sigv4/src/lib.rs +++ b/aws/rust-runtime/aws-sigv4/src/lib.rs @@ -14,11 +14,11 @@ unreachable_pub )] -use chrono::{DateTime, Utc}; +use std::time::SystemTime; pub mod sign; -mod date_fmt; +mod date_time; #[cfg(feature = "sign-eventstream")] pub mod event_stream; @@ -41,8 +41,8 @@ pub struct SigningParams<'a, S> { pub(crate) region: &'a str, /// AWS Service Name to sign for. pub(crate) service_name: &'a str, - /// Timestamp to use in the signature (should be `Utc::now()` unless testing). - pub(crate) date_time: DateTime, + /// Timestamp to use in the signature (should be `SystemTime::now()` unless testing). + pub(crate) time: SystemTime, /// Additional signing settings. These differ between HTTP and Event Stream. pub(crate) settings: S, @@ -58,9 +58,9 @@ impl<'a, S: Default> SigningParams<'a, S> { /// Builder and error for creating [`SigningParams`] pub mod signing_params { use super::SigningParams; - use chrono::{DateTime, Utc}; use std::error::Error; use std::fmt; + use std::time::SystemTime; /// [`SigningParams`] builder error #[derive(Debug)] @@ -89,7 +89,7 @@ pub mod signing_params { security_token: Option<&'a str>, region: Option<&'a str>, service_name: Option<&'a str>, - date_time: Option>, + time: Option, settings: Option, } @@ -144,14 +144,14 @@ pub mod signing_params { self.service_name = service_name; } - /// Sets the date time to be used in the signature (required) - pub fn date_time(mut self, date_time: DateTime) -> Self { - self.date_time = Some(date_time); + /// Sets the time to be used in the signature (required) + pub fn time(mut self, time: SystemTime) -> Self { + self.time = Some(time); self } - /// Sets the date time to be used in the signature (required) - pub fn set_date_time(&mut self, date_time: Option>) { - self.date_time = date_time; + /// Sets the time to be used in the signature (required) + pub fn set_time(&mut self, time: Option) { + self.time = time; } /// Sets additional signing settings (required) @@ -181,9 +181,9 @@ pub mod signing_params { service_name: self .service_name .ok_or_else(|| BuildError::new("service name is required"))?, - date_time: self - .date_time - .ok_or_else(|| BuildError::new("date time is required"))?, + time: self + .time + .ok_or_else(|| BuildError::new("time is required"))?, settings: self .settings .ok_or_else(|| BuildError::new("settings are required"))?, diff --git a/aws/rust-runtime/aws-sigv4/src/sign.rs b/aws/rust-runtime/aws-sigv4/src/sign.rs index 6041b8c5c..3e5dec987 100644 --- a/aws/rust-runtime/aws-sigv4/src/sign.rs +++ b/aws/rust-runtime/aws-sigv4/src/sign.rs @@ -5,12 +5,12 @@ //! Functions to create signing keys and calculate signatures. -use crate::date_fmt::format_date; -use chrono::{Date, Utc}; +use crate::date_time::format_date; use ring::{ digest::{self}, hmac::{self, Key, Tag}, }; +use std::time::SystemTime; /// HashedPayload = Lowercase(HexEncode(Hash(requestPayload))) #[allow(dead_code)] // Unused when compiling without certain features @@ -29,7 +29,7 @@ pub fn calculate_signature(signing_key: Tag, string_to_sign: &[u8]) -> String { /// Generates a signing key for Sigv4 pub fn generate_signing_key( secret: &str, - date: Date, + time: SystemTime, region: &str, service: &str, ) -> hmac::Tag { @@ -41,7 +41,7 @@ pub fn generate_signing_key( let secret = format!("AWS4{}", secret); let secret = hmac::Key::new(hmac::HMAC_SHA256, secret.as_bytes()); - let tag = hmac::sign(&secret, format_date(&date).as_bytes()); + let tag = hmac::sign(&secret, format_date(time).as_bytes()); // sign region let key = hmac::Key::new(hmac::HMAC_SHA256, tag.as_ref()); @@ -59,7 +59,7 @@ pub fn generate_signing_key( #[cfg(test)] mod tests { use super::{calculate_signature, generate_signing_key}; - use crate::date_fmt::parse_date_time; + use crate::date_time::test_parsers::parse_date_time; use crate::http_request::test::test_canonical_request; use crate::sign::sha256_hex_string; @@ -67,9 +67,9 @@ mod tests { fn test_signature_calculation() { let secret = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; let creq = test_canonical_request("iam"); - let date = parse_date_time("20150830T123600Z").unwrap(); + let time = parse_date_time("20150830T123600Z").unwrap(); - let derived_key = generate_signing_key(secret, date.date(), "us-east-1", "iam"); + let derived_key = generate_signing_key(secret, time, "us-east-1", "iam"); let signature = calculate_signature(derived_key, creq.as_bytes()); let expected = "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"; diff --git a/aws/sdk/build.gradle.kts b/aws/sdk/build.gradle.kts index 47708f8cf..dfc8e0ac4 100644 --- a/aws/sdk/build.gradle.kts +++ b/aws/sdk/build.gradle.kts @@ -35,6 +35,7 @@ val runtimeModules = listOf( "aws-smithy-protocol-test", "aws-smithy-query", "aws-smithy-types", + "aws-smithy-types-convert", "aws-smithy-xml" ) val awsModules = listOf( diff --git a/aws/sdk/examples/apigateway/Cargo.toml b/aws/sdk/examples/apigateway/Cargo.toml index 69ec42012..f931a5a58 100644 --- a/aws/sdk/examples/apigateway/Cargo.toml +++ b/aws/sdk/examples/apigateway/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" [dependencies] aws-config = { path = "../../build/aws-sdk/sdk/aws-config" } +aws-smithy-types-convert = { path = "../../build/aws-sdk/sdk/aws-smithy-types-convert", features = ["convert-chrono"] } aws-sdk-apigateway = { path = "../../build/aws-sdk/sdk/apigateway" } tokio = { version = "1", features = ["full"] } structopt = { version = "0.3", default-features = false } diff --git a/aws/sdk/examples/apigateway/src/bin/get_rest_apis.rs b/aws/sdk/examples/apigateway/src/bin/get_rest_apis.rs index 37eccb390..5e91cf536 100644 --- a/aws/sdk/examples/apigateway/src/bin/get_rest_apis.rs +++ b/aws/sdk/examples/apigateway/src/bin/get_rest_apis.rs @@ -5,6 +5,7 @@ use aws_config::meta::region::RegionProviderChain; use aws_sdk_apigateway::{Client, Error, Region, PKG_VERSION}; +use aws_smithy_types_convert::date_time::DateTimeExt; use structopt::StructOpt; #[derive(Debug, StructOpt)] @@ -27,7 +28,10 @@ async fn show_apis(client: &Client) -> Result<(), Error> { println!("Name: {}", api.name().unwrap_or_default()); println!("Description: {}", api.description().unwrap_or_default()); println!("Version: {}", api.version().unwrap_or_default()); - println!("Created: {}", api.created_date().unwrap().to_chrono()); + println!( + "Created: {}", + api.created_date().unwrap().to_chrono_utc() + ); println!(); } diff --git a/aws/sdk/examples/cognitoidentity/Cargo.toml b/aws/sdk/examples/cognitoidentity/Cargo.toml index 3061d266c..b8b5b3708 100644 --- a/aws/sdk/examples/cognitoidentity/Cargo.toml +++ b/aws/sdk/examples/cognitoidentity/Cargo.toml @@ -8,7 +8,8 @@ edition = "2018" [dependencies] aws-config = { path = "../../build/aws-sdk/sdk/aws-config" } -cognitoidentity = { package = "aws-sdk-cognitoidentity", path = "../../build/aws-sdk/sdk/cognitoidentity" } +aws-smithy-types-convert = { path = "../../build/aws-sdk/sdk/aws-smithy-types-convert", features = ["convert-chrono"] } +aws-sdk-cognitoidentity = { path = "../../build/aws-sdk/sdk/cognitoidentity" } aws-types = { path = "../../build/aws-sdk/sdk/aws-types" } tokio = { version = "1", features = ["full"] } chrono = "0.4" diff --git a/aws/sdk/examples/cognitoidentity/src/bin/describe-identity-pool.rs b/aws/sdk/examples/cognitoidentity/src/bin/describe-identity-pool.rs index 8681acf9d..56a1f12fd 100644 --- a/aws/sdk/examples/cognitoidentity/src/bin/describe-identity-pool.rs +++ b/aws/sdk/examples/cognitoidentity/src/bin/describe-identity-pool.rs @@ -4,7 +4,7 @@ */ use aws_config::meta::region::RegionProviderChain; -use cognitoidentity::{Client, Error, Region, PKG_VERSION}; +use aws_sdk_cognitoidentity::{Client, Error, Region, PKG_VERSION}; use structopt::StructOpt; #[derive(Debug, StructOpt)] @@ -64,16 +64,16 @@ async fn main() -> Result<(), Error> { .send() .await?; - let allow_classic = response.allow_classic_flow.unwrap_or_default(); - let allow_unauth_ids = response.allow_unauthenticated_identities; + let allow_classic = response.allow_classic_flow().unwrap_or_default(); + let allow_unauth_ids = response.allow_unauthenticated_identities(); println!(" Allow classic flow {}", allow_classic); println!(" Allow unauthenticated identities: {}", allow_unauth_ids); - if let Some(providers) = response.cognito_identity_providers { + if let Some(providers) = response.cognito_identity_providers() { println!(" Identity Providers:"); for provider in providers { - let client_id = provider.client_id.unwrap_or_default(); - let name = provider.provider_name.unwrap_or_default(); - let server_side_check = provider.server_side_token_check.unwrap_or_default(); + let client_id = provider.client_id().unwrap_or_default(); + let name = provider.provider_name().unwrap_or_default(); + let server_side_check = provider.server_side_token_check().unwrap_or_default(); println!(" Client ID: {}", client_id); println!(" Name: {}", name); @@ -82,15 +82,15 @@ async fn main() -> Result<(), Error> { } } - let developer_provider = response.developer_provider_name.unwrap_or_default(); - let id = response.identity_pool_id.unwrap_or_default(); - let name = response.identity_pool_name.unwrap_or_default(); + let developer_provider = response.developer_provider_name().unwrap_or_default(); + let id = response.identity_pool_id().unwrap_or_default(); + let name = response.identity_pool_name().unwrap_or_default(); println!(" Developer provider: {}", developer_provider); println!(" Identity pool ID: {}", id); println!(" Identity pool name: {}", name); - if let Some(tags) = response.identity_pool_tags { + if let Some(tags) = response.identity_pool_tags() { println!(" Tags:"); for (key, value) in tags { println!(" key: {}", key); @@ -98,14 +98,14 @@ async fn main() -> Result<(), Error> { } } - if let Some(open_id_arns) = response.open_id_connect_provider_ar_ns { + if let Some(open_id_arns) = response.open_id_connect_provider_ar_ns() { println!(" Open ID provider ARNs:"); for arn in open_id_arns { println!(" {}", arn); } } - if let Some(saml_arns) = response.saml_provider_ar_ns { + if let Some(saml_arns) = response.saml_provider_ar_ns() { println!(" SAML provider ARNs:"); for arn in saml_arns { println!(" {}", arn); @@ -113,7 +113,7 @@ async fn main() -> Result<(), Error> { } // SupportedLoginProviders - if let Some(login_providers) = response.supported_login_providers { + if let Some(login_providers) = response.supported_login_providers() { println!(" Supported login providers:"); for (key, value) in login_providers { println!(" key: {}", key); diff --git a/aws/sdk/examples/cognitoidentity/src/bin/list-identity-pools.rs b/aws/sdk/examples/cognitoidentity/src/bin/list-identity-pools.rs index e0fc447dc..86c396f02 100644 --- a/aws/sdk/examples/cognitoidentity/src/bin/list-identity-pools.rs +++ b/aws/sdk/examples/cognitoidentity/src/bin/list-identity-pools.rs @@ -4,7 +4,7 @@ */ use aws_config::meta::region::RegionProviderChain; -use cognitoidentity::{Client, Error, Region, PKG_VERSION}; +use aws_sdk_cognitoidentity::{Client, Error, Region, PKG_VERSION}; use structopt::StructOpt; #[derive(Debug, StructOpt)] @@ -52,18 +52,18 @@ async fn main() -> Result<(), Error> { let response = client.list_identity_pools().max_results(10).send().await?; // Print IDs and names of pools. - if let Some(pools) = response.identity_pools { + if let Some(pools) = response.identity_pools() { println!("Identity pools:"); for pool in pools { - let id = pool.identity_pool_id.unwrap_or_default(); - let name = pool.identity_pool_name.unwrap_or_default(); + let id = pool.identity_pool_id().unwrap_or_default(); + let name = pool.identity_pool_name().unwrap_or_default(); println!(" Identity pool ID: {}", id); println!(" Identity pool name: {}", name); println!(); } } - println!("Next token: {:?}", response.next_token); + println!("Next token: {:?}", response.next_token()); Ok(()) } diff --git a/aws/sdk/examples/cognitoidentity/src/bin/list-pool-identities.rs b/aws/sdk/examples/cognitoidentity/src/bin/list-pool-identities.rs index aac3ca589..35a4d9ae6 100644 --- a/aws/sdk/examples/cognitoidentity/src/bin/list-pool-identities.rs +++ b/aws/sdk/examples/cognitoidentity/src/bin/list-pool-identities.rs @@ -4,7 +4,8 @@ */ use aws_config::meta::region::RegionProviderChain; -use cognitoidentity::{Client, Error, Region, PKG_VERSION}; +use aws_sdk_cognitoidentity::{Client, Error, Region, PKG_VERSION}; +use aws_smithy_types_convert::date_time::DateTimeExt; use structopt::StructOpt; #[derive(Debug, StructOpt)] @@ -66,18 +67,18 @@ async fn main() -> Result<(), Error> { .send() .await?; - if let Some(ids) = response.identities { + if let Some(ids) = response.identities() { println!("Identitities:"); for id in ids { - let creation_timestamp = id.creation_date.unwrap().to_chrono(); - let idid = id.identity_id.unwrap_or_default(); - let mod_timestamp = id.last_modified_date.unwrap().to_chrono(); + let creation_timestamp = id.creation_date().unwrap().to_chrono_utc(); + let idid = id.identity_id().unwrap_or_default(); + let mod_timestamp = id.last_modified_date().unwrap().to_chrono_utc(); println!(" Creation date: {}", creation_timestamp); println!(" ID: {}", idid); println!(" Last modified date: {}", mod_timestamp); println!(" Logins:"); - for login in id.logins.unwrap_or_default() { + for login in id.logins().unwrap_or_default() { println!(" {}", login); } @@ -85,7 +86,7 @@ async fn main() -> Result<(), Error> { } } - println!("Next token: {:?}", response.next_token); + println!("Next token: {:?}", response.next_token()); println!(); diff --git a/aws/sdk/examples/cognitoidentityprovider/Cargo.toml b/aws/sdk/examples/cognitoidentityprovider/Cargo.toml index 09d7482a0..235ed1a1e 100644 --- a/aws/sdk/examples/cognitoidentityprovider/Cargo.toml +++ b/aws/sdk/examples/cognitoidentityprovider/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" [dependencies] aws-config = { path = "../../build/aws-sdk/sdk/aws-config" } +aws-smithy-types-convert = { path = "../../build/aws-sdk/sdk/aws-smithy-types-convert", features = ["convert-chrono"] } aws-sdk-cognitoidentityprovider = { package = "aws-sdk-cognitoidentityprovider", path = "../../build/aws-sdk/sdk/cognitoidentityprovider" } aws-types = { path = "../../build/aws-sdk/sdk/aws-types" } tokio = { version = "1", features = ["full"] } diff --git a/aws/sdk/examples/cognitoidentityprovider/src/bin/list-user-pools.rs b/aws/sdk/examples/cognitoidentityprovider/src/bin/list-user-pools.rs index 96d1169c6..4a1375398 100644 --- a/aws/sdk/examples/cognitoidentityprovider/src/bin/list-user-pools.rs +++ b/aws/sdk/examples/cognitoidentityprovider/src/bin/list-user-pools.rs @@ -5,6 +5,7 @@ use aws_config::meta::region::RegionProviderChain; use aws_sdk_cognitoidentityprovider::{Client, Error, Region, PKG_VERSION}; +use aws_smithy_types_convert::date_time::DateTimeExt; use structopt::StructOpt; @@ -48,25 +49,25 @@ async fn main() -> Result<(), Error> { } let response = client.list_user_pools().max_results(10).send().await?; - if let Some(pools) = response.user_pools { + if let Some(pools) = response.user_pools() { println!("User pools:"); for pool in pools { - println!(" ID: {}", pool.id.unwrap_or_default()); - println!(" Name: {}", pool.name.unwrap_or_default()); - println!(" Status: {:?}", pool.status); - println!(" Lambda Config: {:?}", pool.lambda_config.unwrap()); + println!(" ID: {}", pool.id().unwrap_or_default()); + println!(" Name: {}", pool.name().unwrap_or_default()); + println!(" Status: {:?}", pool.status()); + println!(" Lambda Config: {:?}", pool.lambda_config().unwrap()); println!( " Last modified: {}", - pool.last_modified_date.unwrap().to_chrono() + pool.last_modified_date().unwrap().to_chrono_utc() ); println!( " Creation date: {:?}", - pool.creation_date.unwrap().to_chrono() + pool.creation_date().unwrap().to_chrono_utc() ); println!(); } } - println!("Next token: {}", response.next_token.unwrap_or_default()); + println!("Next token: {}", response.next_token().unwrap_or_default()); Ok(()) } diff --git a/aws/sdk/examples/cognitosync/Cargo.toml b/aws/sdk/examples/cognitosync/Cargo.toml index 97d665537..f155fff8f 100644 --- a/aws/sdk/examples/cognitosync/Cargo.toml +++ b/aws/sdk/examples/cognitosync/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" [dependencies] aws-config = { path = "../../build/aws-sdk/sdk/aws-config" } +aws-smithy-types-convert = { path = "../../build/aws-sdk/sdk/aws-smithy-types-convert", features = ["convert-chrono"] } aws-sdk-cognitosync = { package = "aws-sdk-cognitosync", path = "../../build/aws-sdk/sdk/cognitosync" } aws-types = { path = "../../build/aws-sdk/sdk/aws-types" } tokio = { version = "1", features = ["full"] } diff --git a/aws/sdk/examples/cognitosync/src/bin/list-identity-pool-usage.rs b/aws/sdk/examples/cognitosync/src/bin/list-identity-pool-usage.rs index c75e5c1d0..23c9354bb 100644 --- a/aws/sdk/examples/cognitosync/src/bin/list-identity-pool-usage.rs +++ b/aws/sdk/examples/cognitosync/src/bin/list-identity-pool-usage.rs @@ -5,6 +5,7 @@ use aws_config::meta::region::RegionProviderChain; use aws_sdk_cognitosync::{Client, Error, Region, PKG_VERSION}; +use aws_smithy_types_convert::date_time::DateTimeExt; use structopt::StructOpt; @@ -56,31 +57,31 @@ async fn main() -> Result<(), Error> { .send() .await?; - if let Some(pools) = response.identity_pool_usages { + if let Some(pools) = response.identity_pool_usages() { println!("Identity pools:"); for pool in pools { println!( " Identity pool ID: {}", - pool.identity_pool_id.unwrap_or_default() + pool.identity_pool_id().unwrap_or_default() ); println!( " Data storage: {}", - pool.data_storage.unwrap_or_default() + pool.data_storage().unwrap_or_default() ); println!( " Sync sessions count: {}", - pool.sync_sessions_count.unwrap_or_default() + pool.sync_sessions_count().unwrap_or_default() ); println!( " Last modified: {}", - pool.last_modified_date.unwrap().to_chrono() + pool.last_modified_date().unwrap().to_chrono_utc() ); println!(); } } - println!("Next token: {:?}", response.next_token); + println!("Next token: {:?}", response.next_token()); Ok(()) } diff --git a/aws/sdk/examples/sagemaker/Cargo.toml b/aws/sdk/examples/sagemaker/Cargo.toml index fa8dcf5c1..4ce33dc10 100644 --- a/aws/sdk/examples/sagemaker/Cargo.toml +++ b/aws/sdk/examples/sagemaker/Cargo.toml @@ -8,7 +8,8 @@ edition = "2018" [dependencies] aws-config = { path = "../../build/aws-sdk/sdk/aws-config" } -sagemaker = {package = "aws-sdk-sagemaker", path = "../../build/aws-sdk/sdk/sagemaker"} +aws-sdk-sagemaker = { path = "../../build/aws-sdk/sdk/sagemaker"} +aws-smithy-types-convert = { path = "../../build/aws-sdk/sdk/aws-smithy-types-convert", features = ["convert-chrono"] } aws-types = { path = "../../build/aws-sdk/sdk/aws-types" } tokio = { version = "1", features = ["full"] } diff --git a/aws/sdk/examples/sagemaker/src/bin/list-training-jobs.rs b/aws/sdk/examples/sagemaker/src/bin/list-training-jobs.rs index c13faee0a..618cfbd1e 100644 --- a/aws/sdk/examples/sagemaker/src/bin/list-training-jobs.rs +++ b/aws/sdk/examples/sagemaker/src/bin/list-training-jobs.rs @@ -4,9 +4,9 @@ */ use aws_config::meta::region::RegionProviderChain; - +use aws_sdk_sagemaker as sagemaker; +use aws_smithy_types_convert::date_time::DateTimeExt; use sagemaker::{Client, Region}; - use structopt::StructOpt; #[derive(Debug, StructOpt)] @@ -54,12 +54,12 @@ async fn main() -> Result<(), sagemaker::Error> { let job_details = client.list_training_jobs().send().await?; println!("Job Name\tCreation DateTime\tDuration\tStatus"); - for j in job_details.training_job_summaries.unwrap_or_default() { - let name = j.training_job_name.as_deref().unwrap_or_default(); - let creation_time = j.creation_time.unwrap().to_chrono(); - let training_end_time = j.training_end_time.unwrap().to_chrono(); + for j in job_details.training_job_summaries().unwrap_or_default() { + let name = j.training_job_name().unwrap_or_default(); + let creation_time = j.creation_time().unwrap().to_chrono_utc(); + let training_end_time = j.training_end_time().unwrap().to_chrono_utc(); - let status = j.training_job_status.unwrap(); + let status = j.training_job_status().unwrap(); let duration = training_end_time - creation_time; println!( diff --git a/aws/sdk/examples/sagemaker/src/bin/sagemaker-helloworld.rs b/aws/sdk/examples/sagemaker/src/bin/sagemaker-helloworld.rs index 9d8c6b34d..b5f65d4dd 100644 --- a/aws/sdk/examples/sagemaker/src/bin/sagemaker-helloworld.rs +++ b/aws/sdk/examples/sagemaker/src/bin/sagemaker-helloworld.rs @@ -4,9 +4,8 @@ */ use aws_config::meta::region::RegionProviderChain; - +use aws_sdk_sagemaker as sagemaker; use sagemaker::{Client, Region}; - use structopt::StructOpt; #[derive(Debug, StructOpt)] @@ -52,10 +51,10 @@ async fn main() -> Result<(), sagemaker::Error> { let notebooks = client.list_notebook_instances().send().await?; - for n in notebooks.notebook_instances.unwrap_or_default() { - let n_instance_type = n.instance_type.unwrap(); - let n_status = n.notebook_instance_status.unwrap(); - let n_name = n.notebook_instance_name.as_deref().unwrap_or_default(); + for n in notebooks.notebook_instances().unwrap_or_default() { + let n_instance_type = n.instance_type().unwrap(); + let n_status = n.notebook_instance_status().unwrap(); + let n_name = n.notebook_instance_name().unwrap_or_default(); println!( "Notebook Name : {}, Notebook Status : {:#?}, Notebook Instance Type : {:#?}", diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerHttpProtocolGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerHttpProtocolGenerator.kt index d70be7f32..2ecbc6946 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerHttpProtocolGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerHttpProtocolGenerator.kt @@ -187,12 +187,12 @@ private class ServerHttpProtocolImplGenerator( if (operationShape.errors.isNotEmpty()) { rustTemplate( """ - impl #{SerializeHttpError} for $operationName { - type Output = std::result::Result<#{http}::Response<#{Bytes}>, #{Error}>; - type Struct = #{E}; - fn serialize(&self, error: &Self::Struct) -> Self::Output { - #{serialize_error}(error) - } + impl #{SerializeHttpError} for $operationName { + type Output = std::result::Result<#{http}::Response<#{Bytes}>, #{Error}>; + type Struct = #{E}; + fn serialize(&self, error: &Self::Struct) -> Self::Output { + #{serialize_error}(error) + } }""", *codegenScope, "E" to errorSymbol, @@ -569,7 +569,7 @@ private class ServerHttpProtocolImplGenerator( let value = #{PercentEncoding}::percent_decode_str(value) .decode_utf8() .map_err(|err| #{Error}::DeserializeLabel(err.to_string()))?; - let value = #{Instant}::Instant::from_str(&value, #{format}) + let value = #{DateTime}::DateTime::from_str(&value, #{format}) .map_err(|err| #{Error}::DeserializeLabel(err.to_string()))?; Ok(Some(value)) """.trimIndent(), diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustTypes.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustTypes.kt index 6fd6ee482..9de53b193 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustTypes.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustTypes.kt @@ -151,9 +151,9 @@ fun RustType.render(fullyQualified: Boolean = true): String { } /** - * Returns true if [this] contains [t] anywhere within it's tree. For example, - * Option.contains(Instant) would return true. - * Option.contains(Blob) would return false. + * Returns true if [this] contains [t] anywhere within its tree. For example, + * Option.contains(DateTime) would return true. + * Option.contains(Blob) would return false. */ fun RustType.contains(t: T): Boolean = when (this) { t -> true diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt index 0d648de50..182e6b332 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/RuntimeTypes.kt @@ -177,8 +177,8 @@ data class RuntimeType(val name: String?, val dependency: RustDependency?, val n val StdError = RuntimeType("Error", dependency = null, namespace = "std::error") val String = RuntimeType("String", dependency = null, namespace = "std::string") - fun Instant(runtimeConfig: RuntimeConfig) = - RuntimeType("Instant", CargoDependency.SmithyTypes(runtimeConfig), "${runtimeConfig.crateSrcPrefix}_types") + fun DateTime(runtimeConfig: RuntimeConfig) = + RuntimeType("DateTime", CargoDependency.SmithyTypes(runtimeConfig), "${runtimeConfig.crateSrcPrefix}_types") fun GenericError(runtimeConfig: RuntimeConfig) = RuntimeType("Error", CargoDependency.SmithyTypes(runtimeConfig), "${runtimeConfig.crateSrcPrefix}_types") @@ -219,7 +219,7 @@ data class RuntimeType(val name: String?, val dependency: RustDependency?, val n return RuntimeType( timestampFormat, CargoDependency.SmithyTypes(runtimeConfig), - "${runtimeConfig.crateSrcPrefix}_types::instant::Format" + "${runtimeConfig.crateSrcPrefix}_types::date_time::Format" ) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt index b7a31e5a3..5bfaa46c0 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/SymbolVisitor.kt @@ -286,7 +286,7 @@ class SymbolVisitor( } override fun timestampShape(shape: TimestampShape?): Symbol { - return RuntimeType.Instant(config.runtimeConfig).toSymbol() + return RuntimeType.DateTime(config.runtimeConfig).toSymbol() } private fun symbolBuilder(shape: Shape?, rustType: RustType): Symbol.Builder { diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/SmithyTypesPubUseGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/SmithyTypesPubUseGenerator.kt index 74f165f8f..1d650165c 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/SmithyTypesPubUseGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/customizations/SmithyTypesPubUseGenerator.kt @@ -16,6 +16,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.LibRsSection fun pubUseTypes(runtimeConfig: RuntimeConfig) = listOf( RuntimeType.Blob(runtimeConfig), + RuntimeType.DateTime(runtimeConfig), CargoDependency.SmithyHttp(runtimeConfig).asType().member("result::SdkError"), CargoDependency.SmithyHttp(runtimeConfig).asType().member("byte_stream::ByteStream"), ) @@ -26,6 +27,7 @@ class SmithyTypesPubUseGenerator(private val runtimeConfig: RuntimeConfig) : Lib LibRsSection.Body -> pubUseTypes(runtimeConfig).forEach { rust("pub use #T;", it) } + else -> { } } } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/Instantiator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/Instantiator.kt index cfbd2e03b..a83f32695 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/Instantiator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/Instantiator.kt @@ -90,8 +90,8 @@ class Instantiator( // Wrapped Shapes is TimestampShape -> writer.write( - "#T::from_epoch_seconds(${(arg as NumberNode).value})", - RuntimeType.Instant(runtimeConfig) + "#T::from_secs(${(arg as NumberNode).value})", + RuntimeType.DateTime(runtimeConfig) ) /** diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RequestBindingGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RequestBindingGenerator.kt index 75b1616cb..dcb8140d3 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RequestBindingGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/RequestBindingGenerator.kt @@ -120,7 +120,7 @@ class RequestBindingGenerator( write("let mut uri = String::new();") write("uri_base(input, &mut uri)?;") if (hasQuery) { - write("uri_query(input, &mut uri);") + write("uri_query(input, &mut uri)?;") } if (hasHeaders) { write("let builder = add_headers(input, builder)?;") @@ -257,7 +257,7 @@ class RequestBindingGenerator( val timestampFormat = index.determineTimestampFormat(member, HttpBinding.Location.HEADER, defaultTimestampFormat) val timestampFormatType = RuntimeType.TimestampFormat(runtimeConfig, timestampFormat) - "$targetName.fmt(${writer.format(timestampFormatType)})" + "$targetName.fmt(${writer.format(timestampFormatType)})?" } target.isListShape || target.isMemberShape -> { throw IllegalArgumentException("lists should be handled at a higher level") @@ -322,7 +322,10 @@ class RequestBindingGenerator( return false } val preloadedParams = literalParams.keys + dynamicParams.map { it.locationName } - writer.rustBlockTemplate("fn uri_query(_input: &#{Input}, mut output: &mut String)", *codegenScope) { + writer.rustBlockTemplate( + "fn uri_query(_input: &#{Input}, mut output: &mut String) -> Result<(), #{BuildError}>", + *codegenScope + ) { write("let mut query = #T::new(&mut output);", RuntimeType.QueryFormat(runtimeConfig, "Writer")) literalParams.forEach { (k, v) -> // When `v` is an empty string, no value should be set. @@ -372,6 +375,7 @@ class RequestBindingGenerator( } } } + writer.rust("Ok(())") } return true } @@ -390,7 +394,7 @@ class RequestBindingGenerator( index.determineTimestampFormat(member, HttpBinding.Location.QUERY, defaultTimestampFormat) val timestampFormatType = RuntimeType.TimestampFormat(runtimeConfig, timestampFormat) val func = writer.format(RuntimeType.QueryFormat(runtimeConfig, "fmt_timestamp")) - "&$func($targetName, ${writer.format(timestampFormatType)})" + "&$func($targetName, ${writer.format(timestampFormatType)})?" } target.isListShape || target.isMemberShape -> { throw IllegalArgumentException("lists should be handled at a higher level") @@ -426,7 +430,7 @@ class RequestBindingGenerator( index.determineTimestampFormat(member, HttpBinding.Location.LABEL, defaultTimestampFormat) val timestampFormatType = RuntimeType.TimestampFormat(runtimeConfig, timestampFormat) val func = format(RuntimeType.LabelFormat(runtimeConfig, "fmt_timestamp")) - rust("let $outputVar = $func($input, ${format(timestampFormatType)});") + rust("let $outputVar = $func($input, ${format(timestampFormatType)})?;") } else -> { rust( diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt index 4ab7b7fc4..6a702d112 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/generators/http/ResponseBindingGenerator.kt @@ -58,7 +58,7 @@ class ResponseBindingGenerator( private val index = HttpBindingIndex.of(model) private val headerUtil = CargoDependency.SmithyHttp(runtimeConfig).asType().member("header") private val defaultTimestampFormat = TimestampFormatTrait.Format.EPOCH_SECONDS - private val instant = RuntimeType.Instant(runtimeConfig).toSymbol().rustType() + private val dateTime = RuntimeType.DateTime(runtimeConfig).toSymbol().rustType() private val httpSerdeModule = RustModule.private("http_serde") /** @@ -264,7 +264,7 @@ class ResponseBindingGenerator( rustType to targetType } val parsedValue = safeName() - if (coreType == instant) { + if (coreType == dateTime) { val timestampFormat = index.determineTimestampFormat( memberShape, diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt index f7b414437..190c3a145 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt @@ -605,7 +605,7 @@ class XmlBindingTraitParserGenerator( TimestampFormatTrait.Format.DATE_TIME ) val timestampFormatType = RuntimeType.TimestampFormat(runtimeConfig, timestampFormat) - withBlock("#T::from_str(", ")", RuntimeType.Instant(runtimeConfig)) { + withBlock("#T::from_str(", ")", RuntimeType.DateTime(runtimeConfig)) { provider() rust(", #T", timestampFormatType) } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt index 6388cc253..83386c484 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/JsonSerializerGenerator.kt @@ -337,7 +337,7 @@ class JsonSerializerGenerator( val timestampFormat = httpBindingResolver.timestampFormat(context.shape, HttpLocation.DOCUMENT, EPOCH_SECONDS) val timestampFormatType = RuntimeType.TimestampFormat(runtimeConfig, timestampFormat) - rust("$writer.instant(${value.name}, #T);", timestampFormatType) + rust("$writer.date_time(${value.name}, #T)?;", timestampFormatType) } is CollectionShape -> jsonArrayWriter(context) { arrayName -> serializeCollection(Context(arrayName, context.valueExpression, target)) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/QuerySerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/QuerySerializerGenerator.kt index cd7e8613a..8a7512cbc 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/QuerySerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/QuerySerializerGenerator.kt @@ -229,7 +229,7 @@ abstract class QuerySerializerGenerator(codegenContext: CodegenContext) : Struct is TimestampShape -> { val timestampFormat = determineTimestampFormat(context.shape) val timestampFormatType = RuntimeType.TimestampFormat(runtimeConfig, timestampFormat) - rust("$writer.instant(${value.name}, #T);", timestampFormatType) + rust("$writer.date_time(${value.name}, #T)?;", timestampFormatType) } is CollectionShape -> serializeCollection(context, Context(writer, context.valueExpression, target)) is MapShape -> serializeMap(context, Context(writer, context.valueExpression, target)) diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt index aa7e607c9..198797415 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt @@ -245,7 +245,7 @@ class XmlBindingTraitSerializerGenerator( TimestampFormatTrait.Format.DATE_TIME ) val timestampFormatType = RuntimeType.TimestampFormat(runtimeConfig, timestampFormat) - rust("$input.fmt(#T).as_ref()", timestampFormatType) + rust("$input.fmt(#T)?.as_ref()", timestampFormatType) } else -> TODO(member.toString()) } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/SymbolBuilderTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/SymbolBuilderTest.kt index 5c5ea6a8b..145bb4e54 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/SymbolBuilderTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/SymbolBuilderTest.kt @@ -221,8 +221,8 @@ class SymbolBuilderTest { .unwrap() val provider: SymbolProvider = testSymbolProvider(model) val sym = provider.toSymbol(member) - sym.rustType().render(false) shouldBe "Option" - sym.referenceClosure().map { it.name } shouldContain "Instant" + sym.rustType().render(false) shouldBe "Option" + sym.referenceClosure().map { it.name } shouldContain "DateTime" sym.references[0].dependencies.shouldNotBeEmpty() } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt index 11586a03e..9f948470d 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/StructureGeneratorTest.kt @@ -258,7 +258,7 @@ class StructureGeneratorTest { """ let _: Option<&str> = one.field_string(); let _: Option<&aws_smithy_types::Blob> = one.field_blob(); - let _: Option<&aws_smithy_types::instant::Instant> = one.field_timestamp(); + let _: Option<&aws_smithy_types::DateTime> = one.field_timestamp(); let _: Option<&aws_smithy_types::Document> = one.field_document(); let _: Option = one.field_boolean(); let _: bool = one.field_primitive_boolean(); diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/RequestBindingGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/RequestBindingGeneratorTest.kt index 36e96f36c..80b94a9aa 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/RequestBindingGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/generators/http/RequestBindingGeneratorTest.kt @@ -127,7 +127,10 @@ class RequestBindingGeneratorTest { // some wrappers that can be called directly from the tests. The functions will get duplicated, // but that's not a problem. - rustBlock("pub fn test_uri_query(&self, mut output: &mut String)") { + rustBlock( + "pub fn test_uri_query(&self, mut output: &mut String) -> Result<(), #T>", + TestRuntimeConfig.operationBuildError() + ) { bindingGen.renderUpdateHttpBuilder(this) rust("uri_query(self, output)") } @@ -164,7 +167,7 @@ class RequestBindingGeneratorTest { renderOperation(writer) writer.compileAndTest( """ - let ts = aws_smithy_types::Instant::from_epoch_seconds(10123125); + let ts = aws_smithy_types::DateTime::from_secs(10123125); let inp = PutObjectInput::builder() .bucket_name("somebucket/ok") .key(ts.clone()) @@ -188,7 +191,7 @@ class RequestBindingGeneratorTest { renderOperation(writer) writer.compileAndTest( """ - let ts = aws_smithy_types::Instant::from_epoch_seconds(10123125); + let ts = aws_smithy_types::DateTime::from_secs(10123125); let inp = PutObjectInput::builder() .bucket_name("somebucket/ok") .key(ts.clone()) @@ -210,7 +213,7 @@ class RequestBindingGeneratorTest { writer.compileAndTest( """ use std::collections::HashMap; - let ts = aws_smithy_types::Instant::from_epoch_seconds(10123125); + let ts = aws_smithy_types::DateTime::from_secs(10123125); let inp = PutObjectInput::builder() .bucket_name("buk") .set_date_header_list(Some(vec![ts.clone()])) @@ -247,7 +250,7 @@ class RequestBindingGeneratorTest { writer.compileAndTest( """ use std::collections::HashMap; - let ts = aws_smithy_types::Instant::from_epoch_seconds(10123125); + let ts = aws_smithy_types::DateTime::from_secs(10123125); let inp = PutObjectInput::builder() .bucket_name("buk") .key(ts.clone()) @@ -266,7 +269,7 @@ class RequestBindingGeneratorTest { writer.compileAndTest( """ use std::collections::HashMap; - let ts = aws_smithy_types::Instant::from_epoch_seconds(10123125); + let ts = aws_smithy_types::DateTime::from_secs(10123125); let inp = PutObjectInput::builder() .bucket_name("buk") .key(ts.clone()) @@ -284,7 +287,7 @@ class RequestBindingGeneratorTest { renderOperation(writer) writer.compileAndTest( """ - let ts = aws_smithy_types::Instant::from_epoch_seconds(10123125); + let ts = aws_smithy_types::DateTime::from_secs(10123125); let inp = PutObjectInput::builder() .bucket_name("buk") .key(ts.clone()) @@ -303,7 +306,7 @@ class RequestBindingGeneratorTest { renderOperation(writer) writer.compileAndTest( """ - let ts = aws_smithy_types::Instant::from_epoch_seconds(10123125); + let ts = aws_smithy_types::DateTime::from_secs(10123125); let inp = PutObjectInput::builder() // don't set bucket // .bucket_name("buk") @@ -321,7 +324,7 @@ class RequestBindingGeneratorTest { renderOperation(writer) writer.compileAndTest( """ - let ts = aws_smithy_types::Instant::from_epoch_seconds(10123125); + let ts = aws_smithy_types::DateTime::from_secs(10123125); let inp = PutObjectInput::builder() .bucket_name("buk") // don't set key @@ -339,7 +342,7 @@ class RequestBindingGeneratorTest { renderOperation(writer) writer.compileAndTest( """ - let ts = aws_smithy_types::Instant::from_epoch_seconds(10123125); + let ts = aws_smithy_types::DateTime::from_secs(10123125); let inp = PutObjectInput::builder() .bucket_name("") .key(ts.clone()) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt index af5599a5a..70a9bfe08 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/EventStreamUnmarshallerGeneratorTest.kt @@ -49,7 +49,7 @@ class EventStreamUnmarshallerGeneratorTest { writer.rust( """ use aws_smithy_eventstream::frame::{Header, HeaderValue, Message, UnmarshallMessage, UnmarshalledMessage}; - use aws_smithy_types::{Blob, Instant}; + use aws_smithy_types::{Blob, DateTime}; use crate::error::*; use crate::model::*; @@ -86,15 +86,15 @@ class EventStreamUnmarshallerGeneratorTest { writer.unitTest( name = "message_with_blob", test = """ - let message = msg("event", "MessageWithBlob", "application/octet-stream", b"hello, world!"); - let result = ${writer.format(generator.render())}().unmarshall(&message); - assert!(result.is_ok(), "expected ok, got: {:?}", result); - assert_eq!( - TestStream::MessageWithBlob( - MessageWithBlob::builder().data(Blob::new(&b"hello, world!"[..])).build() - ), - expect_event(result.unwrap()) - ); + let message = msg("event", "MessageWithBlob", "application/octet-stream", b"hello, world!"); + let result = ${writer.format(generator.render())}().unmarshall(&message); + assert!(result.is_ok(), "expected ok, got: {:?}", result); + assert_eq!( + TestStream::MessageWithBlob( + MessageWithBlob::builder().data(Blob::new(&b"hello, world!"[..])).build() + ), + expect_event(result.unwrap()) + ); """, ) @@ -102,14 +102,14 @@ class EventStreamUnmarshallerGeneratorTest { writer.unitTest( "unknown_message", """ - let message = msg("event", "NewUnmodeledMessageType", "application/octet-stream", b"hello, world!"); - let result = ${writer.format(generator.render())}().unmarshall(&message); - assert!(result.is_ok(), "expected ok, got: {:?}", result); - assert_eq!( - TestStream::Unknown, - expect_event(result.unwrap()) - ); - """, + let message = msg("event", "NewUnmodeledMessageType", "application/octet-stream", b"hello, world!"); + let result = ${writer.format(generator.render())}().unmarshall(&message); + assert!(result.is_ok(), "expected ok, got: {:?}", result); + assert_eq!( + TestStream::Unknown, + expect_event(result.unwrap()) + ); + """, ) } @@ -180,7 +180,7 @@ class EventStreamUnmarshallerGeneratorTest { .add_header(Header::new("long", HeaderValue::Int64(9_000_000_000i64))) .add_header(Header::new("short", HeaderValue::Int16(16_000i16))) .add_header(Header::new("string", HeaderValue::String("test".into()))) - .add_header(Header::new("timestamp", HeaderValue::Timestamp(Instant::from_epoch_seconds(5)))); + .add_header(Header::new("timestamp", HeaderValue::Timestamp(DateTime::from_secs(5)))); let result = ${writer.format(generator.render())}().unmarshall(&message); assert!(result.is_ok(), "expected ok, got: {:?}", result); assert_eq!( @@ -192,7 +192,7 @@ class EventStreamUnmarshallerGeneratorTest { .long(9_000_000_000i64) .short(16_000i16) .string("test") - .timestamp(Instant::from_epoch_seconds(5)) + .timestamp(DateTime::from_secs(5)) .build() ), expect_event(result.unwrap()) diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGeneratorTest.kt index 0cdbf77fa..006273b9a 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/serialize/EventStreamMarshallerGeneratorTest.kt @@ -54,7 +54,7 @@ class EventStreamMarshallerGeneratorTest { """ use aws_smithy_eventstream::frame::{Message, Header, HeaderValue, MarshallMessage}; use std::collections::HashMap; - use aws_smithy_types::{Blob, Instant}; + use aws_smithy_types::{Blob, DateTime}; use crate::error::*; use crate::model::*; @@ -171,7 +171,7 @@ class EventStreamMarshallerGeneratorTest { .long(9_000_000_000i64) .short(16_000i16) .string("test") - .timestamp(Instant::from_epoch_seconds(5)) + .timestamp(DateTime::from_secs(5)) .build() ); let result = ${writer.format(generator.render())}().marshall(event); @@ -187,7 +187,7 @@ class EventStreamMarshallerGeneratorTest { .add_header(Header::new("long", HeaderValue::Int64(9_000_000_000i64))) .add_header(Header::new("short", HeaderValue::Int16(16_000i16))) .add_header(Header::new("string", HeaderValue::String("test".into()))) - .add_header(Header::new("timestamp", HeaderValue::Timestamp(Instant::from_epoch_seconds(5)))); + .add_header(Header::new("timestamp", HeaderValue::Timestamp(DateTime::from_secs(5)))); assert_eq!(expected_message, actual_message); """, ) diff --git a/design/src/smithy/simple_shapes.md b/design/src/smithy/simple_shapes.md index 6a6be9962..171196d0d 100644 --- a/design/src/smithy/simple_shapes.md +++ b/design/src/smithy/simple_shapes.md @@ -12,7 +12,7 @@ | double | `f64` | | [bigInteger](#big-numbers) | `BigInteger` (Not implemented yet) | | [bigDecimal](#big-numbers) | `BigDecimal` (Not implemented yet) | -| [timestamp](#timestamps) | [`Instant`](https://github.com/awslabs/smithy-rs/blob/main/rust-runtime/aws-smithy-types/src/instant/mod.rs) | +| [timestamp](#timestamps) | [`DateTime`](https://github.com/awslabs/smithy-rs/blob/main/rust-runtime/aws-smithy-types/src/date_time/mod.rs) | | [document](#documents) | [`Document`](https://github.com/awslabs/smithy-rs/blob/v0.14/rust-runtime/aws-smithy-types/src/lib.rs#L38-L52) | ### Big Numbers @@ -27,13 +27,13 @@ This will enable us to add helpers over time as requested. Users will also be ab As of 5/23/2021 BigInteger / BigDecimal are not included in AWS models. Implementation is tracked [here](https://github.com/awslabs/smithy-rs/issues/312). ### Timestamps -[chrono](https://github.com/chronotope/chrono) is the current de facto library for datetime in Rust, but it is pre-1.0. Instants are represented by an SDK defined structure modeled on `std::time::Duration` from the Rust standard library. +[chrono](https://github.com/chronotope/chrono) is the current de facto library for datetime in Rust, but it is pre-1.0. DateTimes are represented by an SDK defined structure modeled on `std::time::Duration` from the Rust standard library. ```rust -{{#include ../../../rust-runtime/aws-smithy-types/src/instant/mod.rs:instant}} +{{#include ../../../rust-runtime/aws-smithy-types/src/date_time/mod.rs:date_time}} ``` -A `to_chrono()` method on `Instant` enables conversion from SDK instants to `chrono` dates. +Functions in the `aws-smithy-types-convert` crate provide conversions to other crates, such as `time` or `chrono`. ### Strings Rust has two different String representations: diff --git a/rust-runtime/Cargo.toml b/rust-runtime/Cargo.toml index bce0a7421..54b111ca9 100644 --- a/rust-runtime/Cargo.toml +++ b/rust-runtime/Cargo.toml @@ -11,6 +11,7 @@ members = [ "aws-smithy-protocol-test", "aws-smithy-query", "aws-smithy-types", + "aws-smithy-types-convert", "aws-smithy-xml", "aws-smithy-http-server" ] diff --git a/rust-runtime/aws-smithy-eventstream/fuzz/fuzz_targets/mutated_headers.rs b/rust-runtime/aws-smithy-eventstream/fuzz/fuzz_targets/mutated_headers.rs index 64c6737ff..ef5af62a3 100644 --- a/rust-runtime/aws-smithy-eventstream/fuzz/fuzz_targets/mutated_headers.rs +++ b/rust-runtime/aws-smithy-eventstream/fuzz/fuzz_targets/mutated_headers.rs @@ -6,7 +6,7 @@ #![no_main] use aws_smithy_eventstream::frame::{Header, HeaderValue, Message}; -use aws_smithy_types::Instant; +use aws_smithy_types::DateTime; use bytes::{Buf, BufMut}; use crc32fast::Hasher as Crc; use libfuzzer_sys::{fuzz_mutator, fuzz_target}; @@ -35,7 +35,7 @@ fn mutate(data: &mut [u8], size: usize, max_size: usize) -> usize { .add_header(Header::new("str", HeaderValue::String("some str".into()))) .add_header(Header::new( "time", - HeaderValue::Timestamp(Instant::from_epoch_seconds(5_000_000_000)), + HeaderValue::Timestamp(DateTime::from_secs(5_000_000_000)), )) .add_header(Header::new( "uuid", diff --git a/rust-runtime/aws-smithy-eventstream/src/error.rs b/rust-runtime/aws-smithy-eventstream/src/error.rs index 05911c384..023e8d511 100644 --- a/rust-runtime/aws-smithy-eventstream/src/error.rs +++ b/rust-runtime/aws-smithy-eventstream/src/error.rs @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0. */ -use aws_smithy_types::Instant; +use aws_smithy_types::DateTime; use std::error::Error as StdError; use std::fmt; @@ -22,7 +22,7 @@ pub enum Error { MessageTooLong, PayloadTooLong, PreludeChecksumMismatch(u32, u32), - TimestampValueTooLarge(Instant), + TimestampValueTooLarge(DateTime), Marshalling(String), Unmarshalling(String), } diff --git a/rust-runtime/aws-smithy-eventstream/src/frame.rs b/rust-runtime/aws-smithy-eventstream/src/frame.rs index 9d8fa3cca..9783d5635 100644 --- a/rust-runtime/aws-smithy-eventstream/src/frame.rs +++ b/rust-runtime/aws-smithy-eventstream/src/frame.rs @@ -62,7 +62,7 @@ mod value { use crate::error::Error; use crate::frame::checked; use crate::str_bytes::StrBytes; - use aws_smithy_types::Instant; + use aws_smithy_types::DateTime; use bytes::{Buf, BufMut, Bytes}; use std::convert::TryInto; use std::mem::size_of; @@ -89,7 +89,7 @@ mod value { Int64(i64), ByteArray(Bytes), String(StrBytes), - Timestamp(Instant), + Timestamp(DateTime), Uuid(u128), } @@ -143,7 +143,7 @@ mod value { } } - pub fn as_timestamp(&self) -> Result { + pub fn as_timestamp(&self) -> Result { match self { HeaderValue::Timestamp(value) => Ok(*value), _ => Err(self), @@ -199,9 +199,7 @@ mod value { TYPE_TIMESTAMP => { if buffer.remaining() >= size_of::() { let epoch_millis = buffer.get_i64(); - Ok(HeaderValue::Timestamp(Instant::from_epoch_millis( - epoch_millis, - ))) + Ok(HeaderValue::Timestamp(DateTime::from_millis(epoch_millis))) } else { Err(Error::InvalidHeaderValue) } @@ -244,7 +242,7 @@ mod value { Timestamp(time) => { buffer.put_u8(TYPE_TIMESTAMP); buffer.put_i64( - time.to_epoch_millis() + time.to_millis() .map_err(|_| Error::TimestampValueTooLarge(*time))?, ); } @@ -273,7 +271,7 @@ mod value { } TYPE_STRING => HeaderValue::String(StrBytes::from(String::arbitrary(unstruct)?)), TYPE_TIMESTAMP => { - HeaderValue::Timestamp(Instant::from_epoch_seconds(i64::arbitrary(unstruct)?)) + HeaderValue::Timestamp(DateTime::from_secs(i64::arbitrary(unstruct)?)) } TYPE_UUID => HeaderValue::Uuid(u128::arbitrary(unstruct)?), _ => unreachable!(), @@ -526,7 +524,7 @@ fn payload_len(total_len: u32, header_len: u32) -> Result { mod message_tests { use crate::error::Error; use crate::frame::{Header, HeaderValue, Message}; - use aws_smithy_types::Instant; + use aws_smithy_types::DateTime; use bytes::Bytes; macro_rules! read_message_expect_err { @@ -639,7 +637,7 @@ mod message_tests { Header::new("str", HeaderValue::String("some str".into())), Header::new( "time", - HeaderValue::Timestamp(Instant::from_epoch_seconds(5_000_000)) + HeaderValue::Timestamp(DateTime::from_secs(5_000_000)) ), Header::new( "uuid", @@ -667,7 +665,7 @@ mod message_tests { .add_header(Header::new("str", HeaderValue::String("some str".into()))) .add_header(Header::new( "time", - HeaderValue::Timestamp(Instant::from_epoch_seconds(5_000_000)), + HeaderValue::Timestamp(DateTime::from_secs(5_000_000)), )) .add_header(Header::new( "uuid", diff --git a/rust-runtime/aws-smithy-eventstream/src/smithy.rs b/rust-runtime/aws-smithy-eventstream/src/smithy.rs index 53a867bb8..2ccaed072 100644 --- a/rust-runtime/aws-smithy-eventstream/src/smithy.rs +++ b/rust-runtime/aws-smithy-eventstream/src/smithy.rs @@ -6,7 +6,7 @@ use crate::error::Error; use crate::frame::{Header, HeaderValue, Message}; use crate::str_bytes::StrBytes; -use aws_smithy_types::{Blob, Instant}; +use aws_smithy_types::{Blob, DateTime}; macro_rules! expect_shape_fn { (fn $fn_name:ident[$val_typ:ident] -> $result_typ:ident { $val_name:ident -> $val_expr:expr }) => { @@ -30,7 +30,7 @@ expect_shape_fn!(fn expect_int32[Int32] -> i32 { value -> *value }); expect_shape_fn!(fn expect_int64[Int64] -> i64 { value -> *value }); expect_shape_fn!(fn expect_byte_array[ByteArray] -> Blob { bytes -> Blob::new(bytes.as_ref()) }); expect_shape_fn!(fn expect_string[String] -> String { value -> value.as_str().into() }); -expect_shape_fn!(fn expect_timestamp[Timestamp] -> Instant { value -> *value }); +expect_shape_fn!(fn expect_timestamp[Timestamp] -> DateTime { value -> *value }); #[derive(Debug)] pub struct ResponseHeaders<'a> { diff --git a/rust-runtime/aws-smithy-http/src/header.rs b/rust-runtime/aws-smithy-http/src/header.rs index a59f2c5c3..bd3d42b2c 100644 --- a/rust-runtime/aws-smithy-http/src/header.rs +++ b/rust-runtime/aws-smithy-http/src/header.rs @@ -15,9 +15,9 @@ use std::str::FromStr; use http::header::{HeaderName, ValueIter}; use http::HeaderValue; -use aws_smithy_types::instant::Format; +use aws_smithy_types::date_time::Format; use aws_smithy_types::primitive::Parse; -use aws_smithy_types::Instant; +use aws_smithy_types::DateTime; #[derive(Debug, Eq, PartialEq)] #[non_exhaustive] @@ -52,19 +52,19 @@ impl Error for ParseError {} /// Read all the dates from the header map at `key` according the `format` /// -/// This is separate from `read_many` below because we need to invoke `Instant::read` to take advantage +/// This is separate from `read_many` below because we need to invoke `DateTime::read` to take advantage /// of comma-aware parsing pub fn many_dates( values: ValueIter, format: Format, -) -> Result, ParseError> { +) -> Result, ParseError> { let mut out = vec![]; for header in values { let mut header = header .to_str() .map_err(|_| ParseError::new_with_message("header was not valid utf-8 string"))?; while !header.is_empty() { - let (v, next) = Instant::read(header, format, ',').map_err(|err| { + let (v, next) = DateTime::read(header, format, ',').map_err(|err| { ParseError::new_with_message(format!("header could not be parsed as date: {}", err)) })?; out.push(v); diff --git a/rust-runtime/aws-smithy-http/src/label.rs b/rust-runtime/aws-smithy-http/src/label.rs index db15128ed..5b445f0a7 100644 --- a/rust-runtime/aws-smithy-http/src/label.rs +++ b/rust-runtime/aws-smithy-http/src/label.rs @@ -7,7 +7,8 @@ //! [httpLabel](https://awslabs.github.io/smithy/1.0/spec/core/http-traits.html#httplabel-trait) use crate::urlencode::BASE_SET; -use aws_smithy_types::Instant; +use aws_smithy_types::date_time::{DateTimeFormatError, Format}; +use aws_smithy_types::DateTime; use percent_encoding::AsciiSet; const GREEDY: &AsciiSet = &BASE_SET.remove(b'/'); @@ -17,8 +18,8 @@ pub fn fmt_string>(t: T, greedy: bool) -> String { percent_encoding::utf8_percent_encode(t.as_ref(), uri_set).to_string() } -pub fn fmt_timestamp(t: &Instant, format: aws_smithy_types::instant::Format) -> String { - crate::query::fmt_string(t.fmt(format)) +pub fn fmt_timestamp(t: &DateTime, format: Format) -> Result { + Ok(crate::query::fmt_string(t.fmt(format)?)) } #[cfg(test)] diff --git a/rust-runtime/aws-smithy-http/src/operation.rs b/rust-runtime/aws-smithy-http/src/operation.rs index 50a9c03b4..a30bb299e 100644 --- a/rust-runtime/aws-smithy-http/src/operation.rs +++ b/rust-runtime/aws-smithy-http/src/operation.rs @@ -5,6 +5,7 @@ use crate::body::SdkBody; use crate::property_bag::{PropertyBag, SharedPropertyBag}; +use aws_smithy_types::date_time::DateTimeFormatError; use http::uri::InvalidUri; use std::borrow::Cow; use std::error::Error; @@ -85,6 +86,12 @@ impl From for BuildError { } } +impl From for BuildError { + fn from(err: DateTimeFormatError) -> Self { + BuildError::from(SerializationError::from(err)) + } +} + impl Display for BuildError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -126,6 +133,8 @@ impl Error for BuildError { pub enum SerializationError { #[non_exhaustive] CannotSerializeUnknownVariant { union: &'static str }, + #[non_exhaustive] + DateTimeFormatError { cause: DateTimeFormatError }, } impl SerializationError { @@ -137,16 +146,26 @@ impl SerializationError { impl Display for SerializationError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - SerializationError::CannotSerializeUnknownVariant { union } => write!(f, "Cannot serialize `{}::Unknown`.\ - Unknown union variants cannot be serialized. This can occur when round-tripping a \ - response from the server that was not recognized by the SDK. Consider upgrading to the \ - latest version of the SDK.", union) + Self::CannotSerializeUnknownVariant { union } => write!( + f, + "Cannot serialize `{}::Unknown`. Unknown union variants cannot be serialized. \ + This can occur when round-tripping a response from the server that was not \ + recognized by the SDK. Consider upgrading to the latest version of the SDK.", + union + ), + Self::DateTimeFormatError { cause } => write!(f, "{}", cause), } } } impl Error for SerializationError {} +impl From for SerializationError { + fn from(err: DateTimeFormatError) -> SerializationError { + SerializationError::DateTimeFormatError { cause: err } + } +} + #[derive(Debug)] pub struct Operation { request: Request, diff --git a/rust-runtime/aws-smithy-http/src/query.rs b/rust-runtime/aws-smithy-http/src/query.rs index ea5baa282..4a2f16069 100644 --- a/rust-runtime/aws-smithy-http/src/query.rs +++ b/rust-runtime/aws-smithy-http/src/query.rs @@ -3,18 +3,22 @@ * SPDX-License-Identifier: Apache-2.0. */ +//! Utilities for writing Smithy values into a query string. +//! +//! Formatting values into the query string as specified in +//! [httpQuery](https://awslabs.github.io/smithy/1.0/spec/core/http-traits.html#httpquery-trait) + use crate::urlencode::BASE_SET; -/// Formatting values into the query string as specified in -/// [httpQuery](https://awslabs.github.io/smithy/1.0/spec/core/http-traits.html#httpquery-trait) -use aws_smithy_types::Instant; +use aws_smithy_types::date_time::{DateTimeFormatError, Format}; +use aws_smithy_types::DateTime; use percent_encoding::utf8_percent_encode; pub fn fmt_string>(t: T) -> String { utf8_percent_encode(t.as_ref(), BASE_SET).to_string() } -pub fn fmt_timestamp(t: &Instant, format: aws_smithy_types::instant::Format) -> String { - fmt_string(t.fmt(format)) +pub fn fmt_timestamp(t: &DateTime, format: Format) -> Result { + Ok(fmt_string(t.fmt(format)?)) } /// Simple abstraction to enable appending params to a string as query params diff --git a/rust-runtime/aws-smithy-json/src/deserialize/token.rs b/rust-runtime/aws-smithy-json/src/deserialize/token.rs index 18eb5e9e6..915dd0e88 100644 --- a/rust-runtime/aws-smithy-json/src/deserialize/token.rs +++ b/rust-runtime/aws-smithy-json/src/deserialize/token.rs @@ -5,8 +5,8 @@ use crate::deserialize::error::{Error, ErrorReason}; use crate::escape::unescape_string; -use aws_smithy_types::instant::Format; -use aws_smithy_types::{base64, Blob, Document, Instant, Number}; +use aws_smithy_types::date_time::Format; +use aws_smithy_types::{base64, Blob, DateTime, Document, Number}; use std::borrow::Cow; use crate::deserialize::must_not_be_finite; @@ -209,17 +209,17 @@ pub fn expect_blob_or_null(token: Option, Error>>) -> Result, Error>>, timestamp_format: Format, -) -> Result, Error> { +) -> Result, Error> { Ok(match timestamp_format { Format::EpochSeconds => { - expect_number_or_null(token)?.map(|v| Instant::from_f64(v.to_f64())) + expect_number_or_null(token)?.map(|v| DateTime::from_secs_f64(v.to_f64())) } Format::DateTime | Format::HttpDate => expect_string_or_null(token)? - .map(|v| Instant::from_str(v.as_escaped_str(), timestamp_format)) + .map(|v| DateTime::from_str(v.as_escaped_str(), timestamp_format)) .transpose() .map_err(|err| { Error::new( @@ -578,18 +578,18 @@ pub mod test { expect_timestamp_or_null(value_null(0), Format::HttpDate) ); assert_eq!( - Ok(Some(Instant::from_f64(2048.0))), + Ok(Some(DateTime::from_secs_f64(2048.0))), expect_timestamp_or_null(value_number(0, Number::Float(2048.0)), Format::EpochSeconds) ); assert_eq!( - Ok(Some(Instant::from_f64(1445412480.0))), + Ok(Some(DateTime::from_secs_f64(1445412480.0))), expect_timestamp_or_null( value_string(0, "Wed, 21 Oct 2015 07:28:00 GMT"), Format::HttpDate ) ); assert_eq!( - Ok(Some(Instant::from_f64(1445412480.0))), + Ok(Some(DateTime::from_secs_f64(1445412480.0))), expect_timestamp_or_null(value_string(0, "2015-10-21T07:28:00Z"), Format::DateTime) ); let err = Error::new( diff --git a/rust-runtime/aws-smithy-json/src/serialize.rs b/rust-runtime/aws-smithy-json/src/serialize.rs index 23c74aa87..affbccb25 100644 --- a/rust-runtime/aws-smithy-json/src/serialize.rs +++ b/rust-runtime/aws-smithy-json/src/serialize.rs @@ -4,9 +4,9 @@ */ use crate::escape::escape_string; -use aws_smithy_types::instant::Format; +use aws_smithy_types::date_time::{DateTimeFormatError, Format}; use aws_smithy_types::primitive::Encoder; -use aws_smithy_types::{Document, Instant, Number}; +use aws_smithy_types::{DateTime, Document, Number}; use std::borrow::Cow; pub struct JsonValueWriter<'a> { @@ -94,13 +94,18 @@ impl<'a> JsonValueWriter<'a> { } } - /// Writes an Instant `value` with the given `format`. - pub fn instant(self, instant: &Instant, format: Format) { - let formatted = instant.fmt(format); + /// Writes a date-time `value` with the given `format`. + pub fn date_time( + self, + date_time: &DateTime, + format: Format, + ) -> Result<(), DateTimeFormatError> { + let formatted = date_time.fmt(format)?; match format { Format::EpochSeconds => self.output.push_str(&formatted), _ => self.string(&formatted), } + Ok(()) } /// Starts an array. @@ -185,8 +190,8 @@ impl<'a> JsonArrayWriter<'a> { mod tests { use super::{JsonArrayWriter, JsonObjectWriter}; use crate::serialize::JsonValueWriter; - use aws_smithy_types::instant::Format; - use aws_smithy_types::{Document, Instant, Number}; + use aws_smithy_types::date_time::Format; + use aws_smithy_types::{DateTime, Document, Number}; use proptest::proptest; #[test] @@ -279,21 +284,28 @@ mod tests { } #[test] - fn object_instants() { + fn object_date_times() { let mut output = String::new(); let mut object = JsonObjectWriter::new(&mut output); object .key("epoch_seconds") - .instant(&Instant::from_f64(5.2), Format::EpochSeconds); - object.key("date_time").instant( - &Instant::from_str("2021-05-24T15:34:50.123Z", Format::DateTime).unwrap(), - Format::DateTime, - ); - object.key("http_date").instant( - &Instant::from_str("Wed, 21 Oct 2015 07:28:00 GMT", Format::HttpDate).unwrap(), - Format::HttpDate, - ); + .date_time(&DateTime::from_secs_f64(5.2), Format::EpochSeconds) + .unwrap(); + object + .key("date_time") + .date_time( + &DateTime::from_str("2021-05-24T15:34:50.123Z", Format::DateTime).unwrap(), + Format::DateTime, + ) + .unwrap(); + object + .key("http_date") + .date_time( + &DateTime::from_str("Wed, 21 Oct 2015 07:28:00 GMT", Format::HttpDate).unwrap(), + Format::HttpDate, + ) + .unwrap(); object.finish(); assert_eq!( @@ -303,21 +315,28 @@ mod tests { } #[test] - fn array_instants() { + fn array_date_times() { let mut output = String::new(); let mut array = JsonArrayWriter::new(&mut output); array .value() - .instant(&Instant::from_f64(5.2), Format::EpochSeconds); - array.value().instant( - &Instant::from_str("2021-05-24T15:34:50.123Z", Format::DateTime).unwrap(), - Format::DateTime, - ); - array.value().instant( - &Instant::from_str("Wed, 21 Oct 2015 07:28:00 GMT", Format::HttpDate).unwrap(), - Format::HttpDate, - ); + .date_time(&DateTime::from_secs_f64(5.2), Format::EpochSeconds) + .unwrap(); + array + .value() + .date_time( + &DateTime::from_str("2021-05-24T15:34:50.123Z", Format::DateTime).unwrap(), + Format::DateTime, + ) + .unwrap(); + array + .value() + .date_time( + &DateTime::from_str("Wed, 21 Oct 2015 07:28:00 GMT", Format::HttpDate).unwrap(), + Format::HttpDate, + ) + .unwrap(); array.finish(); assert_eq!( diff --git a/rust-runtime/aws-smithy-query/src/lib.rs b/rust-runtime/aws-smithy-query/src/lib.rs index 4ea901b4e..0ba567b9a 100644 --- a/rust-runtime/aws-smithy-query/src/lib.rs +++ b/rust-runtime/aws-smithy-query/src/lib.rs @@ -5,9 +5,9 @@ //! Abstractions for the Smithy AWS Query protocol -use aws_smithy_types::instant::Format; +use aws_smithy_types::date_time::{DateTimeFormatError, Format}; use aws_smithy_types::primitive::Encoder; -use aws_smithy_types::{Instant, Number}; +use aws_smithy_types::{DateTime, Number}; use std::borrow::Cow; use urlencoding::encode; @@ -178,9 +178,14 @@ impl<'a> QueryValueWriter<'a> { } } - /// Writes an Instant `value` with the given `format`. - pub fn instant(self, instant: &Instant, format: Format) { - self.string(&instant.fmt(format)); + /// Writes a date-time `value` with the given `format`. + pub fn date_time( + self, + date_time: &DateTime, + format: Format, + ) -> Result<(), DateTimeFormatError> { + self.string(&date_time.fmt(format)?); + Ok(()) } /// Starts a map. @@ -208,8 +213,8 @@ impl<'a> QueryValueWriter<'a> { #[cfg(test)] mod tests { use crate::QueryWriter; - use aws_smithy_types::instant::Format; - use aws_smithy_types::{Instant, Number}; + use aws_smithy_types::date_time::Format; + use aws_smithy_types::{DateTime, Number}; #[test] fn no_params() { @@ -327,15 +332,22 @@ mod tests { writer .prefix("epoch_seconds") - .instant(&Instant::from_f64(5.2), Format::EpochSeconds); - writer.prefix("date_time").instant( - &Instant::from_str("2021-05-24T15:34:50.123Z", Format::DateTime).unwrap(), - Format::DateTime, - ); - writer.prefix("http_date").instant( - &Instant::from_str("Wed, 21 Oct 2015 07:28:00 GMT", Format::HttpDate).unwrap(), - Format::HttpDate, - ); + .date_time(&DateTime::from_secs_f64(5.2), Format::EpochSeconds) + .unwrap(); + writer + .prefix("date_time") + .date_time( + &DateTime::from_str("2021-05-24T15:34:50.123Z", Format::DateTime).unwrap(), + Format::DateTime, + ) + .unwrap(); + writer + .prefix("http_date") + .date_time( + &DateTime::from_str("Wed, 21 Oct 2015 07:28:00 GMT", Format::HttpDate).unwrap(), + Format::HttpDate, + ) + .unwrap(); writer.finish(); assert_eq!( diff --git a/rust-runtime/aws-smithy-types-convert/Cargo.toml b/rust-runtime/aws-smithy-types-convert/Cargo.toml new file mode 100644 index 000000000..ff07823c6 --- /dev/null +++ b/rust-runtime/aws-smithy-types-convert/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "aws-smithy-types-convert" +version = "0.0.0-smithy-rs-head" +authors = ["AWS Rust SDK Team "] +description = "Conversion of types from aws-smithy-types to other libraries." +edition = "2018" +license = "Apache-2.0" +repository = "https://github.com/awslabs/smithy-rs" + +[features] +convert-chrono = ["chrono"] +convert-time = ["time"] +default = [] + +[dependencies] +aws-smithy-types = { path = "../aws-smithy-types" } +chrono = { version = "0.4.19", optional = true } +time = { version = "0.3.4", optional = true } diff --git a/rust-runtime/aws-smithy-types-convert/LICENSE b/rust-runtime/aws-smithy-types-convert/LICENSE new file mode 100644 index 000000000..67db85882 --- /dev/null +++ b/rust-runtime/aws-smithy-types-convert/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/rust-runtime/aws-smithy-types-convert/src/date_time.rs b/rust-runtime/aws-smithy-types-convert/src/date_time.rs new file mode 100644 index 000000000..165718888 --- /dev/null +++ b/rust-runtime/aws-smithy-types-convert/src/date_time.rs @@ -0,0 +1,237 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Conversions from [`DateTime`] to the types in the +//! [`time`](https://crates.io/crates/time) or +//! [`chrono`](https://crates.io/crates/chrono) +//! crates. + +use aws_smithy_types::DateTime; +use std::error::Error as StdError; +use std::fmt; + +/// Conversion error +#[non_exhaustive] +#[derive(Debug)] +pub enum Error { + /// Conversion failed because the value being converted is out of range for its destination + #[non_exhaustive] + OutOfRange(Box), +} + +impl StdError for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::OutOfRange(cause) => { + write!( + f, + "conversion failed because the value is out of range for its destination: {}", + cause + ) + } + } + } +} + +/// Adds functions to [`DateTime`] to convert it to `time` or `chrono` types. +/// +#[cfg_attr( + feature = "convert-time", + doc = r##" +# Example with `time` + +Make sure your **Cargo.toml** enables the `convert-time` feature: +```toml +[dependencies] +aws-smithy-types-convert = { version = "VERSION", features = ["convert-time"] } +``` + +Then import [`DateTimeExt`] to use the conversions: +```rust +# fn test_fn() -> Result<(), aws_smithy_types_convert::date_time::Error> { +# use aws_smithy_types::DateTime; +use aws_smithy_types_convert::date_time::DateTimeExt; +use time::OffsetDateTime; + +let offset_date_time: OffsetDateTime = DateTime::from_secs(5).to_time()?; +let date_time: DateTime = DateTime::from_time(offset_date_time); +# Ok(()) +# } +``` +"## +)] +#[cfg_attr( + feature = "convert-chrono", + doc = r##" +# Example with `chrono` + +Make sure your **Cargo.toml** enables the `convert-chrono` feature: +```toml +[dependencies] +aws-smithy-types-convert = { version = "VERSION", features = ["convert-chrono"] } +``` + +Then import [`DateTimeExt`] to use the conversions: +```rust +# use aws_smithy_types::DateTime; +use aws_smithy_types_convert::date_time::DateTimeExt; +use chrono::{Utc}; + +let chrono_date_time: chrono::DateTime = DateTime::from_secs(5).to_chrono_utc(); +let date_time: DateTime = DateTime::from_chrono_utc(chrono_date_time); +``` +"## +)] +pub trait DateTimeExt { + /// Converts a [`DateTime`] to a [`chrono::DateTime`] with timezone UTC. + #[cfg(feature = "convert-chrono")] + fn to_chrono_utc(&self) -> chrono::DateTime; + + /// Converts a [`chrono::DateTime`] with timezone UTC to a [`DateTime`]. + #[cfg(feature = "convert-chrono")] + fn from_chrono_utc(time: chrono::DateTime) -> DateTime; + + /// Converts a [`chrono::DateTime`] with an offset timezone to a [`DateTime`]. + #[cfg(feature = "convert-chrono")] + fn from_chrono_fixed(time: chrono::DateTime) -> DateTime; + + /// Converts a [`DateTime`] to a [`time::OffsetDateTime`]. + /// + /// Returns an [`Error::OutOfRange`] if the time is after + /// `9999-12-31T23:59:59.999Z` or before `-9999-01-01T00:00:00.000Z`. + #[cfg(feature = "convert-time")] + fn to_time(&self) -> Result; + + /// Converts a [`time::OffsetDateTime`] to a [`DateTime`]. + #[cfg(feature = "convert-time")] + fn from_time(time: time::OffsetDateTime) -> DateTime; +} + +impl DateTimeExt for DateTime { + #[cfg(feature = "convert-chrono")] + fn to_chrono_utc(&self) -> chrono::DateTime { + chrono::DateTime::::from_utc( + chrono::NaiveDateTime::from_timestamp(self.secs(), self.subsec_nanos()), + chrono::Utc, + ) + } + + #[cfg(feature = "convert-chrono")] + fn from_chrono_utc(value: chrono::DateTime) -> DateTime { + DateTime::from_secs_and_nanos(value.timestamp(), value.timestamp_subsec_nanos()) + } + + #[cfg(feature = "convert-chrono")] + fn from_chrono_fixed(value: chrono::DateTime) -> DateTime { + Self::from_chrono_utc(value.with_timezone(&chrono::Utc)) + } + + #[cfg(feature = "convert-time")] + fn to_time(&self) -> Result { + time::OffsetDateTime::from_unix_timestamp_nanos(self.as_nanos()) + .map_err(|err| Error::OutOfRange(err.into())) + } + + #[cfg(feature = "convert-time")] + fn from_time(time: time::OffsetDateTime) -> DateTime { + DateTime::from_nanos(time.unix_timestamp_nanos()) + .expect("DateTime supports a greater range than OffsetDateTime") + } +} + +#[cfg(all(test, any(feature = "convert-chrono", feature = "convert-time")))] +mod test { + use super::DateTimeExt; + use aws_smithy_types::date_time::{DateTime, Format}; + + #[cfg(feature = "convert-time")] + use super::Error; + + #[test] + #[cfg(feature = "convert-chrono")] + fn from_chrono() { + use chrono::{FixedOffset, TimeZone, Utc}; + + let chrono = Utc.ymd(2039, 7, 8).and_hms_nano(9, 3, 11, 123_000_000); + let expected = DateTime::from_str("2039-07-08T09:03:11.123Z", Format::DateTime).unwrap(); + assert_eq!(expected, DateTime::from_chrono_utc(chrono)); + + let chrono = Utc.ymd(1000, 7, 8).and_hms_nano(9, 3, 11, 456_000_000); + let expected = DateTime::from_str("1000-07-08T09:03:11.456Z", Format::DateTime).unwrap(); + assert_eq!(expected, DateTime::from_chrono_utc(chrono)); + + let chrono = + FixedOffset::west(2 * 3600) + .ymd(2039, 7, 8) + .and_hms_nano(9, 3, 11, 123_000_000); + let expected = DateTime::from_str("2039-07-08T11:03:11.123Z", Format::DateTime).unwrap(); + assert_eq!(expected, DateTime::from_chrono_fixed(chrono)); + } + + #[test] + #[cfg(feature = "convert-chrono")] + fn to_chrono() { + use chrono::{TimeZone, Utc}; + + let date_time = DateTime::from_str("2039-07-08T09:03:11.123Z", Format::DateTime).unwrap(); + let expected = Utc.ymd(2039, 7, 8).and_hms_nano(9, 3, 11, 123_000_000); + assert_eq!(expected, date_time.to_chrono_utc()); + + let date_time = DateTime::from_str("1000-07-08T09:03:11.456Z", Format::DateTime).unwrap(); + let expected = Utc.ymd(1000, 7, 8).and_hms_nano(9, 3, 11, 456_000_000); + assert_eq!(expected, date_time.to_chrono_utc()); + } + + #[test] + #[cfg(feature = "convert-time")] + fn from_time() { + use time::{Date, Month, PrimitiveDateTime, Time}; + + let time = PrimitiveDateTime::new( + Date::from_calendar_date(2039, Month::July, 8).unwrap(), + Time::from_hms_milli(9, 3, 11, 123).unwrap(), + ) + .assume_utc(); + let expected = DateTime::from_str("2039-07-08T09:03:11.123Z", Format::DateTime).unwrap(); + assert_eq!(expected, DateTime::from_time(time)); + + let time = PrimitiveDateTime::new( + Date::from_calendar_date(1000, Month::July, 8).unwrap(), + Time::from_hms_milli(9, 3, 11, 456).unwrap(), + ) + .assume_utc(); + let expected = DateTime::from_str("1000-07-08T09:03:11.456Z", Format::DateTime).unwrap(); + assert_eq!(expected, DateTime::from_time(time)); + } + + #[test] + #[cfg(feature = "convert-time")] + fn to_time() { + use time::{Date, Month, PrimitiveDateTime, Time}; + + let date_time = DateTime::from_str("2039-07-08T09:03:11.123Z", Format::DateTime).unwrap(); + let expected = PrimitiveDateTime::new( + Date::from_calendar_date(2039, Month::July, 8).unwrap(), + Time::from_hms_milli(9, 3, 11, 123).unwrap(), + ) + .assume_utc(); + assert_eq!(expected, date_time.to_time().unwrap()); + + let date_time = DateTime::from_str("1000-07-08T09:03:11.456Z", Format::DateTime).unwrap(); + let expected = PrimitiveDateTime::new( + Date::from_calendar_date(1000, Month::July, 8).unwrap(), + Time::from_hms_milli(9, 3, 11, 456).unwrap(), + ) + .assume_utc(); + assert_eq!(expected, date_time.to_time().unwrap()); + + let date_time = DateTime::from_secs_and_nanos(i64::MAX, 0); + assert!(matches!(date_time.to_time(), Err(Error::OutOfRange(_)))); + let date_time = DateTime::from_secs_and_nanos(i64::MIN, 0); + assert!(matches!(date_time.to_time(), Err(Error::OutOfRange(_)))); + } +} diff --git a/rust-runtime/aws-smithy-types-convert/src/lib.rs b/rust-runtime/aws-smithy-types-convert/src/lib.rs new file mode 100644 index 000000000..fffcfa252 --- /dev/null +++ b/rust-runtime/aws-smithy-types-convert/src/lib.rs @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Conversions between `aws-smithy-types` and the types of frequently used Rust libraries. + +#![warn( + missing_docs, + missing_crate_level_docs, + missing_debug_implementations, + rust_2018_idioms, + unreachable_pub +)] + +#[cfg(any(feature = "convert-time", feature = "convert-chrono"))] +pub mod date_time; diff --git a/rust-runtime/aws-smithy-types/Cargo.toml b/rust-runtime/aws-smithy-types/Cargo.toml index b7fa74ff8..2144c6975 100644 --- a/rust-runtime/aws-smithy-types/Cargo.toml +++ b/rust-runtime/aws-smithy-types/Cargo.toml @@ -7,19 +7,14 @@ edition = "2018" license = "Apache-2.0" repository = "https://github.com/awslabs/smithy-rs" -[features] -chrono-conversions = [] -default = ["chrono-conversions"] - [dependencies] -chrono = { version = "0.4", default-features = false, features = [] } itoa = "0.4.0" num-integer = "0.1" ryu = "1.0.5" +time = { version = "0.3.4", features = ["parsing"] } [dev-dependencies] base64 = "0.13.0" -chrono = { version = "0.4", default-features = false, features = ["alloc"] } lazy_static = "1.4" proptest = "1" serde = { version = "1", features = ["derive"] } diff --git a/rust-runtime/aws-smithy-types/fuzz/Cargo.toml b/rust-runtime/aws-smithy-types/fuzz/Cargo.toml index 3077f819b..1d37a074b 100644 --- a/rust-runtime/aws-smithy-types/fuzz/Cargo.toml +++ b/rust-runtime/aws-smithy-types/fuzz/Cargo.toml @@ -35,3 +35,15 @@ name = "parse_date_time" path = "fuzz_targets/parse_date_time.rs" test = false doc = false + +[[bin]] +name = "read_date_time" +path = "fuzz_targets/read_date_time.rs" +test = false +doc = false + +[[bin]] +name = "read_http_date" +path = "fuzz_targets/read_http_date.rs" +test = false +doc = false diff --git a/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/parse_date_time.rs b/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/parse_date_time.rs index 67014d4e3..9b5876f7e 100644 --- a/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/parse_date_time.rs +++ b/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/parse_date_time.rs @@ -5,12 +5,12 @@ #![no_main] -use aws_smithy_types::instant::{Format, Instant}; +use aws_smithy_types::date_time::{DateTime, Format}; use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { if let Ok(value) = std::str::from_utf8(data) { // Looking for panics. Don't care if the parsing fails. - let _ = Instant::from_str(value, Format::DateTime); + let _ = DateTime::from_str(value, Format::DateTime); } }); diff --git a/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/parse_epoch_seconds.rs b/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/parse_epoch_seconds.rs index e24c04f95..276281796 100644 --- a/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/parse_epoch_seconds.rs +++ b/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/parse_epoch_seconds.rs @@ -5,12 +5,12 @@ #![no_main] -use aws_smithy_types::instant::{Format, Instant}; +use aws_smithy_types::date_time::{DateTime, Format}; use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { if let Ok(value) = std::str::from_utf8(data) { // Looking for panics. Don't care if the parsing fails. - let _ = Instant::from_str(value, Format::EpochSeconds); + let _ = DateTime::from_str(value, Format::EpochSeconds); } }); diff --git a/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/parse_http_date.rs b/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/parse_http_date.rs index 661e37c55..ec1db862d 100644 --- a/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/parse_http_date.rs +++ b/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/parse_http_date.rs @@ -5,12 +5,12 @@ #![no_main] -use aws_smithy_types::instant::{Format, Instant}; +use aws_smithy_types::date_time::{DateTime, Format}; use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { if let Ok(value) = std::str::from_utf8(data) { // Looking for panics. Don't care if the parsing fails. - let _ = Instant::from_str(value, Format::HttpDate); + let _ = DateTime::from_str(value, Format::HttpDate); } }); diff --git a/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/read_date_time.rs b/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/read_date_time.rs new file mode 100644 index 000000000..70cbc69b2 --- /dev/null +++ b/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/read_date_time.rs @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#![no_main] + +use aws_smithy_types::date_time::{DateTime, Format}; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + if let Ok(mut value) = std::str::from_utf8(data) { + // Looking for panics. Don't care if the parsing fails. + while let Ok((_, next)) = DateTime::read(value, Format::DateTime, ',') { + value = next; + } + } +}); diff --git a/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/read_http_date.rs b/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/read_http_date.rs new file mode 100644 index 000000000..6879d2767 --- /dev/null +++ b/rust-runtime/aws-smithy-types/fuzz/fuzz_targets/read_http_date.rs @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#![no_main] + +use aws_smithy_types::date_time::{DateTime, Format}; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + if let Ok(mut value) = std::str::from_utf8(data) { + // Looking for panics. Don't care if the parsing fails. + while let Ok((_, next)) = DateTime::read(value, Format::HttpDate, ',') { + value = next; + } + } +}); diff --git a/rust-runtime/aws-smithy-types/proptest-regressions/instant/format.txt b/rust-runtime/aws-smithy-types/proptest-regressions/instant/format.txt new file mode 100644 index 000000000..409c1ce38 --- /dev/null +++ b/rust-runtime/aws-smithy-types/proptest-regressions/instant/format.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 274da3290b70eec94751bb4ebb152160811daea25f46211ebf54bba47bd3a2e6 # shrinks to secs = -1, nanos = 2 diff --git a/rust-runtime/aws-smithy-types/src/instant/format.rs b/rust-runtime/aws-smithy-types/src/date_time/format.rs similarity index 54% rename from rust-runtime/aws-smithy-types/src/instant/format.rs rename to rust-runtime/aws-smithy-types/src/date_time/format.rs index 1fc9a24b5..df2a3239f 100644 --- a/rust-runtime/aws-smithy-types/src/instant/format.rs +++ b/rust-runtime/aws-smithy-types/src/date_time/format.rs @@ -3,30 +3,59 @@ * SPDX-License-Identifier: Apache-2.0. */ +use std::borrow::Cow; use std::error::Error; use std::fmt; const NANOS_PER_SECOND: u32 = 1_000_000_000; +/// Error returned when date-time parsing fails. #[non_exhaustive] -#[derive(Debug, Eq, PartialEq)] -pub enum DateParseError { - Invalid(&'static str), +#[derive(Debug)] +pub enum DateTimeParseError { + /// The given date-time string was invalid. + #[non_exhaustive] + Invalid(Cow<'static, str>), + /// Failed to parse an integer inside the given date-time string. + #[non_exhaustive] IntParseError, } -impl Error for DateParseError {} +impl Error for DateTimeParseError {} -impl fmt::Display for DateParseError { +impl fmt::Display for DateTimeParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use DateParseError::*; + use DateTimeParseError::*; match self { - Invalid(msg) => write!(f, "invalid date: {}", msg), + Invalid(msg) => write!(f, "invalid date-time: {}", msg), IntParseError => write!(f, "failed to parse int"), } } } +/// Error returned when date-time formatting fails. +#[non_exhaustive] +#[derive(Debug)] +pub enum DateTimeFormatError { + /// The given date-time cannot be represented in the requested date format. + #[non_exhaustive] + OutOfRange(Cow<'static, str>), +} + +impl Error for DateTimeFormatError {} + +impl fmt::Display for DateTimeFormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::OutOfRange(msg) => write!( + f, + "date-time cannot be formatted since it is out of range: {}", + msg + ), + } + } +} + fn remove_trailing_zeros(string: &mut String) { while let Some(b'0') = string.as_bytes().last() { string.pop(); @@ -35,101 +64,117 @@ fn remove_trailing_zeros(string: &mut String) { pub(crate) mod epoch_seconds { use super::remove_trailing_zeros; - use super::DateParseError; - use crate::Instant; + use super::DateTimeParseError; + use crate::DateTime; use std::str::FromStr; - /// Formats an `Instant` into the Smithy epoch seconds date-time format. - pub(crate) fn format(instant: &Instant) -> String { - if instant.subsecond_nanos == 0 { - format!("{}", instant.seconds) + /// Formats a `DateTime` into the Smithy epoch seconds date-time format. + pub(crate) fn format(date_time: &DateTime) -> String { + if date_time.subsecond_nanos == 0 { + format!("{}", date_time.seconds) } else { - let mut result = format!("{}.{:0>9}", instant.seconds, instant.subsecond_nanos); + let mut result = format!("{}.{:0>9}", date_time.seconds, date_time.subsecond_nanos); remove_trailing_zeros(&mut result); result } } - /// Parses the Smithy epoch seconds date-time format into an `Instant`. - pub(crate) fn parse(value: &str) -> Result { + /// Parses the Smithy epoch seconds date-time format into a `DateTime`. + pub(crate) fn parse(value: &str) -> Result { let mut parts = value.splitn(2, '.'); let (mut whole, mut decimal) = (0i64, 0u32); if let Some(whole_str) = parts.next() { - whole = ::from_str(whole_str).map_err(|_| DateParseError::IntParseError)?; + whole = ::from_str(whole_str).map_err(|_| DateTimeParseError::IntParseError)?; } if let Some(decimal_str) = parts.next() { if decimal_str.starts_with('+') || decimal_str.starts_with('-') { - return Err(DateParseError::Invalid("invalid epoch-seconds timestamp")); + return Err(DateTimeParseError::Invalid( + "invalid epoch-seconds timestamp".into(), + )); } if decimal_str.len() > 9 { - return Err(DateParseError::Invalid("decimal is longer than 9 digits")); + return Err(DateTimeParseError::Invalid( + "decimal is longer than 9 digits".into(), + )); } let missing_places = 9 - decimal_str.len() as isize; - decimal = ::from_str(decimal_str).map_err(|_| DateParseError::IntParseError)?; + decimal = + ::from_str(decimal_str).map_err(|_| DateTimeParseError::IntParseError)?; for _ in 0..missing_places { decimal *= 10; } } - Ok(Instant::from_secs_and_nanos(whole, decimal)) + Ok(DateTime::from_secs_and_nanos(whole, decimal)) } } pub(crate) mod http_date { use super::remove_trailing_zeros; - use crate::instant::format::{DateParseError, NANOS_PER_SECOND}; - use crate::Instant; - use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Weekday}; + use crate::date_time::format::{DateTimeFormatError, DateTimeParseError, NANOS_PER_SECOND}; + use crate::DateTime; use std::str::FromStr; + use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday}; // This code is taken from https://github.com/pyfisch/httpdate and modified under an // Apache 2.0 License. Modifications: // - Removed use of unsafe // - Add serialization and deserialization of subsecond nanos // - /// Format an `instant` in the HTTP date format (imf-fixdate) with added support for subsecond precision + /// Format a `DateTime` in the HTTP date format (imf-fixdate) with added support for subsecond precision /// /// Example: "Mon, 16 Dec 2019 23:48:18 GMT" /// /// Some notes: - /// - HTTP date does not support years before `0000`—this will cause a panic. + /// - HTTP date does not support years before `0001`—this will cause a panic. /// - If you _don't_ want subsecond precision (e.g. if you want strict adherence to the spec), - /// you need to zero-out the instant before formatting + /// you need to zero-out the date-time before formatting /// - If subsecond nanos are 0, no fractional seconds are added /// - If subsecond nanos are nonzero, 3 digits of fractional seconds are added - pub(crate) fn format(instant: &Instant) -> String { - let structured = instant.to_chrono_internal(); + pub(crate) fn format(date_time: &DateTime) -> Result { + fn out_of_range(cause: E) -> DateTimeFormatError { + DateTimeFormatError::OutOfRange( + format!( + "HTTP dates support dates between Mon, 01 Jan 0001 00:00:00 GMT \ + and Fri, 31 Dec 9999 23:59:59.999 GMT. {}", + cause + ) + .into(), + ) + } + let structured = OffsetDateTime::from_unix_timestamp_nanos(date_time.as_nanos()) + .map_err(out_of_range)?; let weekday = match structured.weekday() { - Weekday::Mon => "Mon", - Weekday::Tue => "Tue", - Weekday::Wed => "Wed", - Weekday::Thu => "Thu", - Weekday::Fri => "Fri", - Weekday::Sat => "Sat", - Weekday::Sun => "Sun", + Weekday::Monday => "Mon", + Weekday::Tuesday => "Tue", + Weekday::Wednesday => "Wed", + Weekday::Thursday => "Thu", + Weekday::Friday => "Fri", + Weekday::Saturday => "Sat", + Weekday::Sunday => "Sun", }; let month = match structured.month() { - 1 => "Jan", - 2 => "Feb", - 3 => "Mar", - 4 => "Apr", - 5 => "May", - 6 => "Jun", - 7 => "Jul", - 8 => "Aug", - 9 => "Sep", - 10 => "Oct", - 11 => "Nov", - 12 => "Dec", - _ => unreachable!(), + Month::January => "Jan", + Month::February => "Feb", + Month::March => "Mar", + Month::April => "Apr", + Month::May => "May", + Month::June => "Jun", + Month::July => "Jul", + Month::August => "Aug", + Month::September => "Sep", + Month::October => "Oct", + Month::November => "Nov", + Month::December => "Dec", }; let mut out = String::with_capacity(32); fn push_digit(out: &mut String, digit: u8) { + debug_assert!(digit < 10); out.push((b'0' + digit as u8) as char); } out.push_str(weekday); out.push_str(", "); - let day = structured.date().day() as u8; + let day = structured.day(); push_digit(&mut out, day / 10); push_digit(&mut out, day % 10); @@ -139,10 +184,9 @@ pub(crate) mod http_date { out.push(' '); let year = structured.year(); - // Although chrono can handle extremely early years, HTTP date does not support - // years before 0000 - let year = if year < 0 { - panic!("negative years not supported") + // HTTP date does not support years before 0001 + let year = if year < 1 { + return Err(out_of_range("HTTP dates cannot be before the year 0001")); } else { year as u32 }; @@ -155,7 +199,7 @@ pub(crate) mod http_date { out.push(' '); - let hour = structured.time().hour() as u8; + let hour = structured.hour(); // Extract the individual digits from hour push_digit(&mut out, hour / 10); @@ -164,32 +208,31 @@ pub(crate) mod http_date { out.push(':'); // Extract the individual digits from minute - let minute = structured.minute() as u8; + let minute = structured.minute(); push_digit(&mut out, minute / 10); push_digit(&mut out, minute % 10); out.push(':'); - let second = structured.second() as u8; + let second = structured.second(); push_digit(&mut out, second / 10); push_digit(&mut out, second % 10); // If non-zero nanos, push a 3-digit fractional second - let nanos = structured.timestamp_subsec_nanos(); - if nanos / (NANOS_PER_SECOND / 1000) != 0 { + let millis = structured.millisecond(); + if millis != 0 { out.push('.'); - push_digit(&mut out, (nanos / (NANOS_PER_SECOND / 10)) as u8); - push_digit(&mut out, (nanos / (NANOS_PER_SECOND / 100) % 10) as u8); - push_digit(&mut out, (nanos / (NANOS_PER_SECOND / 1000) % 10) as u8); + push_digit(&mut out, (millis / 100 % 10) as u8); + push_digit(&mut out, (millis / 10 % 10) as u8); + push_digit(&mut out, (millis % 10) as u8); remove_trailing_zeros(&mut out); } out.push_str(" GMT"); - - out + Ok(out) } - /// Parse an IMF-fixdate formatted date into an Instant + /// Parse an IMF-fixdate formatted date into a DateTime /// /// This function has a few caveats: /// 1. It DOES NOT support the "deprecated" formats supported by HTTP date @@ -199,23 +242,27 @@ pub(crate) mod http_date { /// Ok: "Mon, 16 Dec 2019 23:48:18.123 GMT" /// Ok: "Mon, 16 Dec 2019 23:48:18.12 GMT" /// Not Ok: "Mon, 16 Dec 2019 23:48:18.1234 GMT" - pub(crate) fn parse(s: &str) -> Result { + pub(crate) fn parse(s: &str) -> Result { if !s.is_ascii() { - return Err(DateParseError::Invalid("not ascii")); + return Err(DateTimeParseError::Invalid( + "date-time must be ASCII".into(), + )); } let x = s.trim().as_bytes(); parse_imf_fixdate(x) } - pub(crate) fn read(s: &str) -> Result<(Instant, &str), DateParseError> { + pub(crate) fn read(s: &str) -> Result<(DateTime, &str), DateTimeParseError> { if !s.is_ascii() { - return Err(DateParseError::Invalid("Date must be valid ascii")); + return Err(DateTimeParseError::Invalid( + "date-time must be ASCII".into(), + )); } let (first_date, rest) = match find_subsequence(s.as_bytes(), b" GMT") { // split_at is correct because we asserted that this date is only valid ASCII so the byte index is // the same as the char index Some(idx) => s.split_at(idx), - None => return Err(DateParseError::Invalid("Date did not end in GMT")), + None => return Err(DateTimeParseError::Invalid("date-time is not GMT".into())), }; Ok((parse(first_date)?, rest)) } @@ -227,7 +274,7 @@ pub(crate) mod http_date { .map(|idx| idx + needle.len()) } - fn parse_imf_fixdate(s: &[u8]) -> Result { + fn parse_imf_fixdate(s: &[u8]) -> Result { // Example: `Sun, 06 Nov 1994 08:49:37 GMT` if s.len() < 29 || s.len() > 33 @@ -236,7 +283,9 @@ pub(crate) mod http_date { || s[19] != b':' || s[22] != b':' { - return Err(DateParseError::Invalid("incorrectly shaped string")); + return Err(DateTimeParseError::Invalid( + "incorrectly shaped string".into(), + )); } let nanos: u32 = match &s[25] { b'.' => { @@ -245,7 +294,9 @@ pub(crate) mod http_date { let fraction_slice = &s[26..s.len() - 4]; if fraction_slice.len() > 3 { // Only thousandths are supported - return Err(DateParseError::Invalid("too much precision")); + return Err(DateTimeParseError::Invalid( + "Smithy http-date only supports millisecond precision".into(), + )); } let fraction: u32 = parse_slice(fraction_slice)?; // We need to convert the fractional second to nanoseconds, so we need to scale @@ -254,41 +305,55 @@ pub(crate) mod http_date { fraction * (NANOS_PER_SECOND / multiplier[fraction_slice.len() - 1]) } b' ' => 0, - _ => return Err(DateParseError::Invalid("incorrectly shaped string")), + _ => { + return Err(DateTimeParseError::Invalid( + "incorrectly shaped string".into(), + )) + } }; let hours = parse_slice(&s[17..19])?; - let minutes = parse_slice(&s[20..22])?; let seconds = parse_slice(&s[23..25])?; - let time = NaiveTime::from_hms_nano(hours, minutes, seconds, nanos); + let time = Time::from_hms_nano(hours, minutes, seconds, nanos).map_err(|err| { + DateTimeParseError::Invalid(format!("time components are out of range: {}", err).into()) + })?; + let month = match &s[7..12] { - b" Jan " => 1, - b" Feb " => 2, - b" Mar " => 3, - b" Apr " => 4, - b" May " => 5, - b" Jun " => 6, - b" Jul " => 7, - b" Aug " => 8, - b" Sep " => 9, - b" Oct " => 10, - b" Nov " => 11, - b" Dec " => 12, - _ => return Err(DateParseError::Invalid("invalid month")), + b" Jan " => Month::January, + b" Feb " => Month::February, + b" Mar " => Month::March, + b" Apr " => Month::April, + b" May " => Month::May, + b" Jun " => Month::June, + b" Jul " => Month::July, + b" Aug " => Month::August, + b" Sep " => Month::September, + b" Oct " => Month::October, + b" Nov " => Month::November, + b" Dec " => Month::December, + month => { + return Err(DateTimeParseError::Invalid( + format!( + "invalid month: {}", + std::str::from_utf8(month).unwrap_or_default() + ) + .into(), + )) + } }; let year = parse_slice(&s[12..16])?; let day = parse_slice(&s[5..7])?; - let date = NaiveDate::from_ymd(year, month, day); - let datetime = NaiveDateTime::new(date, time); + let date = Date::from_calendar_date(year, month, day).map_err(|err| { + DateTimeParseError::Invalid(format!("date components are out of range: {}", err).into()) + })?; + let date_time = PrimitiveDateTime::new(date, time).assume_offset(UtcOffset::UTC); - Ok(Instant::from_secs_and_nanos( - datetime.timestamp(), - datetime.timestamp_subsec_nanos(), - )) + Ok(DateTime::from_nanos(date_time.unix_timestamp_nanos()) + .expect("this date format cannot produce out of range date-times")) } - fn parse_slice(ascii_slice: &[u8]) -> Result + fn parse_slice(ascii_slice: &[u8]) -> Result where T: FromStr, { @@ -296,66 +361,68 @@ pub(crate) mod http_date { std::str::from_utf8(ascii_slice).expect("should only be called on ascii strings"); as_str .parse::() - .map_err(|_| DateParseError::IntParseError) + .map_err(|_| DateTimeParseError::IntParseError) } } pub(crate) mod rfc3339 { - use chrono::format; - - use crate::instant::format::DateParseError; - use crate::Instant; - use chrono::{Datelike, Timelike}; + use crate::date_time::format::{DateTimeFormatError, DateTimeParseError}; + use crate::DateTime; + use time::format_description::well_known::Rfc3339; + use time::OffsetDateTime; // OK: 1985-04-12T23:20:50.52Z // OK: 1985-04-12T23:20:50Z // // Timezones not supported: // Not OK: 1985-04-12T23:20:50-02:00 - pub(crate) fn parse(s: &str) -> Result { - let mut date = format::Parsed::new(); - let format = format::StrftimeItems::new("%Y-%m-%dT%H:%M:%S%.fZ"); - // TODO: it may be helpful for debugging to keep these errors around - chrono::format::parse(&mut date, s, format) - .map_err(|_| DateParseError::Invalid("invalid rfc3339 date"))?; - let utc_date = date - .to_naive_datetime_with_offset(0) - .map_err(|_| DateParseError::Invalid("invalid date"))?; - Ok(Instant::from_secs_and_nanos( - utc_date.timestamp(), - utc_date.timestamp_subsec_nanos(), - )) + pub(crate) fn parse(s: &str) -> Result { + let date_time = OffsetDateTime::parse(s, &Rfc3339).map_err(|err| { + DateTimeParseError::Invalid(format!("invalid RFC-3339 date-time: {}", err).into()) + })?; + Ok(DateTime::from_nanos(date_time.unix_timestamp_nanos()) + .expect("this date format cannot produce out of range date-times")) } /// Read 1 RFC-3339 date from &str and return the remaining str - pub(crate) fn read(s: &str) -> Result<(Instant, &str), DateParseError> { + pub(crate) fn read(s: &str) -> Result<(DateTime, &str), DateTimeParseError> { let delim = s.find('Z').map(|idx| idx + 1).unwrap_or_else(|| s.len()); let (head, rest) = s.split_at(delim); Ok((parse(head)?, rest)) } - /// Format an [Instant] in the RFC-3339 date format - pub(crate) fn format(instant: &Instant) -> String { + /// Format a [DateTime] in the RFC-3339 date format + pub(crate) fn format(date_time: &DateTime) -> Result { use std::fmt::Write; - let (year, month, day, hour, minute, second, nanos) = { - let s = instant.to_chrono_internal(); + fn out_of_range(cause: E) -> DateTimeFormatError { + DateTimeFormatError::OutOfRange( + format!( + "RFC-3339 timestamps support dates between 0001-01-01T00:00:00.000Z \ + and 9999-12-31T23:59:59.999Z. {}", + cause + ) + .into(), + ) + } + let (year, month, day, hour, minute, second, micros) = { + let s = OffsetDateTime::from_unix_timestamp_nanos(date_time.as_nanos()) + .map_err(out_of_range)?; ( s.year(), - s.month(), + u8::from(s.month()), s.day(), - s.time().hour(), - s.time().minute(), - s.time().second(), - s.timestamp_subsec_nanos(), + s.hour(), + s.minute(), + s.second(), + s.microsecond(), ) }; // This is stated in the assumptions for RFC-3339. ISO-8601 allows for years // between -99,999 and 99,999 inclusive, but RFC-3339 is bound between 0 and 9,999. - assert!( - (0..=9_999).contains(&year), - "years must be between 0 and 9,999 in RFC-3339" - ); + if !(1..=9_999).contains(&year) { + return Err(out_of_range("")); + } let mut out = String::with_capacity(33); write!( @@ -364,17 +431,15 @@ pub(crate) mod rfc3339 { year, month, day, hour, minute, second ) .unwrap(); - format_subsecond_fraction(&mut out, nanos); + format_subsecond_fraction(&mut out, micros); out.push('Z'); - out + Ok(out) } /// Formats sub-second fraction for RFC-3339 (including the '.'). - /// Expects to be called with a number of `nanos` between 0 and 999_999_999 inclusive. - /// The formatted fraction will be truncated to microseconds. - fn format_subsecond_fraction(into: &mut String, nanos: u32) { - debug_assert!(nanos < 1_000_000_000); - let micros = nanos / 1000; + /// Expects to be called with a number of `micros` between 0 and 999_999 inclusive. + fn format_subsecond_fraction(into: &mut String, micros: u32) { + debug_assert!(micros < 1_000_000); if micros > 0 { into.push('.'); let (mut remaining, mut place) = (micros, 100_000); @@ -391,7 +456,7 @@ pub(crate) mod rfc3339 { #[cfg(test)] mod tests { use super::*; - use crate::Instant; + use crate::DateTime; use lazy_static::lazy_static; use proptest::prelude::*; use std::fs::File; @@ -407,8 +472,8 @@ mod tests { smithy_format_value: Option, } impl TestCase { - fn time(&self) -> Instant { - Instant::from_secs_and_nanos( + fn time(&self) -> DateTime { + DateTime::from_secs_and_nanos( ::from_str(&self.canonical_seconds).unwrap(), self.canonical_nanos, ) @@ -438,7 +503,7 @@ mod tests { fn format_test(test_cases: &[TestCase], format: F) where - F: Fn(&Instant) -> String, + F: Fn(&DateTime) -> String, { for test_case in test_cases { if let Some(expected) = test_case.smithy_format_value.as_ref() { @@ -452,7 +517,7 @@ mod tests { fn parse_test(test_cases: &[TestCase], parse: F) where - F: Fn(&str) -> Result, + F: Fn(&str) -> Result, { for test_case in test_cases { let expected = test_case.time(); @@ -490,7 +555,10 @@ mod tests { #[test] fn format_http_date() { - format_test(&TEST_CASES.format_http_date, http_date::format); + fn do_format(date_time: &DateTime) -> String { + http_date::format(date_time).unwrap() + } + format_test(&TEST_CASES.format_http_date, do_format); } #[test] @@ -498,9 +566,33 @@ mod tests { parse_test(&TEST_CASES.parse_http_date, http_date::parse); } + #[test] + fn date_time_out_of_range() { + assert_eq!( + "0001-01-01T00:00:00Z", + rfc3339::format(&DateTime::from_secs(-62_135_596_800)).unwrap() + ); + assert_eq!( + "9999-12-31T23:59:59.999999Z", + rfc3339::format(&DateTime::from_secs_and_nanos(253402300799, 999_999_999)).unwrap() + ); + + assert!(matches!( + rfc3339::format(&DateTime::from_secs(-62_135_596_800 - 1)), + Err(DateTimeFormatError::OutOfRange(_)) + )); + assert!(matches!( + rfc3339::format(&DateTime::from_secs(253402300799 + 1)), + Err(DateTimeFormatError::OutOfRange(_)) + )); + } + #[test] fn format_date_time() { - format_test(&TEST_CASES.format_date_time, rfc3339::format); + fn do_format(date_time: &DateTime) -> String { + rfc3339::format(date_time).unwrap() + } + format_test(&TEST_CASES.format_date_time, do_format); } #[test] @@ -530,35 +622,56 @@ mod tests { let (e2, date2) = rfc3339::read(&date[1..]).expect("should succeed"); assert_eq!(date2, ""); assert_eq!(date, ",1985-04-12T23:20:51Z"); - let expected = Instant::from_secs_and_nanos(482196050, 0); + let expected = DateTime::from_secs_and_nanos(482196050, 0); assert_eq!(e1, expected); - let expected = Instant::from_secs_and_nanos(482196051, 0); + let expected = DateTime::from_secs_and_nanos(482196051, 0); assert_eq!(e2, expected); } + #[test] + fn http_date_out_of_range() { + assert_eq!( + "Mon, 01 Jan 0001 00:00:00 GMT", + http_date::format(&DateTime::from_secs(-62_135_596_800)).unwrap() + ); + assert_eq!( + "Fri, 31 Dec 9999 23:59:59.999 GMT", + http_date::format(&DateTime::from_secs_and_nanos(253402300799, 999_999_999)).unwrap() + ); + + assert!(matches!( + http_date::format(&DateTime::from_secs(-62_135_596_800 - 1)), + Err(DateTimeFormatError::OutOfRange(_)) + )); + assert!(matches!( + http_date::format(&DateTime::from_secs(253402300799 + 1)), + Err(DateTimeFormatError::OutOfRange(_)) + )); + } + #[test] fn http_date_too_much_fraction() { let fractional = "Mon, 16 Dec 2019 23:48:18.1212 GMT"; - assert_eq!( + assert!(matches!( http_date::parse(fractional), - Err(DateParseError::Invalid("incorrectly shaped string")) - ); + Err(DateTimeParseError::Invalid(_)) + )); } #[test] fn http_date_bad_fraction() { let fractional = "Mon, 16 Dec 2019 23:48:18. GMT"; - assert_eq!( + assert!(matches!( http_date::parse(fractional), - Err(DateParseError::IntParseError) - ); + Err(DateTimeParseError::IntParseError) + )); } #[test] fn http_date_read_date() { let fractional = "Mon, 16 Dec 2019 23:48:18.123 GMT,some more stuff"; let ts = 1576540098; - let expected = Instant::from_fractional_seconds(ts, 0.123); + let expected = DateTime::from_fractional_secs(ts, 0.123); let (actual, rest) = http_date::read(fractional).expect("valid"); assert_eq!(rest, ",some more stuff"); assert_eq!(expected, actual); @@ -567,8 +680,8 @@ mod tests { #[track_caller] fn http_date_check_roundtrip(epoch_secs: i64, subsecond_nanos: u32) { - let instant = Instant::from_secs_and_nanos(epoch_secs, subsecond_nanos); - let formatted = http_date::format(&instant); + let date_time = DateTime::from_secs_and_nanos(epoch_secs, subsecond_nanos); + let formatted = http_date::format(&date_time).unwrap(); let parsed = http_date::parse(&formatted); let read = http_date::read(&formatted); match parsed { @@ -576,9 +689,9 @@ mod tests { Ok(date) => { assert!(read.is_ok()); if date.subsecond_nanos != subsecond_nanos { - assert_eq!(http_date::format(&instant), formatted); + assert_eq!(http_date::format(&date_time).unwrap(), formatted); } else { - assert_eq!(date, instant) + assert_eq!(date, date_time) } } } diff --git a/rust-runtime/aws-smithy-types/src/date_time/mod.rs b/rust-runtime/aws-smithy-types/src/date_time/mod.rs new file mode 100644 index 000000000..1c66c9f11 --- /dev/null +++ b/rust-runtime/aws-smithy-types/src/date_time/mod.rs @@ -0,0 +1,546 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! DateTime type for representing Smithy timestamps. + +use num_integer::div_mod_floor; +use num_integer::Integer; +use std::convert::TryFrom; +use std::error::Error as StdError; +use std::fmt; +use std::time::Duration; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + +mod format; +pub use self::format::DateTimeFormatError; +pub use self::format::DateTimeParseError; + +const MILLIS_PER_SECOND: i64 = 1000; +const NANOS_PER_MILLI: u32 = 1_000_000; +const NANOS_PER_SECOND: i128 = 1_000_000_000; +const NANOS_PER_SECOND_U32: u32 = 1_000_000_000; + +/* ANCHOR: date_time */ + +/// DateTime in time. +/// +/// DateTime in time represented as seconds and sub-second nanos since +/// the Unix epoch (January 1, 1970 at midnight UTC/GMT). +/// +/// This type can be converted to/from the standard library's [`SystemTime`](std::time::SystemTime): +/// ```rust +/// # fn doc_fn() -> Result<(), aws_smithy_types::date_time::ConversionError> { +/// # use aws_smithy_types::date_time::DateTime; +/// # use std::time::SystemTime; +/// use std::convert::TryFrom; +/// +/// let the_millennium_as_system_time = SystemTime::try_from(DateTime::from_secs(946_713_600))?; +/// let now_as_date_time = DateTime::from(SystemTime::now()); +/// # Ok(()) +/// # } +/// ``` +/// +/// The [`aws-smithy-types-convert`](https://crates.io/crates/aws-smithy-types-convert) crate +/// can be used for conversions to/from other libraries, such as +/// [`time`](https://crates.io/crates/time) or [`chrono`](https://crates.io/crates/chrono). +#[derive(Debug, PartialEq, Clone, Copy)] +pub struct DateTime { + seconds: i64, + subsecond_nanos: u32, +} + +/* ANCHOR_END: date_time */ + +impl DateTime { + /// Creates a `DateTime` from a number of seconds since the Unix epoch. + pub fn from_secs(epoch_seconds: i64) -> Self { + DateTime { + seconds: epoch_seconds, + subsecond_nanos: 0, + } + } + + /// Creates a `DateTime` from a number of milliseconds since the Unix epoch. + pub fn from_millis(epoch_millis: i64) -> DateTime { + let (seconds, millis) = div_mod_floor(epoch_millis, MILLIS_PER_SECOND); + DateTime::from_secs_and_nanos(seconds, millis as u32 * NANOS_PER_MILLI) + } + + /// Creates a `DateTime` from a number of nanoseconds since the Unix epoch. + pub fn from_nanos(epoch_nanos: i128) -> Result { + let (seconds, subsecond_nanos) = epoch_nanos.div_mod_floor(&NANOS_PER_SECOND); + let seconds = i64::try_from(seconds).map_err(|_| { + ConversionError("given epoch nanos are too large to fit into a DateTime") + })?; + let subsecond_nanos = subsecond_nanos as u32; // safe cast because of the modulus + Ok(DateTime { + seconds, + subsecond_nanos, + }) + } + + /// Returns the number of nanoseconds since the Unix epoch that this `DateTime` represents. + pub fn as_nanos(&self) -> i128 { + let seconds = self.seconds as i128 * NANOS_PER_SECOND; + if seconds < 0 { + let adjusted_nanos = self.subsecond_nanos as i128 - NANOS_PER_SECOND; + seconds + NANOS_PER_SECOND + adjusted_nanos + } else { + seconds + self.subsecond_nanos as i128 + } + } + + /// Creates a `DateTime` from a number of seconds and a fractional second since the Unix epoch. + /// + /// # Example + /// ``` + /// # use aws_smithy_types::DateTime; + /// assert_eq!( + /// DateTime::from_secs_and_nanos(1, 500_000_000u32), + /// DateTime::from_fractional_secs(1, 0.5), + /// ); + /// ``` + pub fn from_fractional_secs(epoch_seconds: i64, fraction: f64) -> Self { + let subsecond_nanos = (fraction * 1_000_000_000_f64) as u32; + DateTime::from_secs_and_nanos(epoch_seconds, subsecond_nanos) + } + + /// Creates a `DateTime` from a number of seconds and sub-second nanos since the Unix epoch. + /// + /// # Example + /// ``` + /// # use aws_smithy_types::DateTime; + /// assert_eq!( + /// DateTime::from_fractional_secs(1, 0.5), + /// DateTime::from_secs_and_nanos(1, 500_000_000u32), + /// ); + /// ``` + pub fn from_secs_and_nanos(seconds: i64, subsecond_nanos: u32) -> Self { + if subsecond_nanos >= 1_000_000_000 { + panic!("{} is > 1_000_000_000", subsecond_nanos) + } + DateTime { + seconds, + subsecond_nanos, + } + } + + /// Returns the `DateTime` value as an `f64` representing the seconds since the Unix epoch. + /// + /// _Note: This conversion will lose precision due to the nature of floating point numbers._ + pub fn as_secs_f64(&self) -> f64 { + self.seconds as f64 + self.subsecond_nanos as f64 / 1_000_000_000_f64 + } + + /// Creates a `DateTime` from an `f64` representing the number of seconds since the Unix epoch. + /// + /// # Example + /// ``` + /// # use aws_smithy_types::DateTime; + /// assert_eq!( + /// DateTime::from_fractional_secs(1, 0.5), + /// DateTime::from_secs_f64(1.5), + /// ); + /// ``` + pub fn from_secs_f64(epoch_seconds: f64) -> Self { + let seconds = epoch_seconds.floor() as i64; + let rem = epoch_seconds - epoch_seconds.floor(); + DateTime::from_fractional_secs(seconds, rem) + } + + /// Parses a `DateTime` from a string using the given `format`. + pub fn from_str(s: &str, format: Format) -> Result { + match format { + Format::DateTime => format::rfc3339::parse(s), + Format::HttpDate => format::http_date::parse(s), + Format::EpochSeconds => format::epoch_seconds::parse(s), + } + } + + /// Returns true if sub-second nanos is greater than zero. + pub fn has_subsec_nanos(&self) -> bool { + self.subsecond_nanos != 0 + } + + /// Returns the epoch seconds component of the `DateTime`. + /// + /// _Note: this does not include the sub-second nanos._ + pub fn secs(&self) -> i64 { + self.seconds + } + + /// Returns the sub-second nanos component of the `DateTime`. + /// + /// _Note: this does not include the number of seconds since the epoch._ + pub fn subsec_nanos(&self) -> u32 { + self.subsecond_nanos + } + + /// Converts the `DateTime` to the number of milliseconds since the Unix epoch. + /// + /// This is fallible since `DateTime` holds more precision than an `i64`, and will + /// return a `ConversionError` for `DateTime` values that can't be converted. + pub fn to_millis(self) -> Result { + let subsec_millis = + Integer::div_floor(&i64::from(self.subsecond_nanos), &(NANOS_PER_MILLI as i64)); + if self.seconds < 0 { + self.seconds + .checked_add(1) + .and_then(|seconds| seconds.checked_mul(MILLIS_PER_SECOND)) + .and_then(|millis| millis.checked_sub(1000 - subsec_millis)) + } else { + self.seconds + .checked_mul(MILLIS_PER_SECOND) + .and_then(|millis| millis.checked_add(subsec_millis)) + } + .ok_or(ConversionError( + "DateTime value too large to fit into i64 epoch millis", + )) + } + + /// Read 1 date of `format` from `s`, expecting either `delim` or EOF + /// + /// Enable parsing multiple dates from the same string + pub fn read(s: &str, format: Format, delim: char) -> Result<(Self, &str), DateTimeParseError> { + let (inst, next) = match format { + Format::DateTime => format::rfc3339::read(s)?, + Format::HttpDate => format::http_date::read(s)?, + Format::EpochSeconds => { + let split_point = s.find(delim).unwrap_or_else(|| s.len()); + let (s, rest) = s.split_at(split_point); + (Self::from_str(s, format)?, rest) + } + }; + if next.is_empty() { + Ok((inst, next)) + } else if next.starts_with(delim) { + Ok((inst, &next[1..])) + } else { + Err(DateTimeParseError::Invalid( + "didn't find expected delimiter".into(), + )) + } + } + + /// Formats the `DateTime` to a string using the given `format`. + /// + /// Returns an error if the given `DateTime` cannot be represented by the desired format. + pub fn fmt(&self, format: Format) -> Result { + match format { + Format::DateTime => format::rfc3339::format(self), + Format::EpochSeconds => Ok(format::epoch_seconds::format(self)), + Format::HttpDate => format::http_date::format(self), + } + } +} + +/// Tries to convert a [`DateTime`] into a [`SystemTime`]. +/// +/// This can fail if the the `DateTime` value is larger or smaller than what the `SystemTime` +/// can represent on the operating system it's compiled for. On Linux, for example, it will only +/// fail on `Instant::from_secs(i64::MIN)` (with any nanoseconds value). On Windows, however, +/// Rust's standard library uses a smaller precision type for `SystemTime`, and it will fail +/// conversion for a much larger range of date-times. This is only an issue if dealing with +/// date-times beyond several thousands of years from now. +impl TryFrom for SystemTime { + type Error = ConversionError; + + fn try_from(date_time: DateTime) -> Result { + if date_time.secs() < 0 { + let mut secs = date_time.secs().unsigned_abs(); + let mut nanos = date_time.subsec_nanos(); + if date_time.has_subsec_nanos() { + // This is safe because we just went from a negative number to a positive and are subtracting + secs -= 1; + // This is safe because nanos are < 999,999,999 + nanos = NANOS_PER_SECOND_U32 - nanos; + } + UNIX_EPOCH + .checked_sub(Duration::new(secs, nanos)) + .ok_or(ConversionError( + "overflow occurred when subtracting duration from UNIX_EPOCH", + )) + } else { + UNIX_EPOCH + .checked_add(Duration::new( + date_time.secs().unsigned_abs(), + date_time.subsec_nanos(), + )) + .ok_or(ConversionError( + "overflow occurred when adding duration to UNIX_EPOCH", + )) + } + } +} + +impl From for DateTime { + fn from(time: SystemTime) -> Self { + if time < UNIX_EPOCH { + let duration = UNIX_EPOCH.duration_since(time).expect("time < UNIX_EPOCH"); + let mut secs = -(duration.as_secs() as i128); + let mut nanos = duration.subsec_nanos() as i128; + if nanos != 0 { + secs -= 1; + nanos = NANOS_PER_SECOND - nanos; + } + DateTime::from_nanos(secs * NANOS_PER_SECOND + nanos) + .expect("SystemTime has same precision as DateTime") + } else { + let duration = time.duration_since(UNIX_EPOCH).expect("UNIX_EPOCH <= time"); + DateTime::from_secs_and_nanos( + i64::try_from(duration.as_secs()) + .expect("SystemTime has same precision as DateTime"), + duration.subsec_nanos(), + ) + } + } +} + +/// Failure to convert a `DateTime` to or from another type. +#[derive(Debug)] +#[non_exhaustive] +pub struct ConversionError(&'static str); + +impl StdError for ConversionError {} + +impl fmt::Display for ConversionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Formats for representing a `DateTime` in the Smithy protocols. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Format { + /// RFC-3339 Date Time. + DateTime, + /// Date format used by the HTTP `Date` header, specified in RFC-7231. + HttpDate, + /// Number of seconds since the Unix epoch formatted as a floating point. + EpochSeconds, +} + +#[cfg(test)] +mod test { + use crate::date_time::Format; + use crate::DateTime; + use std::convert::TryFrom; + use std::time::SystemTime; + use time::format_description::well_known::Rfc3339; + use time::OffsetDateTime; + + #[test] + fn test_fmt() { + let date_time = DateTime::from_secs(1576540098); + assert_eq!( + date_time.fmt(Format::DateTime).unwrap(), + "2019-12-16T23:48:18Z" + ); + assert_eq!(date_time.fmt(Format::EpochSeconds).unwrap(), "1576540098"); + assert_eq!( + date_time.fmt(Format::HttpDate).unwrap(), + "Mon, 16 Dec 2019 23:48:18 GMT" + ); + + let date_time = DateTime::from_fractional_secs(1576540098, 0.52); + assert_eq!( + date_time.fmt(Format::DateTime).unwrap(), + "2019-12-16T23:48:18.52Z" + ); + assert_eq!( + date_time.fmt(Format::EpochSeconds).unwrap(), + "1576540098.52" + ); + assert_eq!( + date_time.fmt(Format::HttpDate).unwrap(), + "Mon, 16 Dec 2019 23:48:18.52 GMT" + ); + } + + #[test] + fn test_fmt_zero_seconds() { + let date_time = DateTime::from_secs(1576540080); + assert_eq!( + date_time.fmt(Format::DateTime).unwrap(), + "2019-12-16T23:48:00Z" + ); + assert_eq!(date_time.fmt(Format::EpochSeconds).unwrap(), "1576540080"); + assert_eq!( + date_time.fmt(Format::HttpDate).unwrap(), + "Mon, 16 Dec 2019 23:48:00 GMT" + ); + } + + #[test] + fn test_read_single_http_date() { + let s = "Mon, 16 Dec 2019 23:48:18 GMT"; + let (_, next) = DateTime::read(s, Format::HttpDate, ',').expect("valid"); + assert_eq!(next, ""); + } + + #[test] + fn test_read_single_float() { + let s = "1576540098.52"; + let (_, next) = DateTime::read(s, Format::EpochSeconds, ',').expect("valid"); + assert_eq!(next, ""); + } + + #[test] + fn test_read_many_float() { + let s = "1576540098.52,1576540098.53"; + let (_, next) = DateTime::read(s, Format::EpochSeconds, ',').expect("valid"); + assert_eq!(next, "1576540098.53"); + } + + #[test] + fn test_ready_many_http_date() { + let s = "Mon, 16 Dec 2019 23:48:18 GMT,Tue, 17 Dec 2019 23:48:18 GMT"; + let (_, next) = DateTime::read(s, Format::HttpDate, ',').expect("valid"); + assert_eq!(next, "Tue, 17 Dec 2019 23:48:18 GMT"); + } + + #[derive(Debug)] + struct EpochMillisTestCase { + rfc3339: &'static str, + epoch_millis: i64, + epoch_seconds: i64, + epoch_subsec_nanos: u32, + } + + // These test case values were generated from the following Kotlin JVM code: + // ```kotlin + // val date_time = DateTime.ofEpochMilli(); + // println(DateTimeFormatter.ISO_DATE_TIME.format(date_time.atOffset(ZoneOffset.UTC))) + // println(date_time.epochSecond) + // println(date_time.nano) + // ``` + const EPOCH_MILLIS_TEST_CASES: &[EpochMillisTestCase] = &[ + EpochMillisTestCase { + rfc3339: "2021-07-30T21:20:04.123Z", + epoch_millis: 1627680004123, + epoch_seconds: 1627680004, + epoch_subsec_nanos: 123000000, + }, + EpochMillisTestCase { + rfc3339: "1918-06-04T02:39:55.877Z", + epoch_millis: -1627680004123, + epoch_seconds: -1627680005, + epoch_subsec_nanos: 877000000, + }, + EpochMillisTestCase { + rfc3339: "+292278994-08-17T07:12:55.807Z", + epoch_millis: i64::MAX, + epoch_seconds: 9223372036854775, + epoch_subsec_nanos: 807000000, + }, + EpochMillisTestCase { + rfc3339: "-292275055-05-16T16:47:04.192Z", + epoch_millis: i64::MIN, + epoch_seconds: -9223372036854776, + epoch_subsec_nanos: 192000000, + }, + ]; + + #[test] + fn to_millis() { + for test_case in EPOCH_MILLIS_TEST_CASES { + println!("Test case: {:?}", test_case); + let date_time = DateTime::from_secs_and_nanos( + test_case.epoch_seconds, + test_case.epoch_subsec_nanos, + ); + assert_eq!(test_case.epoch_seconds, date_time.secs()); + assert_eq!(test_case.epoch_subsec_nanos, date_time.subsec_nanos()); + assert_eq!(test_case.epoch_millis, date_time.to_millis().unwrap()); + } + + assert!(DateTime::from_secs_and_nanos(i64::MAX, 0) + .to_millis() + .is_err()); + } + + #[test] + fn from_millis() { + for test_case in EPOCH_MILLIS_TEST_CASES { + println!("Test case: {:?}", test_case); + let date_time = DateTime::from_millis(test_case.epoch_millis); + assert_eq!(test_case.epoch_seconds, date_time.secs()); + assert_eq!(test_case.epoch_subsec_nanos, date_time.subsec_nanos()); + } + } + + #[test] + fn to_from_millis_round_trip() { + for millis in &[0, 1627680004123, -1627680004123, i64::MAX, i64::MIN] { + assert_eq!(*millis, DateTime::from_millis(*millis).to_millis().unwrap()); + } + } + + #[test] + fn as_nanos() { + assert_eq!( + -9_223_372_036_854_775_807_000_000_001_i128, + DateTime::from_secs_and_nanos(i64::MIN, 999_999_999).as_nanos() + ); + assert_eq!( + -10_876_543_211, + DateTime::from_secs_and_nanos(-11, 123_456_789).as_nanos() + ); + assert_eq!(0, DateTime::from_secs_and_nanos(0, 0).as_nanos()); + assert_eq!( + 11_123_456_789, + DateTime::from_secs_and_nanos(11, 123_456_789).as_nanos() + ); + assert_eq!( + 9_223_372_036_854_775_807_999_999_999_i128, + DateTime::from_secs_and_nanos(i64::MAX, 999_999_999).as_nanos() + ); + } + + #[test] + fn from_nanos() { + assert_eq!( + DateTime::from_secs_and_nanos(i64::MIN, 999_999_999), + DateTime::from_nanos(-9_223_372_036_854_775_807_000_000_001_i128).unwrap(), + ); + assert_eq!( + DateTime::from_secs_and_nanos(-11, 123_456_789), + DateTime::from_nanos(-10_876_543_211).unwrap(), + ); + assert_eq!( + DateTime::from_secs_and_nanos(0, 0), + DateTime::from_nanos(0).unwrap(), + ); + assert_eq!( + DateTime::from_secs_and_nanos(11, 123_456_789), + DateTime::from_nanos(11_123_456_789).unwrap(), + ); + assert_eq!( + DateTime::from_secs_and_nanos(i64::MAX, 999_999_999), + DateTime::from_nanos(9_223_372_036_854_775_807_999_999_999_i128).unwrap(), + ); + assert!(DateTime::from_nanos(-10_000_000_000_000_000_000_999_999_999_i128).is_err()); + assert!(DateTime::from_nanos(10_000_000_000_000_000_000_999_999_999_i128).is_err()); + } + + #[test] + fn system_time_conversions() { + // Check agreement + let date_time = DateTime::from_str("1000-01-02T01:23:10.123Z", Format::DateTime).unwrap(); + let off_date_time = OffsetDateTime::parse("1000-01-02T01:23:10.123Z", &Rfc3339).unwrap(); + assert_eq!( + SystemTime::from(off_date_time), + SystemTime::try_from(date_time).unwrap() + ); + + let date_time = DateTime::from_str("2039-10-31T23:23:10.456Z", Format::DateTime).unwrap(); + let off_date_time = OffsetDateTime::parse("2039-10-31T23:23:10.456Z", &Rfc3339).unwrap(); + assert_eq!( + SystemTime::from(off_date_time), + SystemTime::try_from(date_time).unwrap() + ); + } +} diff --git a/rust-runtime/aws-smithy-types/src/instant/mod.rs b/rust-runtime/aws-smithy-types/src/instant/mod.rs deleted file mode 100644 index dc13bc149..000000000 --- a/rust-runtime/aws-smithy-types/src/instant/mod.rs +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -//! Instant value for representing Smithy timestamps. -//! -//! Unlike [`std::time::Instant`], this instant is not opaque. The time inside of it can be -//! read and modified. It also holds logic for parsing and formatting timestamps in any of -//! the timestamp formats that [Smithy](https://awslabs.github.io/smithy/) supports. - -use crate::instant::format::DateParseError; -use chrono::{DateTime, NaiveDateTime, Utc}; -use num_integer::div_mod_floor; -use num_integer::Integer; -use std::error::Error as StdError; -use std::fmt; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -mod format; - -const MILLIS_PER_SECOND: i64 = 1000; -const NANOS_PER_MILLI: u32 = 1_000_000; - -/* ANCHOR: instant */ - -/// Instant in time. -/// -/// Instant in time represented as seconds and sub-second nanos since -/// the Unix epoch (January 1, 1970 at midnight UTC/GMT). -#[derive(Debug, PartialEq, Clone, Copy)] -pub struct Instant { - seconds: i64, - subsecond_nanos: u32, -} - -/* ANCHOR_END: instant */ - -impl Instant { - /// Creates an `Instant` from a number of seconds since the Unix epoch. - pub fn from_epoch_seconds(epoch_seconds: i64) -> Self { - Instant { - seconds: epoch_seconds, - subsecond_nanos: 0, - } - } - - /// Creates an `Instant` from a number of seconds and a fractional second since the Unix epoch. - /// - /// # Example - /// ``` - /// # use aws_smithy_types::Instant; - /// assert_eq!( - /// Instant::from_secs_and_nanos(1, 500_000_000u32), - /// Instant::from_fractional_seconds(1, 0.5), - /// ); - /// ``` - pub fn from_fractional_seconds(epoch_seconds: i64, fraction: f64) -> Self { - let subsecond_nanos = (fraction * 1_000_000_000_f64) as u32; - Instant::from_secs_and_nanos(epoch_seconds, subsecond_nanos) - } - - /// Creates an `Instant` from a number of seconds and sub-second nanos since the Unix epoch. - /// - /// # Example - /// ``` - /// # use aws_smithy_types::Instant; - /// assert_eq!( - /// Instant::from_fractional_seconds(1, 0.5), - /// Instant::from_secs_and_nanos(1, 500_000_000u32), - /// ); - /// ``` - pub fn from_secs_and_nanos(seconds: i64, subsecond_nanos: u32) -> Self { - if subsecond_nanos >= 1_000_000_000 { - panic!("{} is > 1_000_000_000", subsecond_nanos) - } - Instant { - seconds, - subsecond_nanos, - } - } - - /// Creates an `Instant` from an `f64` representing the number of seconds since the Unix epoch. - /// - /// # Example - /// ``` - /// # use aws_smithy_types::Instant; - /// assert_eq!( - /// Instant::from_fractional_seconds(1, 0.5), - /// Instant::from_f64(1.5), - /// ); - /// ``` - pub fn from_f64(epoch_seconds: f64) -> Self { - let seconds = epoch_seconds.floor() as i64; - let rem = epoch_seconds - epoch_seconds.floor(); - Instant::from_fractional_seconds(seconds, rem) - } - - /// Creates an `Instant` from a [`SystemTime`]. - pub fn from_system_time(system_time: SystemTime) -> Self { - let duration = system_time - .duration_since(UNIX_EPOCH) - .expect("SystemTime can never represent a time before the Unix Epoch"); - Instant { - seconds: duration.as_secs() as i64, - subsecond_nanos: duration.subsec_nanos(), - } - } - - /// Parses an `Instant` from a string using the given `format`. - pub fn from_str(s: &str, format: Format) -> Result { - match format { - Format::DateTime => format::rfc3339::parse(s), - Format::HttpDate => format::http_date::parse(s), - Format::EpochSeconds => format::epoch_seconds::parse(s), - } - } - - /// Read 1 date of `format` from `s`, expecting either `delim` or EOF - /// - /// Enable parsing multiple dates from the same string - pub fn read(s: &str, format: Format, delim: char) -> Result<(Self, &str), DateParseError> { - let (inst, next) = match format { - Format::DateTime => format::rfc3339::read(s)?, - Format::HttpDate => format::http_date::read(s)?, - Format::EpochSeconds => { - let split_point = s.find(delim).unwrap_or_else(|| s.len()); - let (s, rest) = s.split_at(split_point); - (Self::from_str(s, format)?, rest) - } - }; - if next.is_empty() { - Ok((inst, next)) - } else if next.starts_with(delim) { - Ok((inst, &next[1..])) - } else { - Err(DateParseError::Invalid("didn't find expected delimiter")) - } - } - - /// Converts the `Instant` to a chrono `DateTime`. - #[cfg(feature = "chrono-conversions")] - pub fn to_chrono(self) -> DateTime { - self.to_chrono_internal() - } - - fn to_chrono_internal(self) -> DateTime { - DateTime::::from_utc( - NaiveDateTime::from_timestamp(self.seconds, self.subsecond_nanos), - Utc, - ) - } - - /// Convert this `Instant` to a [`SystemTime`](std::time::SystemTime) - /// - /// Since SystemTime cannot represent times prior to the unix epoch, if this time is before - /// 1/1/1970, this function will return `None`. - pub fn to_system_time(self) -> Option { - if self.seconds < 0 { - None - } else { - Some( - UNIX_EPOCH - + Duration::from_secs(self.seconds as u64) - + Duration::from_nanos(self.subsecond_nanos as u64), - ) - } - } - - /// Returns true if sub-second nanos is greater than zero. - pub fn has_nanos(&self) -> bool { - self.subsecond_nanos != 0 - } - - /// Returns the `Instant` value as an `f64` representing the seconds since the Unix epoch. - pub fn epoch_fractional_seconds(&self) -> f64 { - self.seconds as f64 + self.subsecond_nanos as f64 / 1_000_000_000_f64 - } - - /// Returns the epoch seconds component of the `Instant`. - /// - /// _Note: this does not include the sub-second nanos._ - pub fn epoch_seconds(&self) -> i64 { - self.seconds - } - - /// Returns the sub-second nanos component of the `Instant`. - /// - /// _Note: this does not include the number of seconds since the epoch._ - pub fn epoch_subsecond_nanos(&self) -> u32 { - self.subsecond_nanos - } - - /// Converts the `Instant` to the number of milliseconds since the Unix epoch. - /// This is fallible since `Instant` holds more precision than an `i64`, and will - /// return a `ConversionError` for `Instant` values that can't be converted. - pub fn to_epoch_millis(self) -> Result { - let subsec_millis = - Integer::div_floor(&i64::from(self.subsecond_nanos), &(NANOS_PER_MILLI as i64)); - if self.seconds < 0 { - self.seconds - .checked_add(1) - .and_then(|seconds| seconds.checked_mul(MILLIS_PER_SECOND)) - .and_then(|millis| millis.checked_sub(1000 - subsec_millis)) - } else { - self.seconds - .checked_mul(MILLIS_PER_SECOND) - .and_then(|millis| millis.checked_add(subsec_millis)) - } - .ok_or(ConversionError( - "Instant value too large to fit into i64 epoch millis", - )) - } - - /// Converts number of milliseconds since the Unix epoch into an `Instant`. - pub fn from_epoch_millis(epoch_millis: i64) -> Instant { - let (seconds, millis) = div_mod_floor(epoch_millis, MILLIS_PER_SECOND); - Instant::from_secs_and_nanos(seconds, millis as u32 * NANOS_PER_MILLI) - } - - /// Formats the `Instant` to a string using the given `format`. - pub fn fmt(&self, format: Format) -> String { - match format { - Format::DateTime => format::rfc3339::format(self), - Format::EpochSeconds => format::epoch_seconds::format(self), - Format::HttpDate => format::http_date::format(self), - } - } -} - -/// Failure to convert an `Instant` to or from another type. -#[derive(Debug)] -#[non_exhaustive] -pub struct ConversionError(&'static str); - -impl StdError for ConversionError {} - -impl fmt::Display for ConversionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[cfg(feature = "chrono-conversions")] -impl From> for Instant { - fn from(value: DateTime) -> Instant { - Instant::from_secs_and_nanos(value.timestamp(), value.timestamp_subsec_nanos()) - } -} - -#[cfg(feature = "chrono-conversions")] -impl From> for Instant { - fn from(value: DateTime) -> Instant { - value.with_timezone(&Utc).into() - } -} - -/// Formats for representing an `Instant` in the Smithy protocols. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum Format { - /// RFC-3339 Date Time. - DateTime, - /// Date format used by the HTTP `Date` header, specified in RFC-7231. - HttpDate, - /// Number of seconds since the Unix epoch formatted as a floating point. - EpochSeconds, -} - -#[cfg(test)] -mod test { - use crate::instant::Format; - use crate::Instant; - - #[test] - fn test_instant_fmt() { - let instant = Instant::from_epoch_seconds(1576540098); - assert_eq!(instant.fmt(Format::DateTime), "2019-12-16T23:48:18Z"); - assert_eq!(instant.fmt(Format::EpochSeconds), "1576540098"); - assert_eq!( - instant.fmt(Format::HttpDate), - "Mon, 16 Dec 2019 23:48:18 GMT" - ); - - let instant = Instant::from_fractional_seconds(1576540098, 0.52); - assert_eq!(instant.fmt(Format::DateTime), "2019-12-16T23:48:18.52Z"); - assert_eq!(instant.fmt(Format::EpochSeconds), "1576540098.52"); - assert_eq!( - instant.fmt(Format::HttpDate), - "Mon, 16 Dec 2019 23:48:18.52 GMT" - ); - } - - #[test] - fn test_instant_fmt_zero_seconds() { - let instant = Instant::from_epoch_seconds(1576540080); - assert_eq!(instant.fmt(Format::DateTime), "2019-12-16T23:48:00Z"); - assert_eq!(instant.fmt(Format::EpochSeconds), "1576540080"); - assert_eq!( - instant.fmt(Format::HttpDate), - "Mon, 16 Dec 2019 23:48:00 GMT" - ); - } - - #[test] - fn test_read_single_http_date() { - let s = "Mon, 16 Dec 2019 23:48:18 GMT"; - let (_, next) = Instant::read(s, Format::HttpDate, ',').expect("valid"); - assert_eq!(next, ""); - } - - #[test] - fn test_read_single_float() { - let s = "1576540098.52"; - let (_, next) = Instant::read(s, Format::EpochSeconds, ',').expect("valid"); - assert_eq!(next, ""); - } - - #[test] - fn test_read_many_float() { - let s = "1576540098.52,1576540098.53"; - let (_, next) = Instant::read(s, Format::EpochSeconds, ',').expect("valid"); - assert_eq!(next, "1576540098.53"); - } - - #[test] - fn test_ready_many_http_date() { - let s = "Mon, 16 Dec 2019 23:48:18 GMT,Tue, 17 Dec 2019 23:48:18 GMT"; - let (_, next) = Instant::read(s, Format::HttpDate, ',').expect("valid"); - assert_eq!(next, "Tue, 17 Dec 2019 23:48:18 GMT"); - } - - #[test] - #[cfg(feature = "chrono-conversions")] - fn chrono_conversions_round_trip() { - for (seconds, nanos) in &[(1234, 56789), (-1234, 4321)] { - let instant = Instant::from_secs_and_nanos(*seconds, *nanos); - let chrono = instant.to_chrono(); - let instant_again: Instant = chrono.into(); - assert_eq!(instant, instant_again); - } - } - - #[derive(Debug)] - struct EpochMillisTestCase { - rfc3339: &'static str, - epoch_millis: i64, - epoch_seconds: i64, - epoch_subsec_nanos: u32, - } - - // These test case values were generated from the following Kotlin JVM code: - // ```kotlin - // val instant = Instant.ofEpochMilli(); - // println(DateTimeFormatter.ISO_DATE_TIME.format(instant.atOffset(ZoneOffset.UTC))) - // println(instant.epochSecond) - // println(instant.nano) - // ``` - const EPOCH_MILLIS_TEST_CASES: &[EpochMillisTestCase] = &[ - EpochMillisTestCase { - rfc3339: "2021-07-30T21:20:04.123Z", - epoch_millis: 1627680004123, - epoch_seconds: 1627680004, - epoch_subsec_nanos: 123000000, - }, - EpochMillisTestCase { - rfc3339: "1918-06-04T02:39:55.877Z", - epoch_millis: -1627680004123, - epoch_seconds: -1627680005, - epoch_subsec_nanos: 877000000, - }, - EpochMillisTestCase { - rfc3339: "+292278994-08-17T07:12:55.807Z", - epoch_millis: i64::MAX, - epoch_seconds: 9223372036854775, - epoch_subsec_nanos: 807000000, - }, - EpochMillisTestCase { - rfc3339: "-292275055-05-16T16:47:04.192Z", - epoch_millis: i64::MIN, - epoch_seconds: -9223372036854776, - epoch_subsec_nanos: 192000000, - }, - ]; - - #[test] - fn to_epoch_millis() { - for test_case in EPOCH_MILLIS_TEST_CASES { - println!("Test case: {:?}", test_case); - let instant = - Instant::from_secs_and_nanos(test_case.epoch_seconds, test_case.epoch_subsec_nanos); - assert_eq!(test_case.epoch_seconds, instant.epoch_seconds()); - assert_eq!( - test_case.epoch_subsec_nanos, - instant.epoch_subsecond_nanos() - ); - assert_eq!(test_case.epoch_millis, instant.to_epoch_millis().unwrap()); - } - - assert!(Instant::from_secs_and_nanos(i64::MAX, 0) - .to_epoch_millis() - .is_err()); - } - - #[test] - fn from_epoch_millis() { - for test_case in EPOCH_MILLIS_TEST_CASES { - println!("Test case: {:?}", test_case); - let instant = Instant::from_epoch_millis(test_case.epoch_millis); - assert_eq!(test_case.epoch_seconds, instant.epoch_seconds()); - assert_eq!( - test_case.epoch_subsec_nanos, - instant.epoch_subsecond_nanos() - ); - } - } - - #[test] - fn to_from_epoch_millis_round_trip() { - for millis in &[0, 1627680004123, -1627680004123, i64::MAX, i64::MIN] { - assert_eq!( - *millis, - Instant::from_epoch_millis(*millis) - .to_epoch_millis() - .unwrap() - ); - } - } -} diff --git a/rust-runtime/aws-smithy-types/src/lib.rs b/rust-runtime/aws-smithy-types/src/lib.rs index b3fe34c61..356edae96 100644 --- a/rust-runtime/aws-smithy-types/src/lib.rs +++ b/rust-runtime/aws-smithy-types/src/lib.rs @@ -16,11 +16,11 @@ use std::collections::HashMap; pub mod base64; -pub mod instant; +pub mod date_time; pub mod primitive; pub mod retry; -pub use crate::instant::Instant; +pub use crate::date_time::DateTime; /// Binary Blob Type ///