Skip to main content

actioncable/
identifier.rs

1use std::collections::BTreeMap;
2use std::fmt;
3
4use serde_json::Value;
5
6/// Names one subscription: a channel and the params that go with it, like the id of the
7/// record the channel streams for.
8///
9/// The server treats the identifier's JSON encoding as an opaque key and echoes it back on
10/// every frame it sends for the subscription, so the encoding has to be stable: the keys
11/// come out sorted, `channel` among them.
12///
13/// ```
14/// use actioncable::Identifier;
15///
16/// let room = Identifier::new("RoomChannel").param("id", 42);
17/// assert_eq!(r#"{"channel":"RoomChannel","id":42}"#, room.key());
18/// ```
19///
20/// A channel with no params needs only the name.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Identifier {
23    channel: String,
24    params: BTreeMap<String, Value>,
25}
26
27impl Identifier {
28    /// Names a channel, with no params.
29    pub fn new(channel: impl Into<String>) -> Identifier {
30        Identifier {
31            channel: channel.into(),
32            params: BTreeMap::new(),
33        }
34    }
35
36    /// Adds one param, like the id of the record the channel streams for.
37    pub fn param(mut self, name: impl Into<String>, value: impl Into<Value>) -> Identifier {
38        self.params.insert(name.into(), value.into());
39        self
40    }
41
42    /// The channel's name.
43    pub fn channel(&self) -> &str {
44        &self.channel
45    }
46
47    /// The params, sorted by name as the key encodes them.
48    pub fn params(&self) -> &BTreeMap<String, Value> {
49        &self.params
50    }
51
52    /// The JSON string the server knows the subscription by. A param named `channel` loses
53    /// to the channel name.
54    ///
55    /// Where the Go client returns an error, this cannot fail: a param is a
56    /// [`serde_json::Value`], and a map of them always encodes.
57    #[allow(
58        clippy::expect_used,
59        reason = "serializing a map of Values has no failure mode to report"
60    )]
61    pub fn key(&self) -> String {
62        let mut fields = self.params.clone();
63        fields.insert("channel".to_string(), Value::String(self.channel.clone()));
64        serde_json::to_string(&fields).expect("a map of JSON values always encodes")
65    }
66}
67
68impl fmt::Display for Identifier {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.write_str(&self.key())
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn the_key_is_the_sorted_json_encoding() {
80        let identifiers = [
81            (
82                Identifier::new("RoomChannel"),
83                r#"{"channel":"RoomChannel"}"#,
84            ),
85            (
86                Identifier::new("RoomChannel").param("id", 42),
87                r#"{"channel":"RoomChannel","id":42}"#,
88            ),
89            (
90                Identifier::new("RoomChannel")
91                    .param("since", "yesterday")
92                    .param("id", 42),
93                r#"{"channel":"RoomChannel","id":42,"since":"yesterday"}"#,
94            ),
95            (
96                Identifier::new("RoomChannel").param("channel", "impostor"),
97                r#"{"channel":"RoomChannel"}"#,
98            ),
99        ];
100
101        for (identifier, key) in identifiers {
102            assert_eq!(key, identifier.key());
103            assert_eq!(key, identifier.to_string());
104        }
105    }
106
107    /// The Go client's key can fail on params that don't encode. Here they can't exist: a
108    /// param is a `Value`, and a float JSON has no spelling for is one before it arrives.
109    #[test]
110    fn an_identifier_always_encodes() {
111        let identifier = Identifier::new("RoomChannel").param("id", f64::NAN);
112
113        assert_eq!(r#"{"channel":"RoomChannel","id":null}"#, identifier.key());
114    }
115}