Skip to main content

solti_api/grpc/convert/
phase.rs

1//! # Phase Conversion
2//!
3//! Converts task phases in both directions.
4//! Unknown domain variants have no v1 wire representation.
5//! `Unspecified` and unknown wire values are invalid request input.
6
7use solti_model::TaskPhase;
8
9use crate::error::ApiError;
10use crate::proto_api;
11
12impl TryFrom<TaskPhase> for proto_api::TaskPhase {
13    type Error = ApiError;
14
15    fn try_from(phase: TaskPhase) -> Result<Self, Self::Error> {
16        Ok(match phase {
17            TaskPhase::Succeeded => Self::Succeeded,
18            TaskPhase::Exhausted => Self::Exhausted,
19            TaskPhase::Canceled => Self::Canceled,
20            TaskPhase::Pending => Self::Pending,
21            TaskPhase::Running => Self::Running,
22            TaskPhase::Timeout => Self::Timeout,
23            TaskPhase::Failed => Self::Failed,
24            _ => {
25                return Err(ApiError::Internal(
26                    "handler returned an unsupported task phase".into(),
27                ));
28            }
29        })
30    }
31}
32
33/// Converts one raw protobuf phase into the domain enum.
34#[cfg(feature = "grpc")]
35pub(crate) fn proto_to_domain_phase(raw: i32) -> Result<TaskPhase, ApiError> {
36    let status = proto_api::TaskPhase::try_from(raw)
37        .map_err(|_| ApiError::InvalidRequest(format!("invalid status value: {raw}")))?;
38
39    match status {
40        proto_api::TaskPhase::Succeeded => Ok(TaskPhase::Succeeded),
41        proto_api::TaskPhase::Exhausted => Ok(TaskPhase::Exhausted),
42        proto_api::TaskPhase::Canceled => Ok(TaskPhase::Canceled),
43        proto_api::TaskPhase::Pending => Ok(TaskPhase::Pending),
44        proto_api::TaskPhase::Running => Ok(TaskPhase::Running),
45        proto_api::TaskPhase::Timeout => Ok(TaskPhase::Timeout),
46        proto_api::TaskPhase::Failed => Ok(TaskPhase::Failed),
47
48        proto_api::TaskPhase::Unspecified => Err(ApiError::InvalidRequest(
49            "status cannot be unspecified".into(),
50        )),
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn task_phase_maps_all_known_variants_both_ways() {
60        let cases = [
61            (TaskPhase::Pending, proto_api::TaskPhase::Pending),
62            (TaskPhase::Running, proto_api::TaskPhase::Running),
63            (TaskPhase::Succeeded, proto_api::TaskPhase::Succeeded),
64            (TaskPhase::Failed, proto_api::TaskPhase::Failed),
65            (TaskPhase::Timeout, proto_api::TaskPhase::Timeout),
66            (TaskPhase::Canceled, proto_api::TaskPhase::Canceled),
67            (TaskPhase::Exhausted, proto_api::TaskPhase::Exhausted),
68        ];
69
70        for (domain, expected_proto) in cases {
71            let proto = proto_api::TaskPhase::try_from(domain).unwrap();
72            assert_eq!(proto, expected_proto, "mismatch for {:?}", domain);
73
74            #[cfg(feature = "grpc")]
75            assert_eq!(
76                proto_to_domain_phase(expected_proto as i32).unwrap(),
77                domain
78            );
79        }
80    }
81
82    #[cfg(feature = "grpc")]
83    #[test]
84    fn proto_to_domain_phase_rejects_unknown_values() {
85        for (raw, expected_message) in [
86            (
87                proto_api::TaskPhase::Unspecified as i32,
88                "unspecified".to_owned(),
89            ),
90            (9999, "9999".to_owned()),
91        ] {
92            let error = proto_to_domain_phase(raw).unwrap_err();
93            assert!(
94                matches!(error, ApiError::InvalidRequest(message) if message.contains(&expected_message))
95            );
96        }
97    }
98}