├── .github └── workflows │ ├── ci.yaml │ └── publish.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src └── lib.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [ push ] 3 | env: 4 | CARGO_TERM_COLOR: always 5 | 6 | jobs: 7 | ci: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | rust: 12 | - stable 13 | - beta 14 | - nightly 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: ${{ matrix.rust }} 23 | override: true 24 | components: rustfmt, clippy 25 | 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | command: build 29 | 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | 34 | - uses: actions-rs/cargo@v1 35 | with: 36 | command: fmt 37 | args: --all -- --check 38 | 39 | - uses: actions-rs/cargo@v1 40 | with: 41 | command: clippy 42 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish package to crates.io 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | toolchain: stable 16 | override: true 17 | - name: cargo fetch 18 | uses: actions-rs/cargo@v1 19 | with: 20 | command: fetch 21 | - name: cargo build 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: build 25 | args: --tests 26 | - name: cargo test 27 | uses: actions-rs/cargo@v1 28 | with: 29 | command: test 30 | 31 | publish_check: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v1 35 | - uses: actions-rs/toolchain@v1 36 | with: 37 | toolchain: stable 38 | override: true 39 | - name: cargo fetch 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: fetch 43 | - name: cargo publish 44 | uses: actions-rs/cargo@v1 45 | with: 46 | command: publish 47 | args: --dry-run 48 | - name: docs check 49 | uses: actions-rs/cargo@v1 50 | with: 51 | command: doc 52 | args: --no-deps 53 | 54 | publish: 55 | name: Publish 56 | runs-on: ubuntu-latest 57 | needs: [ test, publish_check ] 58 | if: startsWith(github.ref, 'refs/tags/') 59 | steps: 60 | - uses: actions/checkout@v1 61 | - uses: actions-rs/toolchain@v1 62 | with: 63 | toolchain: stable 64 | override: true 65 | - name: cargo fetch 66 | uses: actions-rs/cargo@v1 67 | with: 68 | command: fetch 69 | - name: cargo publish 70 | uses: actions-rs/cargo@v1 71 | env: 72 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 73 | with: 74 | command: publish 75 | - name: Create Release 76 | uses: actions/create-release@v1 77 | env: 78 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 79 | with: 80 | tag_name: ${{ github.ref }} 81 | release_name: ${{ github.ref }} 82 | draft: false 83 | prerelease: false 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "socketio-rust-emitter" 3 | version = "0.1.2" 4 | authors = ["Ryo Motozawa "] 5 | edition = "2018" 6 | 7 | description = "A Rust implementation of socketio-emitter " 8 | homepage = "https://github.com/epli2/socketio-rust-emitter" 9 | repository = "https://github.com/epli2/socketio-rust-emitter" 10 | readme = "README.md" 11 | keywords = ["socketio"] 12 | license = "MIT" 13 | 14 | [dependencies] 15 | redis = "0.27" 16 | rmp = "0.8" 17 | serde = "1.0" 18 | serde_derive = "1.0" 19 | rmp-serde = "1.3" 20 | 21 | [dev-dependencies] 22 | testcontainers = { version = "0.22.0", features = ["blocking"] } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ryo Motozawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # socketio-rust-emitter 2 | 3 | [![build status](https://github.com/epli2/socketio-rust-emitter/actions/workflows/ci.yaml/badge.svg?branch=master&event=push)](https://github.com/epli2/socketio-rust-emitter/actions) 4 | [![socketio-rust-emitter at crates.io](https://img.shields.io/crates/v/socketio-rust-emitter.svg)](https://crates.io/crates/socketio-rust-emitter) 5 | [![socketio-rust-emitter at docs.rs](https://docs.rs/socketio-rust-emitter/badge.svg)](https://docs.rs/socketio-rust-emitter) 6 | 7 | A Rust implementation of [socket.io-emitter](https://github.com/socketio/socket.io-emitter). 8 | 9 | ## How to use 10 | 11 | ```rust 12 | use chrono::Utc; 13 | use std::thread; 14 | use std::time::Duration; 15 | 16 | let io = Emitter::new("127.0.0.1"); 17 | let _ = thread::spawn(move || loop { 18 | thread::sleep(Duration::from_millis(5000)); 19 | io.clone().emit(vec!["time", &format!("{}", Utc::now())]); 20 | }).join(); 21 | ``` 22 | 23 | ```rust 24 | // Different constructor options. 25 | 26 | //1. Initialize with host:port string 27 | let io = Emitter::new("localhost:6379") 28 | // 2. Initlize with host, port object. 29 | let io = Emitter::new(EmitterOpts { 30 | host: "localhost".to_owned(), 31 | port: 6379, 32 | ..Default::default() 33 | }); 34 | ``` 35 | 36 | ## Examples 37 | 38 | ```rust 39 | let io = Emitter::new(EmitterOpts { host: "127.0.0.1".to_owned(), port: 6379, ..Default::default() }); 40 | 41 | // sending to all clients 42 | io.clone().emit(vec!["broadcast", /* ... */]); 43 | 44 | // sending to all clients in "game" room 45 | io.clone().to("game").emit(vec!["new-game", /* ... */]); 46 | 47 | // sending to individual socketid (private message) 48 | io.clone().to().emit(vec!["private", /* ... */]); 49 | 50 | let nsp = io.clone().of("/admin"); 51 | 52 | // sending to all clients in "admin" namespace 53 | nsp.clone().emit(vec!["namespace", /* ... */]); 54 | 55 | // sending to all clients in "admin" namespace and in "notifications" room 56 | nsp.clone().to("notifications").emit(vec!["namespace", /* ... */]); 57 | ``` 58 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | 4 | use redis::Commands; 5 | use rmp_serde::Serializer; 6 | use serde::Serialize; 7 | use std::collections::HashMap; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Emitter { 11 | redis: redis::Client, 12 | prefix: String, 13 | nsp: String, 14 | channel: String, 15 | rooms: Vec, 16 | flags: HashMap, 17 | uid: String, 18 | } 19 | 20 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 21 | struct Opts { 22 | rooms: Vec, 23 | flags: HashMap, 24 | } 25 | 26 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 27 | pub struct Packet { 28 | #[serde(rename = "type")] 29 | _type: i32, 30 | data: Vec, 31 | nsp: String, 32 | } 33 | 34 | #[derive(Debug, PartialEq, Clone, Default)] 35 | pub struct EmitterOpts<'a> { 36 | pub host: String, 37 | pub port: i32, 38 | pub socket: Option, 39 | pub key: Option<&'a str>, 40 | } 41 | 42 | pub trait IntoEmitter { 43 | fn into_emitter(self) -> Emitter; 44 | } 45 | 46 | impl IntoEmitter for redis::Client { 47 | fn into_emitter(self) -> Emitter { 48 | create_emitter(self, "socket.io", "/") 49 | } 50 | } 51 | 52 | impl<'a> IntoEmitter for EmitterOpts<'a> { 53 | fn into_emitter(self) -> Emitter { 54 | let addr = format!("redis://{}:{}", self.host, self.port); 55 | let prefix = self.key.unwrap_or("socket.io"); 56 | 57 | create_emitter(redis::Client::open(addr.as_str()).unwrap(), prefix, "/") 58 | } 59 | } 60 | 61 | impl IntoEmitter for &str { 62 | fn into_emitter(self) -> Emitter { 63 | create_emitter( 64 | redis::Client::open(format!("redis://{}", self).as_str()).unwrap(), 65 | "socket.io", 66 | "/", 67 | ) 68 | } 69 | } 70 | 71 | fn create_emitter(redis: redis::Client, prefix: &str, nsp: &str) -> Emitter { 72 | Emitter { 73 | redis, 74 | prefix: prefix.to_string(), 75 | nsp: nsp.to_string(), 76 | channel: format!("{}#{}#", prefix, nsp), 77 | rooms: Vec::new(), 78 | flags: HashMap::new(), 79 | uid: "emitter".to_string(), 80 | } 81 | } 82 | 83 | impl Emitter { 84 | pub fn new(data: I) -> Emitter { 85 | data.into_emitter() 86 | } 87 | 88 | pub fn to(mut self, room: &str) -> Emitter { 89 | self.rooms.push(room.to_string()); 90 | self 91 | } 92 | pub fn of(self, nsp: &str) -> Emitter { 93 | create_emitter(self.redis, self.prefix.as_str(), nsp) 94 | } 95 | pub fn json(mut self) -> Emitter { 96 | let mut flags = HashMap::new(); 97 | flags.insert("json".to_string(), true); 98 | self.flags = flags; 99 | self 100 | } 101 | pub fn volatile(mut self) -> Emitter { 102 | let mut flags = HashMap::new(); 103 | flags.insert("volatile".to_string(), true); 104 | self.flags = flags; 105 | self 106 | } 107 | pub fn broadcast(mut self) -> Emitter { 108 | let mut flags = HashMap::new(); 109 | flags.insert("broadcast".to_string(), true); 110 | self.flags = flags; 111 | self 112 | } 113 | pub fn emit(mut self, message: Vec<&str>) -> Emitter { 114 | let packet = Packet { 115 | _type: 2, 116 | data: message.iter().map(|s| s.to_string()).collect(), 117 | nsp: self.nsp.clone(), 118 | }; 119 | let opts = Opts { 120 | rooms: self.rooms.clone(), 121 | flags: self.flags.clone(), 122 | }; 123 | let mut msg = Vec::new(); 124 | let val = (self.uid.clone(), packet, opts); 125 | val.serialize(&mut Serializer::new(&mut msg).with_struct_map()) 126 | .unwrap(); 127 | 128 | let channel = if self.rooms.len() == 1 { 129 | format!("{}{}#", self.channel, self.rooms.join("#")) 130 | } else { 131 | self.channel.clone() 132 | }; 133 | let _: () = self.redis.publish(channel, msg).unwrap(); 134 | self.rooms = vec![]; 135 | self.flags = HashMap::new(); 136 | self 137 | } 138 | } 139 | 140 | #[cfg(test)] 141 | mod tests { 142 | use crate::{Emitter, Opts, Packet}; 143 | use redis::Msg; 144 | use rmp_serde::Deserializer; 145 | use serde::Deserialize; 146 | use testcontainers::runners::SyncRunner; 147 | 148 | macro_rules! create_redis { 149 | ($redis:ident) => { 150 | let redis = testcontainers::GenericImage::new("redis", "latest") 151 | .with_exposed_port(testcontainers::core::ContainerPort::Tcp(6379)) 152 | .with_wait_for(testcontainers::core::WaitFor::message_on_stdout("Ready to accept connections")) 153 | .start() 154 | .unwrap(); 155 | let redis_url = format!( 156 | "redis://localhost:{}", 157 | redis.get_host_port_ipv4(6379).unwrap() 158 | ); 159 | let $redis = redis::Client::open(redis_url.as_str()).unwrap(); 160 | }; 161 | } 162 | 163 | fn decode_msg(msg: Msg) -> (String, Packet, Opts) { 164 | let payload: Vec = msg.get_payload().unwrap(); 165 | let mut de = Deserializer::new(&payload[..]); 166 | Deserialize::deserialize(&mut de).unwrap() 167 | } 168 | 169 | #[test] 170 | fn emit() { 171 | create_redis!(redis); 172 | let mut con = redis.get_connection().unwrap(); 173 | let mut pubsub = con.as_pubsub(); 174 | pubsub.subscribe("socket.io#/#").unwrap(); 175 | 176 | // act 177 | let io = Emitter::new(redis); 178 | io.emit(vec!["test1", "test2"]); 179 | 180 | // assert 181 | let actual = decode_msg(pubsub.get_message().unwrap()); 182 | assert_eq!("emitter", actual.0); 183 | assert_eq!( 184 | Packet { 185 | _type: 2, 186 | data: vec!["test1".to_string(), "test2".to_string()], 187 | nsp: "/".to_string(), 188 | }, 189 | actual.1 190 | ); 191 | assert_eq!( 192 | Opts { 193 | rooms: vec![], 194 | flags: Default::default() 195 | }, 196 | actual.2 197 | ); 198 | } 199 | 200 | #[test] 201 | fn emit_in_namespaces() { 202 | create_redis!(redis); 203 | let mut con = redis.get_connection().unwrap(); 204 | let mut pubsub = con.as_pubsub(); 205 | pubsub.subscribe("socket.io#/custom#").unwrap(); 206 | 207 | // act 208 | let io = Emitter::new(redis); 209 | io.of("/custom").emit(vec!["test"]); 210 | 211 | // assert 212 | let actual = decode_msg(pubsub.get_message().unwrap()); 213 | assert_eq!("emitter", actual.0); 214 | assert_eq!( 215 | Packet { 216 | _type: 2, 217 | data: vec!["test".to_string()], 218 | nsp: "/custom".to_string(), 219 | }, 220 | actual.1 221 | ); 222 | assert_eq!( 223 | Opts { 224 | rooms: vec![], 225 | flags: Default::default() 226 | }, 227 | actual.2 228 | ); 229 | } 230 | 231 | #[test] 232 | fn emit_to_namespaces() { 233 | create_redis!(redis); 234 | let mut con = redis.get_connection().unwrap(); 235 | let mut pubsub = con.as_pubsub(); 236 | pubsub.subscribe("socket.io#/custom#").unwrap(); 237 | 238 | // act 239 | let io = Emitter::new(redis); 240 | io.of("/custom").emit(vec!["test"]); 241 | 242 | // assert 243 | let actual = decode_msg(pubsub.get_message().unwrap()); 244 | assert_eq!("emitter", actual.0); 245 | assert_eq!( 246 | Packet { 247 | _type: 2, 248 | data: vec!["test".to_string()], 249 | nsp: "/custom".to_string(), 250 | }, 251 | actual.1 252 | ); 253 | assert_eq!( 254 | Opts { 255 | rooms: vec![], 256 | flags: Default::default() 257 | }, 258 | actual.2 259 | ); 260 | } 261 | 262 | #[test] 263 | fn emit_to_room() { 264 | create_redis!(redis); 265 | let mut con = redis.get_connection().unwrap(); 266 | let mut pubsub = con.as_pubsub(); 267 | pubsub.subscribe("socket.io#/#room1#").unwrap(); 268 | 269 | // act 270 | let io = Emitter::new(redis); 271 | io.to("room1").emit(vec!["test"]); 272 | 273 | // assert 274 | let actual = decode_msg(pubsub.get_message().unwrap()); 275 | assert_eq!("emitter", actual.0); 276 | assert_eq!( 277 | Packet { 278 | _type: 2, 279 | data: vec!["test".to_string()], 280 | nsp: "/".to_string(), 281 | }, 282 | actual.1 283 | ); 284 | assert_eq!( 285 | Opts { 286 | rooms: vec!["room1".to_string()], 287 | flags: Default::default() 288 | }, 289 | actual.2 290 | ); 291 | } 292 | } 293 | --------------------------------------------------------------------------------