Skip to main content

hatchet_sdk/
utils.rs

1use std::cell::RefCell;
2use std::time::{Duration, SystemTime, UNIX_EPOCH};
3
4use prost_types::Timestamp;
5use tonic::Request;
6use tonic::metadata::MetadataValue;
7
8use crate::HatchetError;
9
10pub(crate) fn proto_timestamp_now() -> Result<Timestamp, HatchetError> {
11    let now = SystemTime::now().duration_since(UNIX_EPOCH)?;
12
13    Ok(Timestamp {
14        seconds: now.as_secs() as i64,
15        nanos: now.subsec_nanos() as i32,
16    })
17}
18
19pub(crate) fn add_grpc_auth_header<T>(
20    request: &mut Request<T>,
21    token: &str,
22) -> Result<(), HatchetError> {
23    let token_header: MetadataValue<_> = format!("Bearer {}", token).parse().map_err(
24        |e: tonic::metadata::errors::InvalidMetadataValue| {
25            HatchetError::InvalidAuthHeader(e.to_string())
26        },
27    )?;
28    request.metadata_mut().insert("authorization", token_header);
29    Ok(())
30}
31
32/// Converts a std::time::Duration to a string expression.
33pub(crate) fn duration_to_expr(duration: Duration) -> String {
34    const HOUR: u64 = 3600;
35    const MINUTE: u64 = 60;
36
37    let seconds = duration.as_secs();
38
39    if seconds == 0 {
40        return String::from("0s");
41    }
42    if seconds.is_multiple_of(HOUR) {
43        return format!("{}h", seconds / HOUR);
44    }
45    if seconds.is_multiple_of(MINUTE) {
46        return format!("{}m", seconds / MINUTE);
47    }
48    format!("{}s", seconds)
49}
50
51#[derive(Clone, Debug)]
52pub(crate) struct ExecutionContext {
53    pub(crate) workflow_run_id: String,
54    pub(crate) task_run_external_id: String,
55    pub(crate) child_index: i32,
56}
57
58tokio::task_local! {
59    pub(crate) static EXECUTION_CONTEXT: RefCell<ExecutionContext>;
60}
61
62/// A type that serializes to an empty JSON object.
63/// This can be used for workflows that don't need input or don't return output.
64///
65/// # Example
66/// ```rust
67/// use hatchet_sdk::EmptyModel;
68///
69/// // EmptyModel serializes to an empty JSON object
70/// let empty_input = EmptyModel;
71/// let serialized = serde_json::to_value(empty_input).unwrap();
72/// assert_eq!(serialized, serde_json::json!({}));
73/// ```
74#[derive(Debug, Clone, Copy)]
75pub struct EmptyModel;
76
77impl serde::Serialize for EmptyModel {
78    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
79    where
80        S: serde::Serializer,
81    {
82        use serde::ser::SerializeMap;
83        let map = serializer.serialize_map(Some(0))?;
84        map.end()
85    }
86}
87
88impl<'de> serde::Deserialize<'de> for EmptyModel {
89    fn deserialize<D>(deserializer: D) -> Result<EmptyModel, D::Error>
90    where
91        D: serde::Deserializer<'de>,
92    {
93        use std::fmt;
94
95        use serde::de::{self, MapAccess, Visitor};
96
97        struct EmptyModelVisitor;
98
99        impl<'de> Visitor<'de> for EmptyModelVisitor {
100            type Value = EmptyModel;
101
102            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
103                formatter.write_str("an empty object")
104            }
105
106            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
107            where
108                A: MapAccess<'de>,
109            {
110                if map.next_entry::<String, serde_json::Value>()?.is_some() {
111                    return Err(de::Error::custom("expected empty object"));
112                }
113                Ok(EmptyModel)
114            }
115        }
116
117        deserializer.deserialize_map(EmptyModelVisitor)
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_empty_input_serialization() {
127        let empty_model = EmptyModel;
128        let serialized = serde_json::to_value(empty_model).unwrap();
129        assert_eq!(serialized, serde_json::json!({}));
130    }
131
132    #[test]
133    fn test_empty_input_deserialization() {
134        let json = serde_json::json!({});
135        let empty_model: EmptyModel = serde_json::from_value(json).unwrap();
136        assert_eq!(format!("{:?}", empty_model), "EmptyModel");
137    }
138
139    #[test]
140    fn test_duration_to_expr_hours() {
141        assert_eq!(duration_to_expr(Duration::from_secs(3600)), "1h");
142        assert_eq!(duration_to_expr(Duration::from_secs(7200)), "2h");
143        assert_eq!(duration_to_expr(Duration::from_secs(18000)), "5h");
144    }
145
146    #[test]
147    fn test_duration_to_expr_minutes() {
148        assert_eq!(duration_to_expr(Duration::from_secs(60)), "1m");
149        assert_eq!(duration_to_expr(Duration::from_secs(120)), "2m");
150        assert_eq!(duration_to_expr(Duration::from_secs(300)), "5m");
151        assert_eq!(duration_to_expr(Duration::from_secs(3540)), "59m");
152    }
153
154    #[test]
155    fn test_duration_to_expr_seconds() {
156        assert_eq!(duration_to_expr(Duration::from_secs(1)), "1s");
157        assert_eq!(duration_to_expr(Duration::from_secs(30)), "30s");
158        assert_eq!(duration_to_expr(Duration::from_secs(45)), "45s");
159        assert_eq!(duration_to_expr(Duration::from_secs(59)), "59s");
160        assert_eq!(duration_to_expr(Duration::from_secs(3661)), "3661s");
161    }
162
163    #[test]
164    fn test_duration_to_expr_zero() {
165        assert_eq!(duration_to_expr(Duration::from_secs(0)), "0s");
166    }
167}