Skip to content

Commit 873c060

Browse files
committed
feat: add Socket.IO support with socketioxide integration 😭
1 parent df7aa97 commit 873c060

File tree

22 files changed

+1229
-92
lines changed

22 files changed

+1229
-92
lines changed

‎Cargo.lock‎

Lines changed: 206 additions & 80 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ async-trait = "0.1.83"
2525
tower-http = { version = "0.6.6", features = ["limit", "cors", "trace"] }
2626
tracing = "0.1.41"
2727
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
28+
29+
socketioxide = { version = "0.18.0", features = ["axum-websockets"] }
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
POSTGRES_USER=user
2+
POSTGRES_PASSWORD=password
3+
POSTGRES_DB=postgres_db
4+
POSTGRES_DATABASE_URL="postgres://user:password@localhost:5432/postgres_db"
5+
DATABASE_URL="postgres://user:password@localhost:5432/postgres_db"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "socketio-server"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
sword = { workspace = true, features = ["websocket"] }
8+
sword-macros = { workspace = true }
9+
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "time"] }
10+
serde = { workspace = true }
11+
dotenv = "0.15.0"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
version: '3.8'
2+
3+
services:
4+
postgres:
5+
image: postgres:15
6+
env_file:
7+
- "./.env"
8+
container_name: sword_postgres_example
9+
restart: unless-stopped
10+
environment:
11+
POSTGRES_USER: ${POSTGRES_USER}
12+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
13+
POSTGRES_DB: ${POSTGRES_DB}
14+
ports:
15+
- "5432:5432"
16+
volumes:
17+
- postgres_data:/var/lib/postgresql/data
18+
19+
volumes:
20+
postgres_data:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[application]
2+
host = "0.0.0.0"
3+
port = 8080
4+
body_limit = "10MB"
5+
name = "Dependency Injection Example"
6+
7+
[db-config]
8+
uri = "${POSTGRES_DATABASE_URL}"
9+
migrations_path = "config/migrations"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Add migration script here
2+
3+
CREATE TABLE tasks(
4+
id INT PRIMARY KEY,
5+
title TEXT NOT NULL
6+
)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use std::{path::Path, sync::Arc};
2+
3+
use serde::Deserialize;
4+
use sqlx::{migrate::Migrator, PgPool};
5+
use sword::prelude::*;
6+
7+
#[derive(Clone, Deserialize)]
8+
#[config(key = "db-config")]
9+
pub struct DatabaseConfig {
10+
uri: String,
11+
migrations_path: String,
12+
}
13+
14+
#[injectable(provider)]
15+
pub struct Database {
16+
pool: Arc<PgPool>,
17+
}
18+
19+
impl Database {
20+
pub async fn new(db_conf: DatabaseConfig) -> Self {
21+
let pool = PgPool::connect(&db_conf.uri)
22+
.await
23+
.expect("Failed to create Postgres connection pool");
24+
25+
let migrator = Migrator::new(Path::new(&db_conf.migrations_path))
26+
.await
27+
.unwrap();
28+
29+
migrator
30+
.run(&pool)
31+
.await
32+
.expect("Failed to run database migrations");
33+
34+
Self {
35+
pool: Arc::new(pool),
36+
}
37+
}
38+
39+
pub fn get_pool(&self) -> &PgPool {
40+
&self.pool
41+
}
42+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use dotenv::dotenv;
2+
use std::sync::Arc;
3+
use sword::prelude::*;
4+
use sword_macros::{
5+
on_connection, on_disconnect, on_fallback, subscribe_message, web_socket,
6+
web_socket_gateway,
7+
};
8+
9+
use crate::database::{Database, DatabaseConfig};
10+
mod database;
11+
12+
#[controller("/ohno")]
13+
struct AppController {}
14+
15+
#[routes]
16+
impl AppController {
17+
#[get("/test")]
18+
async fn get_data(&self, _req: Request) -> HttpResponse {
19+
let data = vec![
20+
"This is a basic web server",
21+
"It serves static data",
22+
"You can extend it with more routes",
23+
];
24+
25+
HttpResponse::Ok().data(data)
26+
}
27+
}
28+
29+
#[web_socket_gateway]
30+
struct SocketController {
31+
db: Arc<Database>,
32+
}
33+
34+
#[web_socket_gateway]
35+
struct OtherSocketController {}
36+
37+
#[web_socket("/other_socket")]
38+
impl OtherSocketController {
39+
#[on_connection]
40+
async fn on_connect(&self, _socket: SocketRef) {
41+
println!("New client connected to OtherSocketController");
42+
}
43+
}
44+
45+
#[web_socket("/socket")]
46+
impl SocketController {
47+
#[on_connection]
48+
async fn on_connect(&self, _socket: SocketRef) {
49+
println!("New client connected");
50+
}
51+
52+
#[subscribe_message("message")]
53+
async fn on_message(&self, _socket: SocketRef, Data(_data): Data<Value>) {
54+
println!("New message received");
55+
56+
let now = sqlx::query!("SELECT NOW() as now")
57+
.fetch_one(self.db.get_pool())
58+
.await
59+
.expect("Oh no");
60+
61+
println!("Database time: {:?}", now.now);
62+
}
63+
64+
#[subscribe_message("message2")]
65+
async fn other_message(&self, _socket: SocketRef, Data(_data): Data<Value>) {
66+
println!("Other message received");
67+
}
68+
69+
#[subscribe_message("message-with-ack")]
70+
async fn message_with_ack(
71+
&self,
72+
Event(_event): Event,
73+
Data(_data): Data<Value>,
74+
ack: AckSender,
75+
) {
76+
println!("Message with ack received");
77+
let response = Value::from("Acknowledged!");
78+
ack.send(&response).ok();
79+
}
80+
81+
#[subscribe_message("message-with-event")]
82+
async fn message_with_event(
83+
&self,
84+
Event(event): Event,
85+
Data(data): Data<Value>,
86+
) {
87+
println!("Message with event '{}' and data: {:?}", event, data);
88+
}
89+
90+
#[subscribe_message("another-message")]
91+
async fn and_another_one_message(&self, _socket: SocketRef, ack: AckSender) {
92+
println!("Another message received");
93+
94+
ack.send("response for another-message").ok();
95+
}
96+
97+
#[subscribe_message("just-another-message")]
98+
async fn just_another_message(&self) {
99+
println!("Message with just-another-message received");
100+
}
101+
102+
#[on_disconnect]
103+
async fn on_disconnect(&self, _socket: SocketRef) {
104+
println!("Socket disconnected");
105+
}
106+
107+
#[on_fallback]
108+
async fn on_fallback(&self, Event(event): Event, Data(data): Data<Value>) {
109+
println!(
110+
"Fallback handler invoked for event: {} with data: {:?}",
111+
event, data
112+
);
113+
}
114+
}
115+
116+
#[sword::main]
117+
async fn main() {
118+
dotenv().ok();
119+
120+
let app = Application::builder();
121+
let db_config = app.config::<DatabaseConfig>().unwrap();
122+
let db = Database::new(db_config).await;
123+
124+
let container = DependencyContainer::builder().register_provider(db).build();
125+
126+
let app = Application::builder()
127+
.with_dependency_container(container)
128+
.with_socket::<OtherSocketController>()
129+
.with_socket::<SocketController>()
130+
.with_controller::<AppController>()
131+
.build();
132+
133+
app.run().await;
134+
}

‎sword-macros/src/lib.rs‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ mod middlewares;
2828

2929
mod injectable;
3030

31+
mod websocket;
32+
3133
/// Defines a handler for HTTP GET requests.
3234
/// This macro should be used inside an `impl` block of a struct annotated with the `#[controller]` macro.
3335
///
@@ -643,3 +645,146 @@ pub fn main(_args: TokenStream, item: TokenStream) -> TokenStream {
643645

644646
output.into()
645647
}
648+
649+
/// Marks a struct as a WebSocket gateway controller.
650+
/// This macro should be used in combination with the `#[web_socket]` macro for handler implementation.
651+
///
652+
/// ### Usage
653+
/// ```rust,ignore
654+
/// #[web_socket_gateway]
655+
/// struct ChatSocket;
656+
///
657+
/// #[web_socket("/chat")]
658+
/// impl ChatSocket {
659+
/// #[on_connection]
660+
/// async fn on_connect(&self, socket: SocketRef) {
661+
/// println!("Client connected");
662+
/// }
663+
/// }
664+
/// ```
665+
#[proc_macro_attribute]
666+
pub fn web_socket_gateway(attr: TokenStream, item: TokenStream) -> TokenStream {
667+
websocket::expand_websocket_gateway(attr, item)
668+
}
669+
670+
/// Defines WebSocket handlers for a struct.
671+
/// This macro should be used inside an `impl` block of a struct annotated with the `#[web_socket_gateway]` macro.
672+
///
673+
/// ### Parameters
674+
/// - `path`: The path for the WebSocket endpoint, e.g., `"/socket"`
675+
///
676+
/// ### Usage
677+
/// ```rust,ignore
678+
/// #[web_socket_gateway]
679+
/// struct SocketController;
680+
///
681+
/// #[web_socket("/socket")]
682+
/// impl SocketController {
683+
/// #[on_connection]
684+
/// async fn on_connect(&self, socket: SocketRef) {
685+
/// println!("Client connected");
686+
/// }
687+
///
688+
/// #[subscribe_message("message")]
689+
/// async fn on_message(&self, socket: SocketRef, Data(msg): Data<String>) {
690+
/// println!("Received: {}", msg);
691+
/// }
692+
///
693+
/// #[on_disconnect]
694+
/// async fn on_disconnect(&self, socket: WebSocket) {
695+
/// println!("Client disconnected");
696+
/// }
697+
/// }
698+
/// ```
699+
#[proc_macro_attribute]
700+
pub fn web_socket(attr: TokenStream, item: TokenStream) -> TokenStream {
701+
websocket::expand_websocket(attr, item)
702+
}
703+
704+
/// Marks a method as a WebSocket connection handler.
705+
/// This method will be called when a client establishes a WebSocket connection.
706+
///
707+
/// ### Parameters
708+
/// The handler receives a `SocketRef` parameter for interacting with the connected client.
709+
///
710+
/// ### Usage
711+
/// ```rust,ignore
712+
/// #[on_connection]
713+
/// async fn on_connect(&self, socket: SocketRef) {
714+
/// println!("Client connected: {}", socket.id);
715+
/// }
716+
/// ```
717+
#[proc_macro_attribute]
718+
pub fn on_connection(attr: TokenStream, item: TokenStream) -> TokenStream {
719+
let _ = attr;
720+
item
721+
}
722+
723+
/// Marks a method as a WebSocket disconnection handler.
724+
/// This method will be called when a client disconnects from the WebSocket.
725+
///
726+
/// ### Parameters
727+
/// The handler receives a `WebSocket` parameter with the disconnected client's information.
728+
///
729+
/// ### Usage
730+
/// ```rust,ignore
731+
/// #[on_disconnect]
732+
/// async fn on_disconnect(&self, socket: WebSocket) {
733+
/// println!("Client disconnected: {}", socket.id());
734+
/// }
735+
/// ```
736+
#[proc_macro_attribute]
737+
pub fn on_disconnect(attr: TokenStream, item: TokenStream) -> TokenStream {
738+
let _ = attr;
739+
item
740+
}
741+
742+
/// Marks a method as a WebSocket message handler.
743+
/// This method will be called when the client emits an event with the specified message type.
744+
///
745+
/// ### Parameters
746+
/// - `message_type`: The name of the event to handle, e.g., `"message"` or `"*"` for any event
747+
///
748+
/// ### Parameters in handler
749+
/// - `socket: SocketRef` - The connected client's socket
750+
/// - `Data(data): Data<T>` - The message payload deserialized to type T
751+
/// - `ack: AckSender` (optional) - For sending acknowledgments back to the client
752+
///
753+
/// ### Usage
754+
/// ```rust,ignore
755+
/// #[subscribe_message("message")]
756+
/// async fn on_message(&self, socket: SocketRef, Data(msg): Data<String>) {
757+
/// println!("Received: {}", msg);
758+
/// }
759+
///
760+
/// #[subscribe_message("request")]
761+
/// async fn on_request(&self, Data(req): Data<Request>, ack: AckSender) {
762+
/// ack.send("response").ok();
763+
/// }
764+
/// ```
765+
#[proc_macro_attribute]
766+
pub fn subscribe_message(attr: TokenStream, item: TokenStream) -> TokenStream {
767+
let _ = attr;
768+
item
769+
}
770+
771+
/// Marks a method as a WebSocket fallback handler.
772+
/// This method will be called for any event that doesn't match a specific `#[subscribe_message]` handler.
773+
/// It's useful for debugging or handling dynamic events.
774+
///
775+
/// ### Parameters in handler
776+
/// - `Event(name): Event` - The event name
777+
/// - `Data(data): Data<T>` - The message payload
778+
///
779+
/// ### Usage
780+
/// ```rust,ignore
781+
/// #[on_fallback]
782+
/// async fn on_fallback(&self, Event(event): Event, Data(data): Data<Value>) {
783+
/// println!("Unhandled event: {} with data: {:?}", event, data);
784+
/// }
785+
/// ```
786+
#[proc_macro_attribute]
787+
pub fn on_fallback(attr: TokenStream, item: TokenStream) -> TokenStream {
788+
let _ = attr;
789+
item
790+
}

0 commit comments

Comments
 (0)