solti-api 0.0.4

HTTP/JSON and gRPC API layer for the Solti task supervisor SDK.
Documentation
//! # Run Conversion
//!
//! Converts domain run history into protobuf response values.
//! Embedded run history has no public wire representation.

use solti_model::TaskRun;

use super::time::system_time_to_ms;
use crate::error::ApiError;
use crate::proto_api;
use crate::visibility::run_is_visible;

impl TryFrom<TaskRun> for proto_api::TaskRunInfo {
    type Error = ApiError;

    fn try_from(run: TaskRun) -> Result<Self, Self::Error> {
        if !run_is_visible(&run) {
            return Err(ApiError::Internal(
                "handler returned an Embedded task run with no wire representation".into(),
            ));
        }

        let (workload, generation, attempt, phase, started_at, finished_at, error, exit_code) =
            run.into_parts();
        Ok(proto_api::TaskRunInfo {
            workload_api_version: workload.api_version().to_owned(),
            workload_kind: workload.kind().to_owned(),
            generation,
            phase: proto_api::TaskPhase::try_from(phase)? as i32,
            finished_at: finished_at.map(system_time_to_ms).transpose()?,
            started_at: system_time_to_ms(started_at)?,
            exit_code,
            attempt,
            error,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use solti_model::{TaskPhase, WORKLOAD_API_VERSION, WorkloadTypeMeta};
    use std::time::{Duration, UNIX_EPOCH};

    #[test]
    fn run_converts_all_fields() {
        let started = UNIX_EPOCH + Duration::from_millis(1_700_000_000_000);
        let finished = UNIX_EPOCH + Duration::from_millis(1_700_000_001_500);

        let workload = WorkloadTypeMeta::new("example.io/v1", "DatabaseBackup").unwrap();
        let run = TaskRun::from_parts(
            workload,
            3,
            2,
            TaskPhase::Failed,
            started,
            Some(finished),
            Some("boom".into()),
            Some(137),
        )
        .unwrap();

        let proto = proto_api::TaskRunInfo::try_from(run).unwrap();

        assert_eq!(proto.workload_api_version, "example.io/v1");
        assert_eq!(proto.workload_kind, "DatabaseBackup");
        assert_eq!(proto.attempt, 2);
        assert_eq!(proto.generation, 3);
        assert_eq!(proto.phase, proto_api::TaskPhase::Failed as i32);
        assert_eq!(proto.started_at, 1_700_000_000_000);
        assert_eq!(proto.finished_at, Some(1_700_000_001_500));
        assert_eq!(proto.error.as_deref(), Some("boom"));
        assert_eq!(proto.exit_code, Some(137));
    }

    #[test]
    fn run_active_has_no_finished_timestamp() {
        let workload = WorkloadTypeMeta::new(WORKLOAD_API_VERSION, "Subprocess").unwrap();
        let run = TaskRun::starting(1, 1, workload).unwrap();
        let proto = proto_api::TaskRunInfo::try_from(run).unwrap();
        assert_eq!(proto.finished_at, None);
        assert_eq!(proto.exit_code, None);
    }

    #[test]
    fn embedded_run_is_not_exposed() {
        let workload = WorkloadTypeMeta::new(WORKLOAD_API_VERSION, "Embedded").unwrap();
        let run = TaskRun::starting(1, 1, workload).unwrap();

        let error = proto_api::TaskRunInfo::try_from(run).unwrap_err();
        assert!(matches!(error, ApiError::Internal(message) if message.contains("Embedded")));
    }
}