Skip to main content

subc_protocol/
call_mode.rs

1//! Call modes stamped by the daemon from the scope of a route.
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5use crate::scope::ScopeStamp;
6
7/// Declaring this in `capabilities.provides` promises that a module reads the
8/// call mode on every bind and handles classic, codemode and flow calls.
9pub const CALL_MODES_CAPABILITY: &str = "call-modes/v1";
10
11/// The mode the daemon derives from a route's scope, never from its request body.
12///
13/// Providers must refuse calls whose mode is [`CallMode::Unknown`]; an unfamiliar
14/// mode must never silently acquire classic behavior or delegated authority.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum CallMode {
17    Classic,
18    Codemode,
19    Flow,
20    Unknown(String),
21}
22
23impl Serialize for CallMode {
24    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
25        serializer.serialize_str(match self {
26            Self::Classic => "classic",
27            Self::Codemode => "codemode",
28            Self::Flow => "flow",
29            Self::Unknown(value) => value,
30        })
31    }
32}
33
34impl<'de> Deserialize<'de> for CallMode {
35    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
36        Ok(match String::deserialize(deserializer)?.as_str() {
37            "classic" => Self::Classic,
38            "codemode" => Self::Codemode,
39            "flow" => Self::Flow,
40            value => Self::Unknown(value.to_owned()),
41        })
42    }
43}
44
45/// Derive the mode solely from the daemon's scope stamp. A run takes precedence
46/// over a flow; a plain scope or no scope is classic. This also supplies the
47/// fallback for readers receiving a bind from a daemon that predates call modes.
48pub fn derive_call_mode(scope: Option<&ScopeStamp>) -> CallMode {
49    match scope {
50        Some(stamp) if stamp.attributes.run_id.is_some() => CallMode::Codemode,
51        Some(stamp) if stamp.attributes.flow_id.is_some() => CallMode::Flow,
52        _ => CallMode::Classic,
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::{
60        scope::{ScopeAttributes, ScopeKind},
61        Principal,
62    };
63
64    fn stamp(attributes: ScopeAttributes) -> ScopeStamp {
65        ScopeStamp::new(Principal::Direct, "scope-1", 1, ScopeKind::Ephemeral)
66            .with_attributes(attributes)
67    }
68
69    #[test]
70    fn known_modes_round_trip_as_lowercase_strings() {
71        for (mode, wire) in [
72            (CallMode::Classic, "\"classic\""),
73            (CallMode::Codemode, "\"codemode\""),
74            (CallMode::Flow, "\"flow\""),
75        ] {
76            assert_eq!(serde_json::to_string(&mode).unwrap(), wire);
77            assert_eq!(serde_json::from_str::<CallMode>(wire).unwrap(), mode);
78        }
79    }
80
81    #[test]
82    fn unknown_modes_preserve_the_string_and_non_strings_are_refused() {
83        for value in ["future-mode", "Classic", "", "mode\nwith\"escapes"] {
84            let wire = serde_json::to_string(value).unwrap();
85            let mode = serde_json::from_str::<CallMode>(&wire).unwrap();
86            assert_eq!(mode, CallMode::Unknown(value.to_string()));
87            assert_eq!(serde_json::to_string(&mode).unwrap(), wire);
88        }
89        for wire in ["null", "1", "true", "{}", "[]"] {
90            assert!(serde_json::from_str::<CallMode>(wire).is_err());
91        }
92    }
93
94    #[test]
95    fn run_scope_derives_codemode_even_when_delegating() {
96        for delegates in [false, true] {
97            let stamp = stamp(
98                ScopeAttributes::new()
99                    .with_agent_id(Some("agent-1".to_string()))
100                    .with_run_id(Some("run-1".to_string()))
101                    .with_delegates(delegates),
102            );
103            assert_eq!(derive_call_mode(Some(&stamp)), CallMode::Codemode);
104        }
105    }
106
107    #[test]
108    fn flow_scope_derives_flow() {
109        let stamp = stamp(ScopeAttributes::new().with_flow_id(Some("flow-1".to_string())));
110        assert_eq!(derive_call_mode(Some(&stamp)), CallMode::Flow);
111    }
112
113    #[test]
114    fn run_id_takes_precedence_over_flow_id() {
115        let stamp = stamp(
116            ScopeAttributes::new()
117                .with_run_id(Some("run-1".to_string()))
118                .with_flow_id(Some("flow-1".to_string())),
119        );
120        assert_eq!(derive_call_mode(Some(&stamp)), CallMode::Codemode);
121    }
122
123    #[test]
124    fn plain_scope_and_no_scope_derive_classic() {
125        let plain = stamp(ScopeAttributes::new());
126        let agent = stamp(
127            ScopeAttributes::new()
128                .with_agent_id(Some("agent-1".to_string()))
129                .with_delegates(true),
130        );
131        assert_eq!(derive_call_mode(Some(&plain)), CallMode::Classic);
132        assert_eq!(derive_call_mode(Some(&agent)), CallMode::Classic);
133        assert_eq!(derive_call_mode(None), CallMode::Classic);
134    }
135}