Skip to content

Commit df19f3d

Browse files
committed
feat!: type Annotations.lastModified as a string
1 parent 95490fa commit df19f3d

5 files changed

Lines changed: 76 additions & 14 deletions

File tree

‎crates/rmcp/src/model/annotated.rs‎

Lines changed: 72 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,75 @@ 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` from a raw schema string.
61+
pub fn with_last_modified(mut self, value: impl Into<String>) -> Self {
62+
self.last_modified = Some(value.into());
63+
self
64+
}
65+
66+
/// Sets `lastModified` to the current time, serialized as RFC 3339.
5667
pub fn with_timestamp_now(self) -> Self {
5768
self.with_timestamp(Utc::now())
5869
}
5970
}
71+
72+
#[cfg(test)]
73+
mod tests {
74+
use chrono::TimeZone;
75+
use serde_json::json;
76+
77+
use super::*;
78+
79+
fn annotations_of(value: serde_json::Value) -> Annotations {
80+
serde_json::from_value::<Annotations>(value).unwrap()
81+
}
82+
83+
#[test]
84+
fn preserves_date_only_last_modified_verbatim() {
85+
let annotations = annotations_of(json!({ "lastModified": "2025-01-12" }));
86+
assert_eq!(annotations.last_modified.as_deref(), Some("2025-01-12"));
87+
}
88+
89+
#[test]
90+
fn preserves_rfc3339_last_modified_verbatim() {
91+
let value = "2025-01-12T15:00:58Z";
92+
let annotations = annotations_of(json!({ "lastModified": value }));
93+
assert_eq!(annotations.last_modified.as_deref(), Some(value));
94+
}
95+
96+
#[test]
97+
fn missing_last_modified_is_none() {
98+
let annotations = annotations_of(json!({}));
99+
assert_eq!(annotations.last_modified, None);
100+
}
101+
102+
#[test]
103+
fn null_last_modified_is_none() {
104+
let annotations = annotations_of(json!({ "lastModified": null }));
105+
assert_eq!(annotations.last_modified, None);
106+
}
107+
108+
#[test]
109+
fn invalid_last_modified_string_is_preserved() {
110+
let annotations = Annotations::default().with_last_modified("not-a-date");
111+
assert_eq!(annotations.last_modified.as_deref(), Some("not-a-date"));
112+
}
113+
114+
#[test]
115+
fn timestamp_round_trips_as_rfc3339_string() {
116+
let timestamp = Utc.with_ymd_and_hms(2025, 1, 12, 15, 0, 58).unwrap();
117+
let annotations = Annotations::default().with_timestamp(timestamp);
118+
119+
let value = serde_json::to_value(&annotations).unwrap();
120+
assert_eq!(value["lastModified"], json!(timestamp.to_rfc3339()));
121+
122+
let round_tripped: Annotations = serde_json::from_value(value).unwrap();
123+
assert_eq!(round_tripped, annotations);
124+
}
125+
}

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