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