mirror of https://github.com/smithy-lang/smithy-rs
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:
parent
3ae9bcf78f
commit
72eae556ae
53
CHANGELOG.md
53
CHANGELOG.md
|
@ -1,7 +1,60 @@
|
|||
vNext (Month Day, Year)
|
||||
=======================
|
||||
|
||||
**TODO Next Release:**
|
||||
- Update README & aws-sdk-rust CI for MSRV upgrade to 1.54
|
||||
|
||||
**Breaking Changes**
|
||||
|
||||
Several breaking changes around `aws_smithy_types::Instant` were introduced by smithy-rs#849:
|
||||
- `aws_smithy_types::Instant` from was renamed to `DateTime` to avoid confusion with the standard library's monotonically nondecreasing `Instant` type.
|
||||
- `DateParseError` in `aws_smithy_types` has been renamed to `DateTimeParseError` to match the type that's being parsed.
|
||||
- The `chrono-conversions` feature and associated functions have been moved to the `aws-smithy-types-convert` crate.
|
||||
- Calls to `Instant::from_chrono` should be changed to:
|
||||
```rust
|
||||
use aws_smithy_types::DateTime;
|
||||
use aws_smithy_types_convert::date_time::DateTimeExt;
|
||||
|
||||
// For chrono::DateTime<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)
|
||||
===================================
|
||||
|
||||
|
|
|
@ -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)
|
||||
===================================
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -9,8 +9,8 @@
|
|||
//!
|
||||
//! ```rust
|
||||
//! use aws_sigv4::event_stream::{sign_message, SigningParams};
|
||||
//! use chrono::Utc;
|
||||
//! use aws_smithy_eventstream::frame::{Header, HeaderValue, Message};
|
||||
//! use std::time::SystemTime;
|
||||
//!
|
||||
//! // The `last_signature` argument is the previous message's signature, or
|
||||
//! // the signature of the initial HTTP request if a message hasn't been signed yet.
|
||||
|
@ -26,7 +26,7 @@
|
|||
//! .secret_key("example secret key")
|
||||
//! .region("us-east-1")
|
||||
//! .service_name("exampleservice")
|
||||
//! .date_time(Utc::now())
|
||||
//! .time(SystemTime::now())
|
||||
//! .settings(())
|
||||
//! .build()
|
||||
//! .unwrap();
|
||||
|
@ -36,13 +36,13 @@
|
|||
//! sign_message(&message_to_sign, &last_signature, ¶ms).into_parts();
|
||||
//! ```
|
||||
|
||||
use crate::date_fmt::{format_date, format_date_time};
|
||||
use crate::date_time::{format_date, format_date_time, truncate_subsecs};
|
||||
use crate::sign::{calculate_signature, generate_signing_key, sha256_hex_string};
|
||||
use crate::SigningOutput;
|
||||
use aws_smithy_eventstream::frame::{write_headers_to, Header, HeaderValue, Message};
|
||||
use bytes::Bytes;
|
||||
use chrono::{DateTime, SubsecRound, Utc};
|
||||
use std::io::Write;
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// Event stream signing parameters
|
||||
pub type SigningParams<'a> = super::SigningParams<'a, ()>;
|
||||
|
@ -51,13 +51,13 @@ pub type SigningParams<'a> = super::SigningParams<'a, ()>;
|
|||
fn calculate_string_to_sign(
|
||||
message_payload: &[u8],
|
||||
last_signature: &str,
|
||||
date_time: &DateTime<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,
|
||||
¶ms.date_time,
|
||||
params.time,
|
||||
¶ms
|
||||
))
|
||||
.unwrap()
|
||||
|
@ -204,7 +197,7 @@ mod tests {
|
|||
security_token: None,
|
||||
region: "us-east-1",
|
||||
service_name: "testservice",
|
||||
date_time: (UNIX_EPOCH + Duration::new(123_456_789_u64, 1234u32)).into(),
|
||||
time: (UNIX_EPOCH + Duration::new(123_456_789_u64, 1234u32)).into(),
|
||||
settings: (),
|
||||
};
|
||||
|
||||
|
@ -219,9 +212,9 @@ mod tests {
|
|||
}
|
||||
assert_eq!(":date", signed.headers()[1].name().as_str());
|
||||
if let HeaderValue::Timestamp(value) = signed.headers()[1].value() {
|
||||
assert_eq!(123_456_789_i64, value.epoch_seconds());
|
||||
assert_eq!(123_456_789_i64, value.secs());
|
||||
// The subseconds should have been truncated off
|
||||
assert_eq!(0, value.epoch_subsecond_nanos());
|
||||
assert_eq!(0, value.subsec_nanos());
|
||||
} else {
|
||||
panic!("expected timestamp for :date header");
|
||||
}
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
|
||||
use super::query_writer::QueryWriter;
|
||||
use super::{Error, PayloadChecksumKind, SignableBody, SignatureLocation, SigningParams};
|
||||
use crate::date_fmt::{format_date, format_date_time, parse_date, parse_date_time};
|
||||
use crate::date_time::{format_date, format_date_time};
|
||||
use crate::http_request::sign::SignableRequest;
|
||||
use crate::http_request::PercentEncodingMode;
|
||||
use crate::sign::sha256_hex_string;
|
||||
use chrono::{Date, DateTime, Utc};
|
||||
use http::header::{HeaderName, CONTENT_LENGTH, CONTENT_TYPE, HOST, USER_AGENT};
|
||||
use http::{HeaderMap, HeaderValue, Method, Uri};
|
||||
use std::borrow::Cow;
|
||||
|
@ -18,6 +17,7 @@ use std::convert::TryFrom;
|
|||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
use std::str::FromStr;
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub(crate) mod header {
|
||||
pub(crate) const X_AMZ_CONTENT_SHA_256: &str = "x-amz-content-sha256";
|
||||
|
@ -133,7 +133,7 @@ impl<'a> CanonicalRequest<'a> {
|
|||
};
|
||||
let payload_hash = Self::payload_hash(req.body());
|
||||
|
||||
let date_time = format_date_time(¶ms.date_time);
|
||||
let date_time = format_date_time(params.time);
|
||||
let (signed_headers, canonical_headers) =
|
||||
Self::headers(req, params, &payload_hash, &date_time)?;
|
||||
let signed_headers = SignedHeaders::new(signed_headers);
|
||||
|
@ -150,7 +150,7 @@ impl<'a> CanonicalRequest<'a> {
|
|||
credential: format!(
|
||||
"{}/{}/{}/{}/aws4_request",
|
||||
params.access_key,
|
||||
format_date(¶ms.date_time.date()),
|
||||
format_date(params.time),
|
||||
params.region,
|
||||
params.service_name,
|
||||
),
|
||||
|
@ -429,7 +429,7 @@ impl Ord for CanonicalHeaderName {
|
|||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub(super) struct SigningScope<'a> {
|
||||
pub(super) date: Date<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");
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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"))?,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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!();
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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!();
|
||||
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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 : {:#?}",
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 -> { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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);
|
||||
""",
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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 }
|
|
@ -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.
|
|
@ -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(_))));
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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"] }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
///
|
||||
|
|
Loading…
Reference in New Issue