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}