Skip to content

Commit

Permalink
Do not convert dates (#1986)
Browse files Browse the repository at this point in the history
* Do not convert dates

Technically, we weren't because we weren't capturing the return of `to_offset()`, but this cleans up the code and adds some tests that caused us to miss this.

Relates to #1982

* Update RFC 1123 functions to RFC 7231

Actually resolves #1982

* Correct markdown links
  • Loading branch information
heaths authored Jan 10, 2025
1 parent 590556d commit 9639a22
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl Header for IfSourceModifiedSinceCondition {
fn value(&self) -> headers::HeaderValue {
match self {
IfSourceModifiedSinceCondition::Modified(date)
| IfSourceModifiedSinceCondition::Unmodified(date) => date::to_rfc1123(date).into(),
| IfSourceModifiedSinceCondition::Unmodified(date) => date::to_rfc7231(date).into(),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl Policy for AuthorizationPolicy {
);

// x-ms-date and the string used in the signature must be exactly the same, so just generate it here once.
let date_string = date::to_rfc1123(&OffsetDateTime::now_utc()).to_lowercase();
let date_string = date::to_rfc7231(&OffsetDateTime::now_utc()).to_lowercase();

let resource_link: &ResourceLink = ctx
.value()
Expand Down Expand Up @@ -179,7 +179,7 @@ mod tests {
#[tokio::test]
async fn generate_authorization_for_token_credential() {
let time_nonce = date::parse_rfc3339("1900-01-01T01:00:00.000000000+00:00").unwrap();
let date_string = date::to_rfc1123(&time_nonce).to_lowercase();
let date_string = date::to_rfc7231(&time_nonce).to_lowercase();
let cred = Arc::new(TestTokenCredential("test_token".to_string()));
let auth_token = Credential::Token(cred);

Expand Down Expand Up @@ -209,7 +209,7 @@ mod tests {
#[cfg(feature = "key_auth")]
async fn generate_authorization_for_primary_key_0() {
let time_nonce = date::parse_rfc3339("1900-01-01T01:00:00.000000000+00:00").unwrap();
let date_string = date::to_rfc1123(&time_nonce).to_lowercase();
let date_string = date::to_rfc7231(&time_nonce).to_lowercase();

let auth_token = Credential::PrimaryKey(
"8F8xXXOptJxkblM1DBXW7a6NMI5oE8NnwPGYBmwxLCKfejOK7B7yhcCHMGvN3PBrlMLIOeol1Hv9RCdzAZR5sg==".into(),
Expand Down Expand Up @@ -243,7 +243,7 @@ mod tests {
#[cfg(feature = "key_auth")]
async fn generate_authorization_for_primary_key_1() {
let time_nonce = date::parse_rfc3339("2017-04-27T00:51:12.000000000+00:00").unwrap();
let date_string = date::to_rfc1123(&time_nonce).to_lowercase();
let date_string = date::to_rfc7231(&time_nonce).to_lowercase();

let auth_token = Credential::PrimaryKey(
"dsZQi3KtZmCv1ljt3VNWNm7sQUF1y5rJfC6kv5JiwvW0EndXdDku/dkKBp8/ufDToSxL".into(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ mod tests {
#[test]
fn into_signable_string_generates_correct_value() {
let time_nonce = date::parse_rfc3339("1900-01-01T01:00:00.000000000+00:00").unwrap();
let date_string = date::to_rfc1123(&time_nonce).to_lowercase();
let date_string = date::to_rfc7231(&time_nonce).to_lowercase();

let ret = SignatureTarget::new(
azure_core::Method::Get,
Expand Down
3 changes: 1 addition & 2 deletions sdk/typespec/typespec_client_core/src/date/iso8601.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use time::{
iso8601::{Config, EncodedConfig, TimePrecision},
Iso8601,
},
OffsetDateTime, UtcOffset,
OffsetDateTime,
};
use typespec::error::{ErrorKind, ResultExt};

Expand Down Expand Up @@ -49,7 +49,6 @@ pub fn serialize<S>(date: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Er
where
S: Serializer,
{
date.to_offset(UtcOffset::UTC);
let as_str = to_iso8601(date).map_err(serde::ser::Error::custom)?;
serializer.serialize_str(&as_str)
}
Expand Down
121 changes: 75 additions & 46 deletions sdk/typespec/typespec_client_core/src/date/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,31 @@

//! Date and time parsing and formatting functions.
// RFC 3339 vs ISO 8601
// <https://ijmacd.github.io/rfc3339-iso8601/>

use std::time::Duration;
pub use time::{error::ComponentRange, OffsetDateTime};
use time::{
format_description::{well_known::Rfc3339, FormatItem},
macros::format_description,
PrimitiveDateTime, UtcOffset,
PrimitiveDateTime,
};
use typespec::error::{ErrorKind, ResultExt};

// Serde modules.
pub use time::serde::rfc3339;
pub use time::serde::timestamp;

// RFC 3339 vs ISO 8601: <https://ijmacd.github.io/rfc3339-iso8601/>
pub mod iso8601;
pub mod rfc1123;
pub mod rfc7231;

/// RFC 3339: Date and Time on the Internet: Timestamps.
///
/// <https://www.rfc-editor.org/rfc/rfc3339>
///
/// In REST API specifications it is specified as `"format": "date-time"`.
/// In [TypeSpec](https://aka.ms/typespec) properties are specified as `utcDateTime` or `offsetDateTime`.
/// In OpenAPI 2.0 specifications properties are specified as `"format": "date-time"`.
///
/// 1985-04-12T23:20:50.52Z
/// Example string: `1985-04-12T23:20:50.52Z`.
pub fn parse_rfc3339(s: &str) -> crate::Result<OffsetDateTime> {
OffsetDateTime::parse(s, &Rfc3339).with_context(ErrorKind::DataConversion, || {
format!("unable to parse rfc3339 date '{s}")
Expand All @@ -38,59 +38,57 @@ pub fn parse_rfc3339(s: &str) -> crate::Result<OffsetDateTime> {
///
/// <https://www.rfc-editor.org/rfc/rfc3339>
///
/// In REST API specifications it is specified as `"format": "date-time"`.
/// In [TypeSpec](https://aka.ms/typespec) properties are specified as `utcDateTime` or `offsetDateTime`.
/// In OpenAPI 2.0 specifications properties are specified as `"format": "date-time"`.
///
/// 1985-04-12T23:20:50.52Z
/// Example string: `1985-04-12T23:20:50.52Z`.
pub fn to_rfc3339(date: &OffsetDateTime) -> String {
// known format does not panic
date.format(&Rfc3339).unwrap()
}

/// RFC 1123: Requirements for Internet Hosts - Application and Support.
///
/// <https://www.rfc-editor.org/rfc/rfc1123>
/// RFC 7231: Requirements for Internet Hosts - Application and Support.
///
/// In REST API specifications it is specified as `"format": "date-time-rfc1123"`.
/// <https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.1>
///
/// In .NET it is the `rfc1123pattern`.
/// <https://learn.microsoft.com/dotnet/api/system.globalization.datetimeformatinfo.rfc1123pattern>
/// In [TypeSpec](https://aka.ms/typespec) headers are specified as `utcDateTime`.
/// In REST API specifications headers are specified as `"format": "date-time-rfc1123"`.
///
/// This format is also the preferred HTTP date format.
/// <https://httpwg.org/specs/rfc9110.html#http.date>
/// This format is also the preferred HTTP date-based header format.
/// * <https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2>
/// * <https://datatracker.ietf.org/doc/html/rfc7232>
///
/// Sun, 06 Nov 1994 08:49:37 GMT
pub fn parse_rfc1123(s: &str) -> crate::Result<OffsetDateTime> {
Ok(PrimitiveDateTime::parse(s, RFC1123_FORMAT)
/// Example string: `Sun, 06 Nov 1994 08:49:37 GMT`.
pub fn parse_rfc7231(s: &str) -> crate::Result<OffsetDateTime> {
Ok(PrimitiveDateTime::parse(s, RFC7231_FORMAT)
.with_context(ErrorKind::DataConversion, || {
format!("unable to parse rfc1123 date '{s}")
format!("unable to parse rfc7231 date '{s}")
})?
.assume_utc())
}

const RFC1123_FORMAT: &[FormatItem] = format_description!(
const RFC7231_FORMAT: &[FormatItem] = format_description!(
"[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT"
);

/// RFC 1123: Requirements for Internet Hosts - Application and Support.
/// RFC 7231: Requirements for Internet Hosts - Application and Support.
///
/// <https://www.rfc-editor.org/rfc/rfc1123>
/// <https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.1>
///
/// In REST API specifications it is specified as `"format": "date-time-rfc1123"`.
/// In [TypeSpec](https://aka.ms/typespec) headers are specified as `utcDateTime`.
/// In REST API specifications headers are specified as `"format": "date-time-rfc1123"`.
///
/// In .NET it is the `rfc1123pattern`.
/// <https://learn.microsoft.com/dotnet/api/system.globalization.datetimeformatinfo.rfc1123pattern>
/// This format is also the preferred HTTP date-based header format.
/// * <https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2>
/// * <https://datatracker.ietf.org/doc/html/rfc7232>
///
/// This format is also the preferred HTTP date format.
/// <https://httpwg.org/specs/rfc9110.html#http.date>
///
/// Sun, 06 Nov 1994 08:49:37 GMT
pub fn to_rfc1123(date: &OffsetDateTime) -> String {
date.to_offset(UtcOffset::UTC);
/// Example string: `Sun, 06 Nov 1994 08:49:37 GMT`.
pub fn to_rfc7231(date: &OffsetDateTime) -> String {
// known format does not panic
date.format(&RFC1123_FORMAT).unwrap()
date.format(&RFC7231_FORMAT).unwrap()
}

/// Similar to RFC 1123, but includes milliseconds.
/// Similar to RFC 7231, but includes milliseconds.
///
/// <https://learn.microsoft.com/rest/api/cosmos-db/patch-a-document>
///
Expand All @@ -114,7 +112,6 @@ const LAST_STATE_CHANGE_FORMAT: &[FormatItem] = format_description!(
///
/// x-ms-last-state-change-utc: Fri, 25 Mar 2016 21:27:20.035 GMT
pub fn to_last_state_change(date: &OffsetDateTime) -> String {
date.to_offset(UtcOffset::UTC);
// known format does not panic
date.format(LAST_STATE_CHANGE_FORMAT).unwrap()
}
Expand Down Expand Up @@ -157,15 +154,24 @@ mod tests {
}

#[test]
fn test_roundtrip_rfc3339() -> crate::Result<()> {
fn roundtrip_rfc3339() -> crate::Result<()> {
let s = "2019-10-12T07:20:50.52Z";
let dt = parse_rfc3339(s)?;
assert_eq!(s, to_rfc3339(&dt));
Ok(())
}

#[test]
fn test_device_update_dates() -> crate::Result<()> {
fn roundtrip_rfc3339_offset() -> crate::Result<()> {
let s = "2019-10-12T00:20:50.52-08:00";
let dt = parse_rfc3339(s)?;
assert!(!dt.offset().is_utc());
assert_eq!(s, to_rfc3339(&dt));
Ok(())
}

#[test]
fn device_update_dates() -> crate::Result<()> {
let created = parse_rfc3339("1999-09-10T21:59:22Z")?;
let last_action = parse_rfc3339("1999-09-10T03:05:07.3845533+01:00")?;
assert_eq!(created, datetime!(1999-09-10 21:59:22 UTC));
Expand All @@ -174,16 +180,39 @@ mod tests {
}

#[test]
fn test_to_rfc1123() -> crate::Result<()> {
fn test_to_rfc7231() -> crate::Result<()> {
let dt = datetime!(1994-11-06 08:49:37 UTC);
assert_eq!("Sun, 06 Nov 1994 08:49:37 GMT", to_rfc1123(&dt));
assert_eq!("Sun, 06 Nov 1994 08:49:37 GMT", to_rfc7231(&dt));
Ok(())
}

#[test]
fn test_parse_rfc1123() -> crate::Result<()> {
fn test_parse_rfc7231() -> crate::Result<()> {
let dt = datetime!(1994-11-06 08:49:37 UTC);
assert_eq!(parse_rfc1123("Sun, 06 Nov 1994 08:49:37 GMT")?, dt);
assert_eq!(parse_rfc7231("Sun, 06 Nov 1994 08:49:37 GMT")?, dt);
Ok(())
}

#[test]
fn parse_rfc7231_offset() {
assert!(parse_rfc7231("Sun, 06 Nov 1994 00:49:37 PST").is_err());
}

#[test]
fn roundtrip_rfc7231() -> crate::Result<()> {
let s = "Sat, 12 Oct 2019 07:20:50 GMT";
let dt = parse_rfc7231(s)?;
assert_eq!(s, to_rfc7231(&dt));
Ok(())
}

#[test]
#[ignore = "https://github.com/Azure/azure-sdk-for-rust/issues/1982"]
fn roundtrip_rfc7231_offset() -> crate::Result<()> {
let s = "Sat, 12 Oct 2019 07:20:50 PST";
let dt = parse_rfc7231(s)?;
assert!(!dt.offset().is_utc());
assert_eq!(s, to_rfc7231(&dt));
Ok(())
}

Expand All @@ -197,17 +226,17 @@ mod tests {
}

#[test]
fn test_list_blob_creation_time() -> crate::Result<()> {
fn list_blob_creation_time() -> crate::Result<()> {
let creation_time = "Thu, 01 Jul 2021 10:45:02 GMT";
assert_eq!(
datetime!(2021-07-01 10:45:02 UTC),
parse_rfc1123(creation_time)?
parse_rfc7231(creation_time)?
);
Ok(())
}

#[test]
fn test_serde_rfc3339_none_optional() -> crate::Result<()> {
fn serde_rfc3339_none_optional() -> crate::Result<()> {
let json_state = r#"{
"created_time": "2021-07-01T10:45:02Z"
}"#;
Expand All @@ -221,7 +250,7 @@ mod tests {
}

#[test]
fn test_serde_rfc3339_some_optional() -> crate::Result<()> {
fn serde_rfc3339_some_optional() -> crate::Result<()> {
let json_state = r#"{
"created_time": "2021-07-01T10:45:02Z",
"deleted_time": "2022-03-28T11:05:31Z"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,50 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

//! RFC 1123 date and time parsing and formatting functions.
use crate::date::{parse_rfc1123, to_rfc1123};
//! RFC 7231 date and time parsing and formatting functions.
use crate::date::{parse_rfc7231, to_rfc7231};
use serde::{de, Deserialize, Deserializer, Serializer};
use time::OffsetDateTime;

/// Deserialize an RFC 1123 date and time string into an [`OffsetDateTime`].
/// Deserialize an RFC 7231 date and time string into an [`OffsetDateTime`].
pub fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
parse_rfc1123(&s).map_err(de::Error::custom)
parse_rfc7231(&s).map_err(de::Error::custom)
}

/// Serialize an [`OffsetDateTime`] to an RFC 1123 date and time string.
/// Serialize an [`OffsetDateTime`] to an RFC 7231 date and time string.
pub fn serialize<S>(date: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&to_rfc1123(date))
serializer.serialize_str(&to_rfc7231(date))
}

pub mod option {
use crate::date::{parse_rfc1123, to_rfc1123};
use crate::date::{parse_rfc7231, to_rfc7231};
use serde::{Deserialize, Deserializer, Serializer};
use time::OffsetDateTime;

/// Deserialize an RFC 1123 date and time string into an optional [`OffsetDateTime`].
/// Deserialize an RFC 7231 date and time string into an optional [`OffsetDateTime`].
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
s.map(|s| parse_rfc1123(&s).map_err(serde::de::Error::custom))
s.map(|s| parse_rfc7231(&s).map_err(serde::de::Error::custom))
.transpose()
}

/// Serialize an optional [`OffsetDateTime`] to an RFC 1123 date and time string.
/// Serialize an optional [`OffsetDateTime`] to an RFC 7231 date and time string.
pub fn serialize<S>(date: &Option<OffsetDateTime>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(date) = date {
serializer.serialize_str(&to_rfc1123(date))
serializer.serialize_str(&to_rfc7231(date))
} else {
serializer.serialize_none()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ use std::{sync::Arc, time::Duration};
use tracing::{debug, trace};
use typespec::error::{Error, ErrorKind, ResultExt};

/// Attempts to parse the supplied string as an HTTP date, of the form defined by RFC 1123 (e.g. `Fri, 01 Jan 2021 00:00:00 GMT`).
/// Attempts to parse the supplied string as an HTTP date, of the form defined by RFC 7231 (e.g. `Fri, 01 Jan 2021 00:00:00 GMT`).
/// Returns `None` if the string is not a valid HTTP date.
fn try_parse_retry_after_http_date(http_date: &str) -> Option<OffsetDateTime> {
crate::date::parse_rfc1123(http_date).ok()
crate::date::parse_rfc7231(http_date).ok()
}

/// A function that returns an `OffsetDateTime`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl Header for IfModifiedSince {
}

fn value(&self) -> headers::HeaderValue {
date::to_rfc1123(&self.0).into()
date::to_rfc7231(&self.0).into()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl Header for IfModifiedSinceCondition {
fn value(&self) -> headers::HeaderValue {
match self {
IfModifiedSinceCondition::Modified(date)
| IfModifiedSinceCondition::Unmodified(date) => date::to_rfc1123(date),
| IfModifiedSinceCondition::Unmodified(date) => date::to_rfc7231(date),
}
.into()
}
Expand Down
Loading

0 comments on commit 9639a22

Please sign in to comment.