1#[derive(Debug, Clone, PartialEq, Eq)]
6#[non_exhaustive]
7pub enum Operation {
8 Subscribe,
9 Publish,
10 Presence,
11 ObjectSubscribe,
12 ObjectPublish,
13 AnnotationSubscribe,
14 AnnotationPublish,
15 MessageUpdateOwn,
16 MessageUpdateAny,
17 MessageDeleteOwn,
18 MessageDeleteAny,
19 History,
20 Stats,
21 PushSubscribe,
22 PushAdmin,
23 ChannelMetadata,
24 PrivilegedHeaders,
25 Other(String),
27}
28
29impl Operation {
30 pub fn as_str(&self) -> &str {
32 match self {
33 Operation::Subscribe => "subscribe",
34 Operation::Publish => "publish",
35 Operation::Presence => "presence",
36 Operation::ObjectSubscribe => "object-subscribe",
37 Operation::ObjectPublish => "object-publish",
38 Operation::AnnotationSubscribe => "annotation-subscribe",
39 Operation::AnnotationPublish => "annotation-publish",
40 Operation::MessageUpdateOwn => "message-update-own",
41 Operation::MessageUpdateAny => "message-update-any",
42 Operation::MessageDeleteOwn => "message-delete-own",
43 Operation::MessageDeleteAny => "message-delete-any",
44 Operation::History => "history",
45 Operation::Stats => "stats",
46 Operation::PushSubscribe => "push-subscribe",
47 Operation::PushAdmin => "push-admin",
48 Operation::ChannelMetadata => "channel-metadata",
49 Operation::PrivilegedHeaders => "privileged-headers",
50 Operation::Other(s) => s.as_str(),
51 }
52 }
53}
54
55#[derive(Debug, Clone, Default, PartialEq, Eq)]
59pub struct Capability(std::collections::BTreeMap<String, std::collections::BTreeSet<String>>);
60
61impl Capability {
62 pub fn new() -> Self {
64 Self::default()
65 }
66
67 pub fn allow(
70 mut self,
71 resource: impl Into<String>,
72 ops: impl IntoIterator<Item = Operation>,
73 ) -> Self {
74 let entry = self.0.entry(resource.into()).or_default();
75 for op in ops {
76 entry.insert(op.as_str().to_owned());
77 }
78 self
79 }
80
81 pub fn to_capability_string(&self) -> String {
85 serde_json::to_string(&self.0).expect("capability map is always serializable")
87 }
88
89 pub fn for_room(self, room: &str, ops: impl IntoIterator<Item = Operation>) -> Self {
101 self.allow(room.to_owned(), ops)
102 }
103
104 pub fn to_json(&self) -> serde_json::Value {
107 serde_json::to_value(&self.0).expect("capability map is always serializable")
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn operation_as_str_matches_ably_strings() {
117 assert_eq!(Operation::Publish.as_str(), "publish");
118 assert_eq!(Operation::Subscribe.as_str(), "subscribe");
119 assert_eq!(Operation::ObjectSubscribe.as_str(), "object-subscribe");
120 assert_eq!(Operation::AnnotationPublish.as_str(), "annotation-publish");
121 assert_eq!(Operation::MessageUpdateOwn.as_str(), "message-update-own");
122 assert_eq!(Operation::MessageDeleteAny.as_str(), "message-delete-any");
123 assert_eq!(Operation::ChannelMetadata.as_str(), "channel-metadata");
124 assert_eq!(Operation::PrivilegedHeaders.as_str(), "privileged-headers");
125 assert_eq!(Operation::Other("custom".into()).as_str(), "custom");
126 }
127
128 #[test]
129 fn canonical_string_sorts_keys_and_ops_no_whitespace() {
130 let cap = Capability::new()
131 .allow("z-room", [Operation::Subscribe, Operation::Publish])
132 .allow("a-room", [Operation::History]);
133 assert_eq!(
135 cap.to_capability_string(),
136 r#"{"a-room":["history"],"z-room":["publish","subscribe"]}"#
137 );
138 }
139
140 #[test]
141 fn allow_merges_repeated_resource() {
142 let cap = Capability::new()
143 .allow("r", [Operation::Publish])
144 .allow("r", [Operation::History]);
145 assert_eq!(cap.to_capability_string(), r#"{"r":["history","publish"]}"#);
146 }
147
148 #[test]
149 fn for_room_scopes_bare_room_name() {
150 let cap = Capability::new().for_room("sports", [Operation::Publish, Operation::History]);
152 assert_eq!(
153 cap.to_capability_string(),
154 r#"{"sports":["history","publish"]}"#
155 );
156 }
157
158 #[test]
159 fn to_json_returns_native_object() {
160 let cap = Capability::new().allow("r", [Operation::Publish, Operation::History]);
161 let v = cap.to_json();
162 assert!(v.is_object());
163 assert_eq!(v["r"], serde_json::json!(["history", "publish"]));
164 }
165}