diff --git a/crates/rmcp/src/model/annotated.rs b/crates/rmcp/src/model/annotated.rs index 06800d1f..ac3dbd3e 100644 --- a/crates/rmcp/src/model/annotated.rs +++ b/crates/rmcp/src/model/annotated.rs @@ -20,20 +20,23 @@ pub struct Annotations { #[serde(skip_serializing_if = "Option::is_none")] pub priority: Option, #[serde(skip_serializing_if = "Option::is_none", rename = "lastModified")] - pub last_modified: Option>, + pub last_modified: Option, } impl Annotations { - /// Creates a new Annotations instance specifically for resources - /// optional priority, and a timestamp (defaults to now if None) + /// Creates annotations for a resource with priority and an RFC 3339 timestamp. + /// + /// # Panics + /// + /// Panics if `priority` is not in the inclusive range `0.0..=1.0`. pub fn for_resource(priority: f32, timestamp: DateTime) -> Self { assert!( (0.0..=1.0).contains(&priority), "Priority {priority} must be between 0.0 and 1.0" ); - Annotations { + Self { priority: Some(priority), - last_modified: Some(timestamp), + last_modified: Some(timestamp.to_rfc3339()), audience: None, } } @@ -48,12 +51,69 @@ impl Annotations { self } + /// Sets `lastModified` from a typed timestamp, serialized as RFC 3339. pub fn with_timestamp(mut self, timestamp: DateTime) -> Self { - self.last_modified = Some(timestamp); + self.last_modified = Some(timestamp.to_rfc3339()); self } + /// Sets `lastModified` to the current time, serialized as RFC 3339. pub fn with_timestamp_now(self) -> Self { self.with_timestamp(Utc::now()) } } + +#[cfg(test)] +mod tests { + use chrono::TimeZone; + use serde_json::json; + + use super::*; + + fn annotations_of(value: serde_json::Value) -> Annotations { + serde_json::from_value::(value).unwrap() + } + + #[test] + fn preserves_date_only_last_modified_verbatim() { + let annotations = annotations_of(json!({ "lastModified": "2025-01-12" })); + assert_eq!(annotations.last_modified.as_deref(), Some("2025-01-12")); + } + + #[test] + fn preserves_rfc3339_last_modified_verbatim() { + let value = "2025-01-12T15:00:58Z"; + let annotations = annotations_of(json!({ "lastModified": value })); + assert_eq!(annotations.last_modified.as_deref(), Some(value)); + } + + #[test] + fn missing_last_modified_is_none() { + let annotations = annotations_of(json!({})); + assert_eq!(annotations.last_modified, None); + } + + #[test] + fn null_last_modified_is_none() { + let annotations = annotations_of(json!({ "lastModified": null })); + assert_eq!(annotations.last_modified, None); + } + + #[test] + fn invalid_last_modified_string_is_preserved() { + let annotations = annotations_of(json!({ "lastModified": "not-a-date" })); + assert_eq!(annotations.last_modified.as_deref(), Some("not-a-date")); + } + + #[test] + fn timestamp_round_trips_as_rfc3339_string() { + let timestamp = Utc.with_ymd_and_hms(2025, 1, 12, 15, 0, 58).unwrap(); + let annotations = Annotations::default().with_timestamp(timestamp); + + let value = serde_json::to_value(&annotations).unwrap(); + assert_eq!(value["lastModified"], json!(timestamp.to_rfc3339())); + + let round_tripped: Annotations = serde_json::from_value(value).unwrap(); + assert_eq!(round_tripped, annotations); + } +} diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json index 3bfdb7c4..c88cee46 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json @@ -54,8 +54,7 @@ "type": [ "string", "null" - ], - "format": "date-time" + ] }, "priority": { "type": [ diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json index 3bfdb7c4..c88cee46 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json @@ -54,8 +54,7 @@ "type": [ "string", "null" - ], - "format": "date-time" + ] }, "priority": { "type": [ diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json index fcf821f5..9b37ed21 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json @@ -54,8 +54,7 @@ "type": [ "string", "null" - ], - "format": "date-time" + ] }, "priority": { "type": [ diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json index fcf821f5..9b37ed21 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json @@ -54,8 +54,7 @@ "type": [ "string", "null" - ], - "format": "date-time" + ] }, "priority": { "type": [