Skip to content

Commit a719459

Browse files
authored
feat!: type Annotations.lastModified as a string (#956)
1 parent 11f525c commit a719459

5 files changed

Lines changed: 70 additions & 14 deletions

File tree

crates/rmcp/src/model/annotated.rs

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,23 @@ pub struct Annotations {
2020
#[serde(skip_serializing_if = "Option::is_none")]
2121
pub priority: Option<f32>,
2222
#[serde(skip_serializing_if = "Option::is_none", rename = "lastModified")]
23-
pub last_modified: Option<DateTime<Utc>>,
23+
pub last_modified: Option<String>,
2424
}
2525

2626
impl Annotations {
27-
/// Creates a new Annotations instance specifically for resources
28-
/// optional priority, and a timestamp (defaults to now if None)
27+
/// Creates annotations for a resource with priority and an RFC 3339 timestamp.
28+
///
29+
/// # Panics
30+
///
31+
/// Panics if `priority` is not in the inclusive range `0.0..=1.0`.
2932
pub fn for_resource(priority: f32, timestamp: DateTime<Utc>) -> Self {
3033
assert!(
3134
(0.0..=1.0).contains(&priority),
3235
"Priority {priority} must be between 0.0 and 1.0"
3336
);
34-
Annotations {
37+
Self {
3538
priority: Some(priority),
36-
last_modified: Some(timestamp),
39+
last_modified: Some(timestamp.to_rfc3339()),
3740
audience: None,
3841
}
3942
}
@@ -48,12 +51,69 @@ impl Annotations {
4851
self
4952
}
5053

54+
/// Sets `lastModified` from a typed timestamp, serialized as RFC 3339.
5155
pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
52-
self.last_modified = Some(timestamp);
56+
self.last_modified = Some(timestamp.to_rfc3339());
5357
self
5458
}
5559

60+
/// Sets `lastModified` to the current time, serialized as RFC 3339.
5661
pub fn with_timestamp_now(self) -> Self {
5762
self.with_timestamp(Utc::now())
5863
}
5964
}
65+
66+
#[cfg(test)]
67+
mod tests {
68+
use chrono::TimeZone;
69+
use serde_json::json;
70+
71+
use super::*;
72+
73+
fn annotations_of(value: serde_json::Value) -> Annotations {
74+
serde_json::from_value::<Annotations>(value).unwrap()
75+
}
76+
77+
#[test]
78+
fn preserves_date_only_last_modified_verbatim() {
79+
let annotations = annotations_of(json!({ "lastModified": "2025-01-12" }));
80+
assert_eq!(annotations.last_modified.as_deref(), Some("2025-01-12"));
81+
}
82+
83+
#[test]
84+
fn preserves_rfc3339_last_modified_verbatim() {
85+
let value = "2025-01-12T15:00:58Z";
86+
let annotations = annotations_of(json!({ "lastModified": value }));
87+
assert_eq!(annotations.last_modified.as_deref(), Some(value));
88+
}
89+
90+
#[test]
91+
fn missing_last_modified_is_none() {
92+
let annotations = annotations_of(json!({}));
93+
assert_eq!(annotations.last_modified, None);
94+
}
95+
96+
#[test]
97+
fn null_last_modified_is_none() {
98+
let annotations = annotations_of(json!({ "lastModified": null }));
99+
assert_eq!(annotations.last_modified, None);
100+
}
101+
102+
#[test]
103+
fn invalid_last_modified_string_is_preserved() {
104+
let annotations = annotations_of(json!({ "lastModified": "not-a-date" }));
105+
assert_eq!(annotations.last_modified.as_deref(), Some("not-a-date"));
106+
}
107+
108+
#[test]
109+
fn timestamp_round_trips_as_rfc3339_string() {
110+
let timestamp = Utc.with_ymd_and_hms(2025, 1, 12, 15, 0, 58).unwrap();
111+
let annotations = Annotations::default().with_timestamp(timestamp);
112+
113+
let value = serde_json::to_value(&annotations).unwrap();
114+
assert_eq!(value["lastModified"], json!(timestamp.to_rfc3339()));
115+
116+
let round_tripped: Annotations = serde_json::from_value(value).unwrap();
117+
assert_eq!(round_tripped, annotations);
118+
}
119+
}

crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@
5454
"type": [
5555
"string",
5656
"null"
57-
],
58-
"format": "date-time"
57+
]
5958
},
6059
"priority": {
6160
"type": [

crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@
5454
"type": [
5555
"string",
5656
"null"
57-
],
58-
"format": "date-time"
57+
]
5958
},
6059
"priority": {
6160
"type": [

crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@
5454
"type": [
5555
"string",
5656
"null"
57-
],
58-
"format": "date-time"
57+
]
5958
},
6059
"priority": {
6160
"type": [

crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@
5454
"type": [
5555
"string",
5656
"null"
57-
],
58-
"format": "date-time"
57+
]
5958
},
6059
"priority": {
6160
"type": [

0 commit comments

Comments
 (0)