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
This commit is contained in:
John DiSanti 2021-11-11 16:01:30 -08:00 committed by GitHub
parent 3ae9bcf78f
commit 72eae556ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1955 additions and 1001 deletions

View File

@ -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<Utc>
let date_time = DateTime::from_chrono_utc(chrono_date_time);
// For chrono::DateTime<FixedOffset>
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<Utc>`.
**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)
===================================

View File

@ -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<Utc>
let date_time = DateTime::from_chrono_utc(chrono_date_time);
// For chrono::DateTime<FixedOffset>
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)
===================================

View File

@ -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,

View File

@ -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

View File

@ -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()

View File

@ -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")

View File

@ -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"] }

View File

@ -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<Utc>` in `YYYYMMDD` format.
pub(crate) fn format_date(date: &Date<Utc>) -> String {
date.format(DATE_FORMAT).to_string()
}
/// Parses `YYYYMMDD` formatted dates into a chrono `Date<Utc>`.
pub(crate) fn parse_date(date_str: &str) -> Result<Date<Utc>, ParseError> {
Ok(Date::<Utc>::from_utc(
NaiveDate::parse_from_str(date_str, "%Y%m%d")?,
Utc,
))
}
/// Formats a chrono `DateTime<Utc>` in `YYYYMMDD'T'HHMMSS'Z'` format.
pub(crate) fn format_date_time(date_time: &DateTime<Utc>) -> String {
date_time.format(DATE_TIME_FORMAT).to_string()
}
/// Parses `YYYYMMDD'T'HHMMSS'Z'` formatted dates into a chrono `DateTime<Utc>`.
pub(crate) fn parse_date_time(date_time_str: &str) -> Result<DateTime<Utc>, ParseError> {
Ok(DateTime::<Utc>::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));
}
}

View File

@ -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<SystemTime, ParseError> {
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<SystemTime, ParseError> {
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));
}
}

View File

@ -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, &params).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<Utc>,
time: SystemTime,
params: &SigningParams<'_>,
) -> Vec<u8> {
// 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<u8> = 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<Message> {
// 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,
&params.date_time,
params.time,
&params
))
.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");
}

View File

@ -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(&params.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(&params.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<Utc>,
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<SigningScope<'a>, 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<Utc>,
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<Self, Self::Error> {
let lines = s.lines().collect::<Vec<&str>>();
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<Utc>,
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");

View File

@ -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();

View File

@ -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,
};

View File

@ -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<Utc>,
/// 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<DateTime<Utc>>,
time: Option<SystemTime>,
settings: Option<S>,
}
@ -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<Utc>) -> 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<DateTime<Utc>>) {
self.date_time = date_time;
/// Sets the time to be used in the signature (required)
pub fn set_time(&mut self, time: Option<SystemTime>) {
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"))?,

View File

@ -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<Utc>,
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";

View File

@ -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(

View File

@ -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 }

View File

@ -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!();
}

View File

@ -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"

View File

@ -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);

View File

@ -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(())
}

View File

@ -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!();

View File

@ -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"] }

View File

@ -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(())
}

View File

@ -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"] }

View File

@ -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(())
}

View File

@ -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"] }

View File

@ -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!(

View File

@ -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 : {:#?}",

View File

@ -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(),

View File

@ -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<Instant>.contains(Instant) would return true.
* Option<Instant>.contains(Blob) would return false.
* Returns true if [this] contains [t] anywhere within its tree. For example,
* Option<DateTime>.contains(DateTime) would return true.
* Option<DateTime>.contains(Blob) would return false.
*/
fun <T : RustType> RustType.contains(t: T): Boolean = when (this) {
t -> true

View File

@ -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"
)
}

View File

@ -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 {

View File

@ -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 -> { }
}
}
}

View File

@ -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)
)
/**

View File

@ -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(

View File

@ -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,

View File

@ -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)
}

View File

@ -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))

View File

@ -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))

View File

@ -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())
}

View File

@ -221,8 +221,8 @@ class SymbolBuilderTest {
.unwrap()
val provider: SymbolProvider = testSymbolProvider(model)
val sym = provider.toSymbol(member)
sym.rustType().render(false) shouldBe "Option<Instant>"
sym.referenceClosure().map { it.name } shouldContain "Instant"
sym.rustType().render(false) shouldBe "Option<DateTime>"
sym.referenceClosure().map { it.name } shouldContain "DateTime"
sym.references[0].dependencies.shouldNotBeEmpty()
}

View File

@ -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<bool> = one.field_boolean();
let _: bool = one.field_primitive_boolean();

View File

@ -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())

View File

@ -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())

View File

@ -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);
""",
)

View File

@ -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:

View File

@ -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"
]

View File

@ -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",

View File

@ -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),
}

View File

@ -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<Instant, &Self> {
pub fn as_timestamp(&self) -> Result<DateTime, &Self> {
match self {
HeaderValue::Timestamp(value) => Ok(*value),
_ => Err(self),
@ -199,9 +199,7 @@ mod value {
TYPE_TIMESTAMP => {
if buffer.remaining() >= size_of::<i64>() {
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<u32, Error> {
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",

View File

@ -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> {

View File

@ -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<HeaderValue>,
format: Format,
) -> Result<Vec<Instant>, ParseError> {
) -> Result<Vec<DateTime>, 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);

View File

@ -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: AsRef<str>>(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<String, DateTimeFormatError> {
Ok(crate::query::fmt_string(t.fmt(format)?))
}
#[cfg(test)]

View File

@ -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<SerializationError> for BuildError {
}
}
impl From<DateTimeFormatError> 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<DateTimeFormatError> for SerializationError {
fn from(err: DateTimeFormatError) -> SerializationError {
SerializationError::DateTimeFormatError { cause: err }
}
}
#[derive(Debug)]
pub struct Operation<H, R> {
request: Request,

View File

@ -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: AsRef<str>>(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<String, DateTimeFormatError> {
Ok(fmt_string(t.fmt(format)?))
}
/// Simple abstraction to enable appending params to a string as query params

View File

@ -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<Result<Token<'_>, Error>>) -> Result<Op
/// Expects a [Token::ValueNull], [Token::ValueString], or [Token::ValueNumber] depending
/// on the passed in `timestamp_format`. If there is a non-null value, it interprets it as an
/// [Instant] in the requested format.
/// [`DateTime` ] in the requested format.
pub fn expect_timestamp_or_null(
token: Option<Result<Token<'_>, Error>>,
timestamp_format: Format,
) -> Result<Option<Instant>, Error> {
) -> Result<Option<DateTime>, 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(

View File

@ -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!(

View File

@ -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!(

View File

@ -0,0 +1,18 @@
[package]
name = "aws-smithy-types-convert"
version = "0.0.0-smithy-rs-head"
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
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 }

View File

@ -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.

View File

@ -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<dyn StdError + Send + Sync + 'static>),
}
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<Utc> = 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<chrono::Utc>;
/// Converts a [`chrono::DateTime`] with timezone UTC to a [`DateTime`].
#[cfg(feature = "convert-chrono")]
fn from_chrono_utc(time: chrono::DateTime<chrono::Utc>) -> DateTime;
/// Converts a [`chrono::DateTime`] with an offset timezone to a [`DateTime`].
#[cfg(feature = "convert-chrono")]
fn from_chrono_fixed(time: chrono::DateTime<chrono::FixedOffset>) -> 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<time::OffsetDateTime, Error>;
/// 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::Utc> {
chrono::DateTime::<chrono::Utc>::from_utc(
chrono::NaiveDateTime::from_timestamp(self.secs(), self.subsec_nanos()),
chrono::Utc,
)
}
#[cfg(feature = "convert-chrono")]
fn from_chrono_utc(value: chrono::DateTime<chrono::Utc>) -> DateTime {
DateTime::from_secs_and_nanos(value.timestamp(), value.timestamp_subsec_nanos())
}
#[cfg(feature = "convert-chrono")]
fn from_chrono_fixed(value: chrono::DateTime<chrono::FixedOffset>) -> DateTime {
Self::from_chrono_utc(value.with_timezone(&chrono::Utc))
}
#[cfg(feature = "convert-time")]
fn to_time(&self) -> Result<time::OffsetDateTime, Error> {
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(_))));
}
}

View File

@ -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;

View File

@ -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"] }

View File

@ -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

View File

@ -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);
}
});

View File

@ -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);
}
});

View File

@ -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);
}
});

View File

@ -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;
}
}
});

View File

@ -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;
}
}
});

View File

@ -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

View File

@ -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<Instant, DateParseError> {
/// Parses the Smithy epoch seconds date-time format into a `DateTime`.
pub(crate) fn parse(value: &str) -> Result<DateTime, DateTimeParseError> {
let mut parts = value.splitn(2, '.');
let (mut whole, mut decimal) = (0i64, 0u32);
if let Some(whole_str) = parts.next() {
whole = <i64>::from_str(whole_str).map_err(|_| DateParseError::IntParseError)?;
whole = <i64>::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 = <u32>::from_str(decimal_str).map_err(|_| DateParseError::IntParseError)?;
decimal =
<u32>::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<String, DateTimeFormatError> {
fn out_of_range<E: std::fmt::Display>(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<Instant, DateParseError> {
pub(crate) fn parse(s: &str) -> Result<DateTime, DateTimeParseError> {
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<Instant, DateParseError> {
fn parse_imf_fixdate(s: &[u8]) -> Result<DateTime, DateTimeParseError> {
// 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<T>(ascii_slice: &[u8]) -> Result<T, DateParseError>
fn parse_slice<T>(ascii_slice: &[u8]) -> Result<T, DateTimeParseError>
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::<T>()
.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<Instant, DateParseError> {
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<DateTime, DateTimeParseError> {
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<String, DateTimeFormatError> {
use std::fmt::Write;
let (year, month, day, hour, minute, second, nanos) = {
let s = instant.to_chrono_internal();
fn out_of_range<E: std::fmt::Display>(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<String>,
}
impl TestCase {
fn time(&self) -> Instant {
Instant::from_secs_and_nanos(
fn time(&self) -> DateTime {
DateTime::from_secs_and_nanos(
<i64>::from_str(&self.canonical_seconds).unwrap(),
self.canonical_nanos,
)
@ -438,7 +503,7 @@ mod tests {
fn format_test<F>(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<F>(test_cases: &[TestCase], parse: F)
where
F: Fn(&str) -> Result<Instant, DateParseError>,
F: Fn(&str) -> Result<DateTime, DateTimeParseError>,
{
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)
}
}
}

View File

@ -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<Self, ConversionError> {
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<Self, DateTimeParseError> {
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<i64, ConversionError> {
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<String, DateTimeFormatError> {
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<DateTime> for SystemTime {
type Error = ConversionError;
fn try_from(date_time: DateTime) -> Result<Self, Self::Error> {
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<SystemTime> 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(<epoch milli value>);
// 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()
);
}
}

View File

@ -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<Self, DateParseError> {
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<Utc>`.
#[cfg(feature = "chrono-conversions")]
pub fn to_chrono(self) -> DateTime<Utc> {
self.to_chrono_internal()
}
fn to_chrono_internal(self) -> DateTime<Utc> {
DateTime::<Utc>::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<SystemTime> {
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<i64, ConversionError> {
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<DateTime<Utc>> for Instant {
fn from(value: DateTime<Utc>) -> Instant {
Instant::from_secs_and_nanos(value.timestamp(), value.timestamp_subsec_nanos())
}
}
#[cfg(feature = "chrono-conversions")]
impl From<DateTime<chrono::FixedOffset>> for Instant {
fn from(value: DateTime<chrono::FixedOffset>) -> 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(<epoch milli value>);
// 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()
);
}
}
}

View File

@ -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
///