-
-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
313 additions
and
36 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
# Controller basics | ||
|
||
Rwf comes with multiple pre-build controllers that can be used out of the box, for example to handle WebSocket connections, REST-style interactions, or serving static files. For everything else, the `Controller` trait can be implemented to handle any kind of HTTP requests. | ||
|
||
## What's a controller? | ||
|
||
The controller is the **C** in MVC: it handles user interactions with the web app and performs actions on their behalf. User inputs, like forms, and requests to the web app via HTTP, is taken care of by controllers. | ||
|
||
## Writing a controller | ||
|
||
A controller is a plain Rust struct which implements the `rwf::controller::Controller` trait. For example, a simple controller which responds with the current time can be with a few simple steps. | ||
|
||
#### Import types | ||
|
||
```rust | ||
use rwf::prelude::*; | ||
``` | ||
|
||
The prelude module contains most of the types and traits necessary to work with Rwf. Including it will save you time and effort when writing code. | ||
|
||
#### Define the struct | ||
|
||
```rust | ||
#[derive(Default)] | ||
struct CurrentTime; | ||
``` | ||
|
||
A controller is any Rust struct that implements the `Controller` trait. The `Default` trait is derived automatically to provide a convenient way to instantiate it. | ||
|
||
#### Implement the `Controller` trait | ||
|
||
```rust | ||
#[async_trait] | ||
impl Controller for CurrentTime { | ||
/// This function responds to all incoming HTTP requests. | ||
async fn handle(&self, request: &Request) -> Result<Response, Error> { | ||
let time = OffsetDateTime::now_utc(); | ||
|
||
// This creates an HTTP "200 OK" response, | ||
// with "Content-Type: text/plain" header. | ||
let response = Response::new() | ||
.text(format!("The current time is: {:?}", time)); | ||
|
||
Ok(response) | ||
} | ||
} | ||
``` | ||
|
||
The `Controller` trait is asynchronous. Support for async traits in Rust is still incomplete, so we use the `async_trait` package to make it easy to use. The trait itself has a few methods, most of which have reasonable defaults. The only method | ||
that needs to be written by hand is `async fn handle()`. | ||
|
||
#### `handle` | ||
|
||
The `handle` method accepts [`rwf::http::Request`](https://docs.rs/rwf/latest/rwf/http/request/struct.Request.html) and must return [`rwf::http::Response`](https://docs.rs/rwf/latest/rwf/http/response/struct.Response.html). The response can be any valid HTTP response, including `404` or even `500`. | ||
See [Request](request) documentation for examples of requests, and [Response](response) documentation for more information on creating responses. | ||
|
||
|
||
## Connecting controllers | ||
|
||
Once you have a controller, adding it to the app requires mapping it to a route. A route is a unique URL, starting at the root of the app. For example, a route displaying all the users in our app could be `/app`, which would be handled by the `Users` controller. | ||
|
||
Adding controllers to the app happens at server startup. A serve can be launched from anywhere in the code, but typically is done so in the `main` function. | ||
|
||
```rust | ||
use rwf::prelude::*; | ||
use rwf::http::{self, Server}; | ||
|
||
#[tokio::main] | ||
async fn main() -> Result<(), http::Error> { | ||
Server::new(vec![ | ||
// Map the `/time` route to the `CurrentTime` controller. | ||
route!("/time" => CurrentTime), | ||
]) | ||
.launch("0.0.0.0:8000") | ||
.await | ||
} | ||
``` | ||
|
||
!!! note | ||
The `route!` macro is a shorthand for calling `CurrentTime::default().route("/time")` and it looks pretty. | ||
You can instantiate your controller struct in any way you need, and call the `Controller::route` method when you're ready to add it to the server. | ||
|
||
You can also implement the `Default` trait for your controller and continue to use the macro. | ||
|
||
### Test with cURL | ||
|
||
Once the server is running, you can test your endpoints with cURL (or with a regular browser, like [Firefox](https://firefox.com)): | ||
|
||
=== "cURL" | ||
```bash | ||
curl localhost:8000/time -w '\n' | ||
``` | ||
=== "Output" | ||
``` | ||
The current time is: 2024-10-17 0:23:34.6191103 +00:00:00 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Requests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Responses |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
# WebSockets | ||
|
||
Rwf comes with built-in WebSockets support, requiring no additional dependencies or configuration. | ||
|
||
## What are WebSockets? | ||
|
||
A WebSocket is a bidirectional communication protocol which allows browsers and servers | ||
to talk to each other. Unlike normal HTTP responses, | ||
which are only delivered when the client asks for them, WebSocket messages can be sent by the server at any time. | ||
|
||
This is useful for updating web apps in real-time, or sending push notifications when something important | ||
happens on the server, for example. | ||
|
||
### How do WebSockets work? | ||
|
||
A WebSocket connection is a TCP connection. It's established by sending a regular HTTP request with a special header. | ||
If the server supports WebSockets, lie Rwf does, it responds by a special response and upgrades the connection to use | ||
the WebSocket protocol instead of HTTP. | ||
|
||
WebSockets allow both client and server to send text and binary data. Rwf supports both formats. | ||
|
||
## Writing a WebSocket controller | ||
|
||
A WebSocket controller is any Rust struct that implements the | ||
[`WebsocketController`](https://docs.rs/rwf/latest/rwf/controller/trait.WebsocketController.html) trait. | ||
|
||
The trait has two methods of interest; the first one handles new WebSocket connections, and the other | ||
incoming messages from the client. | ||
|
||
```rust | ||
use rwf::controller::Websocket; | ||
use rwf::prelude::*; | ||
|
||
#[derive(Default, macros::WebsocketController)] | ||
struct Echo; | ||
|
||
#[async_trait] | ||
impl WebsocketController for Echo { | ||
/// Run some code when a new client connects to the WebScoket server. | ||
async fn handle_connection( | ||
&self, | ||
client: &SessionId, | ||
) -> Result<(), Error> { | ||
log::info!("Client {:?} connected to the echo server", client); | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Run some code when a client sends a message to the server. | ||
async fn handle_message( | ||
&self, | ||
client: &SessionId, | ||
message: Message, | ||
) -> Result<(), Error> { | ||
// Get an app-wide WebSocket channel to the client. | ||
// This will send a message to the client via WebScoket | ||
// connection from anywhere in the code. | ||
let comms = Comms::websocket(client); | ||
|
||
// Send the message back to the client (we're an echo server). | ||
comms.send(message)?; | ||
|
||
Ok(()) | ||
} | ||
} | ||
``` | ||
|
||
A few things to unpack here. The `handle_message` method is called every time a client sends a message | ||
addressed to this WebSocket controller. What to do with the message depends on the application, but if we | ||
were writing a real-time chat app, we would save it to the database and notify all interested clients of a | ||
new message. | ||
|
||
The [`Comms`](https://docs.rs/rwf/latest/rwf/comms/struct.Comms.html) struct is a global data structure that keeps track of who is connected to our server. You can use it | ||
to send a [`Message`](https://docs.rs/rwf/latest/rwf/http/websocket/enum.Message.html) to any client at any time. | ||
|
||
!!! note | ||
The `macros::WebsocketController` automatically implements the `Controller` trait. | ||
All Rwf controllers have to implement the `Controller` trait, and the `WebsocketController` is no exception. | ||
The trait automatically implements the `handle` method, however due to the nature of Rust dynamic dispatch, | ||
the `handle` method of the supertrait has to be called explicitly in the base trait. | ||
|
||
If you were not to use the macro, you could do the same thing manually: | ||
|
||
```rust | ||
#[async_trait] | ||
impl Controller for Echo { | ||
async fn handle(&self, request: &Request) -> Result<Response, Error> { | ||
WebsocketController::handle(self, request).await | ||
} | ||
} | ||
``` | ||
|
||
## Starting a WebSocket server | ||
|
||
Since WebSockets are built into Rwf, you can just add the controller to the server at startup: | ||
|
||
```rust | ||
use rwf::prelude::*; | ||
use rwf::http::{Server, self}; | ||
|
||
#[tokio::main] | ||
async fn main() -> Result<(), http::Error> { | ||
let server = Server::new(vec![ | ||
route!("/websocket" => Echo), | ||
]) | ||
.launch("0.0.0.0:8000") | ||
.await | ||
} | ||
``` | ||
|
||
### Testing the connection | ||
|
||
In a browser of your choice, open up the developer tools console and connect to the WebSocket server: | ||
|
||
```javascript | ||
const ws = new WebSocket("ws://localhost:8000/websocket"); | ||
``` | ||
|
||
If everything works, you should see a log line in the terminal where the server is running indicating a new | ||
client has joined the party. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.