Skip to main content

app_facade_api/
call.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum ActionMode {
7    #[default]
8    Direct,
9    Prepare,
10    Commit,
11    Reject,
12}
13
14pub use conversation_api::execution::ResponseDetail;
15
16#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase", deny_unknown_fields)]
18pub struct FacadeControl {
19    pub mode: ActionMode,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub prepared_action_id: Option<String>,
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub approval_token: Option<String>,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub response_detail: Option<ResponseDetail>,
26}
27
28impl FacadeControl {
29    pub fn validate(&self) -> Result<(), &'static str> {
30        match self.mode {
31            ActionMode::Direct | ActionMode::Prepare => {
32                if self.prepared_action_id.is_some() || self.approval_token.is_some() {
33                    return Err("direct and prepare controls cannot carry commit credentials");
34                }
35            }
36            ActionMode::Commit => {
37                if self
38                    .prepared_action_id
39                    .as_deref()
40                    .is_none_or(|value| value.trim().is_empty())
41                    || self
42                        .approval_token
43                        .as_deref()
44                        .is_none_or(|value| value.trim().is_empty())
45                {
46                    return Err("commit control requires preparedActionId and approvalToken");
47                }
48            }
49            ActionMode::Reject => {
50                if self
51                    .prepared_action_id
52                    .as_deref()
53                    .is_none_or(|value| value.trim().is_empty())
54                {
55                    return Err("reject control requires preparedActionId");
56                }
57                if self.approval_token.is_some() {
58                    return Err("reject control cannot carry approvalToken");
59                }
60            }
61        }
62        Ok(())
63    }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase", deny_unknown_fields)]
68pub struct FacadeCall {
69    pub control: FacadeControl,
70    pub input: Value,
71}
72
73impl FacadeCall {
74    #[must_use]
75    pub fn direct(input: Value) -> Self {
76        Self {
77            control: FacadeControl::default(),
78            input,
79        }
80    }
81
82    pub fn validate(&self) -> Result<(), &'static str> {
83        self.control.validate()?;
84        if self.input.get("outputFormat").is_some() {
85            return Err("outputFormat is not business input; use control.responseDetail");
86        }
87        Ok(())
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn direct_call_has_a_strict_envelope() {
97        let call: FacadeCall = serde_json::from_value(serde_json::json!({
98            "control": {"mode": "direct"},
99            "input": {"value": 1}
100        }))
101        .unwrap();
102        assert_eq!(call.control.mode, ActionMode::Direct);
103        assert_eq!(call.input["value"], 1);
104        assert!(serde_json::from_value::<FacadeCall>(serde_json::json!({
105            "mode": "direct",
106            "input": {}
107        }))
108        .is_err());
109    }
110
111    #[test]
112    fn action_credentials_are_mode_specific() {
113        let invalid = FacadeControl {
114            mode: ActionMode::Commit,
115            prepared_action_id: Some("action-1".into()),
116            ..FacadeControl::default()
117        };
118        assert!(invalid.validate().is_err());
119
120        let whitespace = FacadeControl {
121            mode: ActionMode::Reject,
122            prepared_action_id: Some(" \t".into()),
123            ..FacadeControl::default()
124        };
125        assert!(whitespace.validate().is_err());
126    }
127}