Skip to main content

a3s_flow/model/
hook.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4use crate::error::{FlowError, Result};
5
6use super::JsonValue;
7
8/// HTTP route metadata for external hook callbacks.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10#[non_exhaustive]
11pub struct HookCallbackRoute {
12    /// Uppercase HTTP method expected by the callback route.
13    pub method: String,
14    /// Host-owned callback path.
15    pub path: String,
16}
17
18impl HookCallbackRoute {
19    /// Creates callback route metadata and normalizes the method to uppercase.
20    pub fn new(method: impl Into<String>, path: impl Into<String>) -> Self {
21        Self {
22            method: method.into().to_ascii_uppercase(),
23            path: path.into(),
24        }
25    }
26
27    /// Creates callback route metadata for an HTTP `POST` endpoint.
28    pub fn post(path: impl Into<String>) -> Self {
29        Self::new("POST", path)
30    }
31}
32
33/// Typed helper for common hook metadata fields.
34///
35/// Hook metadata is still persisted as JSON in `flow.hook.created` events. This
36/// type only gives Rust workflow authors a stable shape for audit and callback
37/// routing fields.
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39#[non_exhaustive]
40pub struct HookMetadata {
41    /// Application-defined hook kind used for routing and audit.
42    pub kind: String,
43    /// Optional human-readable subject.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub subject: Option<String>,
46    /// Optional callback route exposed by the host.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub callback: Option<HookCallbackRoute>,
49    /// Searchable string labels.
50    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
51    pub labels: BTreeMap<String, String>,
52    /// Application-defined structured metadata.
53    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
54    pub data: BTreeMap<String, JsonValue>,
55}
56
57impl HookMetadata {
58    /// Creates metadata for an application-defined hook kind.
59    pub fn new(kind: impl Into<String>) -> Self {
60        Self {
61            kind: kind.into(),
62            subject: None,
63            callback: None,
64            labels: BTreeMap::new(),
65            data: BTreeMap::new(),
66        }
67    }
68
69    /// Creates metadata for a human-approval hook.
70    pub fn human_approval(subject: impl Into<String>) -> Self {
71        Self::new("human_approval").with_subject(subject)
72    }
73
74    /// Sets the human-readable subject.
75    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
76        self.subject = Some(subject.into());
77        self
78    }
79
80    /// Sets the host callback route.
81    pub fn with_callback_route(mut self, callback: HookCallbackRoute) -> Self {
82        self.callback = Some(callback);
83        self
84    }
85
86    /// Adds or replaces one searchable label.
87    pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
88        self.labels.insert(key.into(), value.into());
89        self
90    }
91
92    /// Adds or replaces one structured metadata value.
93    pub fn with_data(mut self, key: impl Into<String>, value: impl Into<JsonValue>) -> Self {
94        self.data.insert(key.into(), value.into());
95        self
96    }
97
98    /// Serializes the typed metadata into its durable JSON representation.
99    pub fn into_json(self) -> Result<JsonValue> {
100        serde_json::to_value(self).map_err(FlowError::from)
101    }
102}