Skip to main content

actioncable/
protocol.rs

1use std::fmt;
2
3use crate::error::{DisconnectReason, Error};
4use crate::message::Message;
5
6/// Translates between Action Cable commands and the bytes on the wire. It is the seam where
7/// an Action Cable protocol plugs in.
8///
9/// One protocol speaks one subprotocol. A client offers every protocol it was given and
10/// speaks the one the server picks, so supporting a new protocol means adding one rather
11/// than replacing the list.
12pub trait Protocol: Send + Sync {
13    /// The name this protocol negotiates under.
14    fn subprotocol(&self) -> &str;
15
16    /// Turns a command into one outgoing message.
17    fn encode(&self, command: &Command) -> Result<Vec<u8>, Error>;
18
19    /// Turns one incoming message into a frame the client understands. A frame of
20    /// [`Kind::Message`] must carry a [`message`](Incoming::message); the client drops one
21    /// that doesn't, where the Go client delivers an empty message.
22    fn decode(&self, payload: &[u8]) -> Result<Incoming, Error>;
23}
24
25/// The sentinel an Action Cable server names when it speaks none of the subprotocols
26/// offered. The client offers it last on every handshake, the way Rails' own clients do, so
27/// a server with nothing in common can say so outright instead of leaving the subprotocol
28/// blank.
29pub const SUBPROTOCOL_UNSUPPORTED: &str = "actioncable-unsupported";
30
31/// The verb of a client-to-server command.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CommandName {
34    /// Asks the server for a subscription to one identifier.
35    Subscribe,
36    /// Tells the server to drop one.
37    Unsubscribe,
38    /// Carries a payload to the channel.
39    Message,
40}
41
42impl CommandName {
43    /// The verb as the wire spells it.
44    pub fn as_str(self) -> &'static str {
45        match self {
46            CommandName::Subscribe => "subscribe",
47            CommandName::Unsubscribe => "unsubscribe",
48            CommandName::Message => "message",
49        }
50    }
51}
52
53impl fmt::Display for CommandName {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        f.write_str(self.as_str())
56    }
57}
58
59/// A client-to-server message. `data` carries the already encoded action payload and is
60/// only set for [`CommandName::Message`].
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Command {
63    /// The verb.
64    pub name: CommandName,
65    /// The identifier the command is about, as [`Identifier::key`](crate::Identifier::key)
66    /// spells it.
67    pub identifier: String,
68    /// The already encoded action payload, on a [`CommandName::Message`] and nothing else.
69    pub data: Option<String>,
70}
71
72impl Command {
73    /// Asks for a subscription to `identifier`.
74    pub fn subscribe(identifier: impl Into<String>) -> Command {
75        Command {
76            name: CommandName::Subscribe,
77            identifier: identifier.into(),
78            data: None,
79        }
80    }
81
82    /// Tells the server to drop `identifier`.
83    pub fn unsubscribe(identifier: impl Into<String>) -> Command {
84        Command {
85            name: CommandName::Unsubscribe,
86            identifier: identifier.into(),
87            data: None,
88        }
89    }
90
91    /// Carries `data`, already encoded, to the channel behind `identifier`.
92    pub fn message(identifier: impl Into<String>, data: impl Into<String>) -> Command {
93        Command {
94            name: CommandName::Message,
95            identifier: identifier.into(),
96            data: Some(data.into()),
97        }
98    }
99}
100
101/// The type of a server-to-client frame.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum Kind {
104    /// The server finished setting the connection up and will act on commands from here.
105    Welcome,
106    /// The heartbeat, every three seconds.
107    Ping,
108    /// The server is hanging up, and says whether to come back.
109    Disconnect,
110    /// The channel accepted a subscription.
111    Confirmation,
112    /// The channel turned one down.
113    Rejection,
114    /// Something a channel broadcast or transmitted.
115    Message,
116}
117
118impl fmt::Display for Kind {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        f.write_str(match self {
121            Kind::Welcome => "welcome",
122            Kind::Ping => "ping",
123            Kind::Disconnect => "disconnect",
124            Kind::Confirmation => "confirm_subscription",
125            Kind::Rejection => "reject_subscription",
126            Kind::Message => "message",
127        })
128    }
129}
130
131/// A decoded server-to-client frame. `reason` and `reconnect` are only set on
132/// [`Kind::Disconnect`], `message` on [`Kind::Message`] and [`Kind::Ping`], and the
133/// identifier is empty on the frames that concern the connection rather than a
134/// subscription. A [`Kind::Message`] frame with no `message` is dropped and logged rather
135/// than delivered empty, which is where this client parts from the Go one.
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct Incoming {
138    /// What the server sent.
139    pub kind: Kind,
140    /// The subscription it is about, empty on the frames about the connection itself.
141    pub identifier: String,
142    /// The payload, on [`Kind::Message`] and [`Kind::Ping`].
143    pub message: Option<Message>,
144    /// Why the server is hanging up, on [`Kind::Disconnect`].
145    pub reason: Option<DisconnectReason>,
146    /// Whether the server expects the client back, on [`Kind::Disconnect`].
147    pub reconnect: bool,
148}
149
150impl Incoming {
151    /// An otherwise empty frame of `kind`, to fill in.
152    pub fn new(kind: Kind) -> Incoming {
153        Incoming {
154            kind,
155            identifier: String::new(),
156            message: None,
157            reason: None,
158            reconnect: false,
159        }
160    }
161}