cloud-sdk 0.60.0

no_std-first provider-neutral cloud SDK foundations.
Documentation
use super::*;
use crate::action_polling::ActionUpdate;
use core::fmt::{self, Write};

fn id(value: &str) -> AsyncResourceId<'_> {
    AsyncResourceId::new(value).unwrap_or_else(|_| unreachable!())
}

fn text(value: &str) -> AsyncResourceText<'_> {
    AsyncResourceText::new(value).unwrap_or_else(|_| unreachable!())
}

fn link(value: &str) -> AsyncResourceLink<'_> {
    AsyncResourceLink::new(value).unwrap_or_else(|_| unreachable!())
}

fn timestamp(value: &str) -> AsyncResourceTimestamp<'_> {
    AsyncResourceTimestamp::parse(value).unwrap_or_else(|_| unreachable!())
}

fn parts<'a>(
    status: AsyncResourceStatus,
    started_at: Option<AsyncResourceTimestamp<'a>>,
    finished_at: Option<AsyncResourceTimestamp<'a>>,
    progress: &'a [AsyncProgressStep<'a>],
    errors: &'a [AsyncTaskError<'a>],
) -> AsyncTaskParts<'a> {
    AsyncTaskParts {
        id: id("018f4a55-8970-7db9-b6b5-4afed75820af"),
        kind: text("contact-mean-validation"),
        status,
        link: Some(link("/v2/notification/contactMean/example")),
        message: Some(text("provider-controlled task message")),
        created_at: timestamp("2026-08-07T08:00:00Z"),
        updated_at: timestamp("2026-08-07T08:00:02.500000000Z"),
        started_at,
        finished_at,
        progress,
        errors,
    }
}

#[test]
fn sensitive_values_enforce_exact_bounds_and_redact_debug() {
    let max_id = "x".repeat(MAX_ASYNC_ID_BYTES);
    assert!(AsyncResourceId::new(&max_id).is_ok());
    assert_eq!(
        AsyncResourceId::new(&(max_id + "x")),
        Err(AsyncResourceValidationError::IdTooLong)
    );
    assert_eq!(
        AsyncResourceId::new("id with space"),
        Err(AsyncResourceValidationError::InvalidId)
    );
    let max_text = "x".repeat(MAX_ASYNC_TEXT_BYTES);
    assert!(AsyncResourceText::new(&max_text).is_ok());
    assert_eq!(
        AsyncResourceText::new("message\nsecret"),
        Err(AsyncResourceValidationError::TextControl)
    );
    assert_eq!(
        AsyncResourceLink::new("/resource token"),
        Err(AsyncResourceValidationError::InvalidLink)
    );
    let mut output = DebugBuffer::new();
    write!(output, "{:?}", text("sentinel-secret")).unwrap_or_else(|_| unreachable!());
    assert_eq!(output.as_str(), "AsyncResourceText([redacted])");
}

#[test]
fn utc_timestamps_validate_calendar_fraction_and_order() {
    for value in [
        "2024-02-29T23:59:59Z",
        "2026-08-07T08:00:00.1Z",
        "2026-08-07T08:00:00.000000001Z",
    ] {
        assert!(AsyncResourceTimestamp::parse(value).is_ok(), "{value}");
    }
    for value in [
        "2023-02-29T00:00:00Z",
        "2026-13-01T00:00:00Z",
        "2026-01-01t00:00:00Z",
        "2026-01-01T24:00:00Z",
        "2026-01-01T00:00:60Z",
        "2026-01-01T00:00:00.Z",
        "2026-01-01T00:00:00+00:00",
    ] {
        assert_eq!(
            AsyncResourceTimestamp::parse(value),
            Err(AsyncResourceValidationError::InvalidTimestamp),
            "{value}"
        );
    }
    assert_eq!(
        timestamp("2026-01-01T00:00:00.9Z").compare(timestamp("2026-01-01T00:00:01Z")),
        core::cmp::Ordering::Less
    );
    assert_eq!(
        timestamp("2026-01-01T00:00:00.1Z"),
        timestamp("2026-01-01T00:00:00.100Z")
    );
    assert_eq!(
        timestamp("2026-01-01T00:00:00Z"),
        timestamp("2026-01-01T00:00:00.0Z")
    );
}

#[test]
fn task_lifecycle_is_bounded_coherent_and_pollable() {
    let progress = [AsyncProgressStep::new(
        text("validation"),
        AsyncResourceStatus::Running,
    )];
    let running = AsyncTask::new(parts(
        AsyncResourceStatus::Running,
        Some(timestamp("2026-08-07T08:00:01Z")),
        None,
        &progress,
        &[],
    ))
    .unwrap_or_else(|_| unreachable!());
    assert_eq!(
        running.poll_disposition(),
        AsyncPollDisposition::Update(ActionUpdate::Running)
    );
    assert_eq!(running.progress(), &progress);

    let errors = [AsyncTaskError::new(text("sentinel-provider-error"))];
    let failed = AsyncTask::new(parts(
        AsyncResourceStatus::Failed,
        Some(timestamp("2026-08-07T08:00:01Z")),
        Some(timestamp("2026-08-07T08:00:02Z")),
        &progress,
        &errors,
    ))
    .unwrap_or_else(|_| unreachable!());
    assert_eq!(
        failed.poll_disposition(),
        AsyncPollDisposition::Update(ActionUpdate::Failed(errors.as_slice()))
    );

    let mut waiting_parts = parts(
        AsyncResourceStatus::WaitingForInput,
        Some(timestamp("2026-08-07T08:00:01Z")),
        None,
        &progress,
        &[],
    );
    waiting_parts.link = None;
    waiting_parts.message = None;
    let waiting = AsyncTask::new(waiting_parts).unwrap_or_else(|_| unreachable!());
    assert_eq!(
        waiting.poll_disposition(),
        AsyncPollDisposition::WaitingForInput
    );
    assert_eq!(waiting.link(), None);
    assert_eq!(waiting.message(), None);

    assert!(matches!(
        AsyncTask::new(parts(AsyncResourceStatus::Succeeded, None, None, &[], &[],)),
        Err(AsyncResourceValidationError::TerminalTimeMismatch)
    ));
    let contradictory = AsyncTask::new(parts(
        AsyncResourceStatus::Succeeded,
        Some(timestamp("2026-08-07T08:00:01Z")),
        Some(timestamp("2026-08-07T08:00:02Z")),
        &[],
        &errors,
    ))
    .unwrap_or_else(|_| unreachable!());
    assert_eq!(
        contradictory.poll_disposition(),
        AsyncPollDisposition::ContradictorySuccess(errors.as_slice())
    );
    assert!(matches!(
        AsyncTask::new(parts(
            AsyncResourceStatus::Running,
            Some(timestamp("2026-08-07T08:00:02.600Z")),
            None,
            &[],
            &[],
        )),
        Err(AsyncResourceValidationError::TimestampOrder)
    ));
}

#[test]
fn generic_events_are_bounded_without_claiming_an_endpoint() {
    let event = AsyncEvent::new(AsyncEventParts {
        id: id("event-1"),
        kind: text("resource.changed"),
        observed_at: timestamp("2026-08-07T08:00:00Z"),
        link: Some(link("/fixture/resource")),
        message: Some(text("sentinel-event-message")),
    });
    let events = [event];
    let batch = AsyncEventBatch::new(&events).unwrap_or_else(|_| unreachable!());
    assert_eq!(batch.events().len(), 1);
    let mut output = DebugBuffer::new();
    write!(output, "{batch:?}").unwrap_or_else(|_| unreachable!());
    assert_eq!(output.as_str(), "AsyncEventBatch { events: 1 }");
}

#[test]
fn collection_bounds_accept_exact_limits_and_reject_plus_one() {
    let progress = AsyncProgressStep::new(text("phase"), AsyncResourceStatus::Running);
    let progress_limit = [progress; MAX_ASYNC_PROGRESS_STEPS];
    assert!(
        AsyncTask::new(parts(
            AsyncResourceStatus::Running,
            None,
            None,
            &progress_limit,
            &[],
        ))
        .is_ok()
    );
    let progress_over = [progress; MAX_ASYNC_PROGRESS_STEPS + 1];
    assert!(matches!(
        AsyncTask::new(parts(
            AsyncResourceStatus::Running,
            None,
            None,
            &progress_over,
            &[],
        )),
        Err(AsyncResourceValidationError::TooManyProgressSteps)
    ));

    let task_error = AsyncTaskError::new(text("provider error"));
    let errors_over = [task_error; MAX_ASYNC_ERRORS + 1];
    assert!(matches!(
        AsyncTask::new(parts(
            AsyncResourceStatus::Running,
            None,
            None,
            &[],
            &errors_over,
        )),
        Err(AsyncResourceValidationError::TooManyErrors)
    ));

    let event = AsyncEvent::new(AsyncEventParts {
        id: id("event-1"),
        kind: text("fixture"),
        observed_at: timestamp("2026-08-07T08:00:00Z"),
        link: None,
        message: None,
    });
    let event_limit = [event; MAX_ASYNC_EVENTS];
    assert!(AsyncEventBatch::new(&event_limit).is_ok());
    let event_over = [event; MAX_ASYNC_EVENTS + 1];
    assert!(matches!(
        AsyncEventBatch::new(&event_over),
        Err(AsyncResourceValidationError::TooManyEvents)
    ));
}

struct DebugBuffer {
    bytes: [u8; 128],
    len: usize,
}

impl DebugBuffer {
    const fn new() -> Self {
        Self {
            bytes: [0; 128],
            len: 0,
        }
    }

    fn as_str(&self) -> &str {
        core::str::from_utf8(self.bytes.get(..self.len).unwrap_or_default()).unwrap_or_default()
    }
}

impl Write for DebugBuffer {
    fn write_str(&mut self, value: &str) -> fmt::Result {
        let end = self.len.checked_add(value.len()).ok_or(fmt::Error)?;
        let target = self.bytes.get_mut(self.len..end).ok_or(fmt::Error)?;
        target.copy_from_slice(value.as_bytes());
        self.len = end;
        Ok(())
    }
}