sail-rs 0.6.3

Official Rust SDK for Sail: create and drive sailboxes (sandboxed cloud VMs) with lifecycle, streaming exec, file transfer, and ingress.
Documentation
// rfc3339_micros.rs (de)serializes RFC 3339 timestamps truncated to
// microsecond precision.
//
// The service may emit nanosecond fractions, but not every consumer can
// represent them: Python's datetime holds microseconds, and its fromisoformat
// rejects fractions past six digits before 3.11. These timestamps only
// originate from service responses, so truncating on deserialize is enough;
// the serialize half just delegates so the module works with `serde(with)`.

use serde::{Deserializer, Serializer};
use time::OffsetDateTime;

fn truncate(value: OffsetDateTime) -> OffsetDateTime {
    value
        .replace_nanosecond(value.nanosecond() / 1_000 * 1_000)
        .expect("truncated nanosecond is in range")
}

pub(crate) fn serialize<S>(value: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    time::serde::rfc3339::serialize(value, serializer)
}

pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
where
    D: Deserializer<'de>,
{
    time::serde::rfc3339::deserialize(deserializer).map(truncate)
}

pub(crate) mod option {
    use super::{Deserializer, OffsetDateTime, Serializer};

    // serde hands a `serialize_with` module a reference to the whole field, and
    // `time::serde::rfc3339::option` takes the same, so this stays
    // `&Option<_>`; `Option<&_>` does not compile against either.
    #[allow(clippy::ref_option)]
    pub(crate) fn serialize<S>(
        value: &Option<OffsetDateTime>,
        serializer: S,
    ) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        time::serde::rfc3339::option::serialize(value, serializer)
    }

    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
    where
        D: Deserializer<'de>,
    {
        time::serde::rfc3339::option::deserialize(deserializer)
            .map(|value| value.map(super::truncate))
    }
}

#[cfg(test)]
mod tests {
    use serde::{Deserialize, Serialize};
    use time::OffsetDateTime;

    #[derive(Serialize, Deserialize)]
    struct Row {
        #[serde(with = "crate::rfc3339_micros")]
        at: OffsetDateTime,
        #[serde(with = "crate::rfc3339_micros::option")]
        maybe: Option<OffsetDateTime>,
    }

    #[test]
    fn truncates_sub_microsecond_fractions() {
        let row: Row = serde_json::from_str(
            r#"{"at":"2026-01-02T03:04:05.123456789Z","maybe":"2026-01-02T03:04:05.999999999Z"}"#,
        )
        .unwrap();
        assert_eq!(row.at.nanosecond(), 123_456_000);
        assert_eq!(row.maybe.unwrap().nanosecond(), 999_999_000);
        assert_eq!(
            serde_json::to_string(&row).unwrap(),
            r#"{"at":"2026-01-02T03:04:05.123456Z","maybe":"2026-01-02T03:04:05.999999Z"}"#,
        );
    }

    #[test]
    fn whole_second_timestamps_round_trip_unchanged() {
        let row: Row =
            serde_json::from_str(r#"{"at":"2026-01-02T03:04:05Z","maybe":null}"#).unwrap();
        assert_eq!(row.at.nanosecond(), 0);
        assert_eq!(
            serde_json::to_string(&row).unwrap(),
            r#"{"at":"2026-01-02T03:04:05Z","maybe":null}"#,
        );
    }
}