diff --git a/crates/cloud-sdk/src/applications/models.rs b/crates/cloud-sdk/src/applications/models.rs index 29d5f53..79eb479 100644 --- a/crates/cloud-sdk/src/applications/models.rs +++ b/crates/cloud-sdk/src/applications/models.rs @@ -2,13 +2,61 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use futures::Stream; use reqwest::header::HeaderValue; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json; -use std::{collections::HashMap, pin::Pin}; +use std::{collections::HashMap, fmt::Display, pin::Pin}; use uuid::Uuid; use crate::error::SdkError; +/// A custom DateTime type that handles RFC3339 timestamps with missing 'Z' timezone indicator. +/// When deserializing, if the timestamp doesn't end with 'Z', it's automatically appended. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +#[serde(transparent)] +pub struct Rfc3339DateTime(DateTime); + +impl Rfc3339DateTime { + pub fn now() -> Self { + Self(Utc::now()) + } +} + +impl From> for Rfc3339DateTime { + fn from(value: DateTime) -> Self { + Self(value) + } +} + +impl Display for Rfc3339DateTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.to_rfc3339()) + } +} + +impl<'de> Deserialize<'de> for Rfc3339DateTime { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mut s = String::deserialize(deserializer)?; + if !s.ends_with("Z") && !s.ends_with("+00:00") { + s.push('Z'); + } + + DateTime::parse_from_rfc3339(&s) + .map(|dt| Rfc3339DateTime(dt.with_timezone(&Utc))) + .map_err(serde::de::Error::custom) + } +} + +impl std::ops::Deref for Rfc3339DateTime { + type Target = DateTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, Builder)] pub struct ApplicationManifest { #[builder(setter(into))] @@ -644,7 +692,7 @@ pub struct RequestProgressUpdated { #[serde(default)] pub attributes: Option, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for RequestProgressUpdated { @@ -665,11 +713,11 @@ impl RequestEventMetadata for RequestProgressUpdated { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -682,7 +730,7 @@ pub struct RequestFinishedEvent { #[serde(default)] pub outcome: RequestOutcome, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for RequestFinishedEvent { @@ -703,11 +751,11 @@ impl RequestEventMetadata for RequestFinishedEvent { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -718,7 +766,7 @@ pub struct RequestStartedEvent { pub application_version: String, pub request_id: String, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for RequestStartedEvent { @@ -739,11 +787,11 @@ impl RequestEventMetadata for RequestStartedEvent { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -756,7 +804,7 @@ pub struct FunctionRunCreated { pub function_name: String, pub function_run_id: String, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for FunctionRunCreated { @@ -777,11 +825,11 @@ impl RequestEventMetadata for FunctionRunCreated { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -796,7 +844,7 @@ pub struct FunctionRunAssigned { pub allocation_id: String, pub executor_id: String, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for FunctionRunAssigned { @@ -817,11 +865,11 @@ impl RequestEventMetadata for FunctionRunAssigned { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -844,7 +892,7 @@ pub struct FunctionRunCompleted { pub allocation_id: String, pub outcome: FunctionRunOutcomeSummary, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for FunctionRunCompleted { @@ -865,11 +913,11 @@ impl RequestEventMetadata for FunctionRunCompleted { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -882,7 +930,7 @@ pub struct FunctionRunMatchedCache { pub function_name: String, pub function_run_id: String, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for FunctionRunMatchedCache { @@ -903,11 +951,11 @@ impl RequestEventMetadata for FunctionRunMatchedCache { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -1227,3 +1275,102 @@ pub struct ProgressUpdatesJson { pub updates: Vec, pub next_token: Option, } + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Datelike; + use serde_json::json; + + #[test] + fn test_rfc3339_datetime_with_z() { + let json = json!("2024-01-15T10:30:45Z"); + let result: Result = serde_json::from_value(json); + assert!(result.is_ok()); + } + + #[test] + fn test_rfc3339_datetime_without_z() { + let json = json!("2024-01-15T10:30:45"); + let result: Result = serde_json::from_value(json); + assert!(result.is_ok()); + let dt = result.unwrap(); + // Verify it was parsed correctly as UTC + assert_eq!(dt.0.year(), 2024); + assert_eq!(dt.0.month(), 1); + assert_eq!(dt.0.day(), 15); + } + + #[test] + fn test_rfc3339_datetime_with_timezone_offset() { + let json = json!("2024-01-15T10:30:45+00:00"); + let result: Result = serde_json::from_value(json); + assert!(result.is_ok()); + } + + #[test] + fn test_request_started_event_deserialization() { + let json = json!({ + "namespace": "test", + "application_name": "app", + "application_version": "1.0", + "request_id": "req-123", + "created_at": "2024-01-15T10:30:45" + }); + let result: Result = serde_json::from_value(json); + assert!(result.is_ok()); + let event = result.unwrap(); + assert!(event.created_at.is_some()); + } + + #[test] + fn test_rfc3339_datetime_serialization() { + // Test that serializing Rfc3339DateTime produces a plain string, not a nested struct + let now = chrono::Utc::now(); + let rfc_dt = Rfc3339DateTime(now); + let serialized = serde_json::to_value(&rfc_dt).unwrap(); + + // Should be a string, not an object + assert!( + serialized.is_string(), + "Expected serialized DateTime to be a string, got: {:?}", + serialized + ); + + // Should contain 'Z' at the end + let date_str = serialized.as_str().unwrap(); + assert!( + date_str.ends_with('Z'), + "Expected 'Z' at end of serialized DateTime" + ); + } + + #[test] + fn test_request_started_event_serialization() { + // Test that serializing an event doesn't nest the created_at field + let event = RequestStartedEvent { + namespace: "test".to_string(), + application_name: "app".to_string(), + application_version: "1.0".to_string(), + request_id: "req-123".to_string(), + created_at: Some(Rfc3339DateTime(Utc::now())), + }; + + let serialized = serde_json::to_value(&event).unwrap(); + let obj = serialized.as_object().unwrap(); + + // created_at should be a string directly, not an object + let created_at = &obj["created_at"]; + assert!( + created_at.is_string(), + "Expected created_at to be a string, got: {:?}", + created_at + ); + + let date_str = created_at.as_str().unwrap(); + assert!( + date_str.ends_with('Z'), + "Expected 'Z' at end of created_at value" + ); + } +} diff --git a/crates/cloud-sdk/src/images/mod.rs b/crates/cloud-sdk/src/images/mod.rs index 4aedb75..a53354a 100644 --- a/crates/cloud-sdk/src/images/mod.rs +++ b/crates/cloud-sdk/src/images/mod.rs @@ -24,7 +24,7 @@ //! .application_name("my-app") //! .application_version("1.0.0") //! .function_name("main") -//! .sdk_version("0.3.12") +//! .sdk_version("0.2") //! .build().unwrap(); //! //! images_client.build_image(build_request); @@ -113,7 +113,7 @@ impl ImagesClient { /// .application_name("my-app") /// .application_version("1.0.0") /// .function_name("main") - /// .sdk_version("0.2.75") + /// .sdk_version("0.2") /// .build()?; /// /// images_client.build_image(request).await?; diff --git a/crates/cloud-sdk/src/images/models.rs b/crates/cloud-sdk/src/images/models.rs index 5de26ea..cc054aa 100644 --- a/crates/cloud-sdk/src/images/models.rs +++ b/crates/cloud-sdk/src/images/models.rs @@ -321,7 +321,16 @@ impl Image { lines.push(render_build_operation(op)); } - lines.push(format!("RUN pip install tensorlake=={}", sdk_version)); + if sdk_version.starts_with("~=") + || sdk_version.starts_with(">=") + || sdk_version.starts_with("<=") + || sdk_version.starts_with("!=") + || sdk_version.starts_with("==") + { + lines.push(format!("RUN pip install tensorlake{}", sdk_version)); + } else { + lines.push(format!("RUN pip install tensorlake=={}", sdk_version)); + } lines.join("\n") } diff --git a/crates/cloud-sdk/tests/common.rs b/crates/cloud-sdk/tests/common.rs index 4d30b86..e5c7a98 100644 --- a/crates/cloud-sdk/tests/common.rs +++ b/crates/cloud-sdk/tests/common.rs @@ -39,7 +39,7 @@ pub async fn build_test_image( .application_name(application_name) .application_version(application_version) .function_name(func_name) - .sdk_version("0.2.75") + .sdk_version("~=0.2") .build() .unwrap();