Skip to content

Commit

Permalink
save docs
Browse files Browse the repository at this point in the history
  • Loading branch information
levkk committed Oct 17, 2024
1 parent d8c0bf0 commit ee4e6d0
Show file tree
Hide file tree
Showing 13 changed files with 313 additions and 36 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

96 changes: 96 additions & 0 deletions docs/docs/controllers/index.md
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
```
1 change: 1 addition & 0 deletions docs/docs/controllers/request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Requests
1 change: 1 addition & 0 deletions docs/docs/controllers/response.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Responses
120 changes: 120 additions & 0 deletions docs/docs/controllers/websockets.md
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.
16 changes: 5 additions & 11 deletions docs/docs/models/create-records.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,12 @@ If the record matching the `INSERT` statement exists already, Rwf supports retur
.await?;
```
=== "SQL"
This executes _up to_ two queries, starting with:

```postgresql
SELECT * FROM "users" WHERE "email" = $1
SELECT * FROM "users" WHERE "email" = $1;
INSERT INTO "users" ("email") VALUES ($1) RETURNING *;
```

If a row is returned, no more queries are executed. However, if no rows matching the condition exist,
an `INSERT` query is executed:

```postgresql
INSERT INTO "users" ("email") VALUES ($1) RETURNING *
```
This executes _up to_ two queries, starting with a `SELECT` to see if a row already exists, and if it doesn't, an `INSERT` to create it.

### Combining with a unique constraint

Expand All @@ -130,7 +124,7 @@ it's possible to combine `unque_by` with `find_or_create_by` executed inside a s
=== "SQL"
A transaction is started explicitely:
```postgresql
BEGIN;
BEGIN
```

Afterwards, the ORM attempts to find a record matching the columns
Expand All @@ -153,5 +147,5 @@ it's possible to combine `unque_by` with `find_or_create_by` executed inside a s
Finally, the transaction is committed to the database:

```postgresql
COMMIT;
COMMIT
```
27 changes: 27 additions & 0 deletions docs/docs/models/fetch-records.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,30 @@ in the query, by specifying them using the `order` method:
```postgresql
SELECT * FROM "users" ORDER BY "email", "id" DESC
```

## Locking rows

In busy production applications, it's common for the same row to be accessed from multiple places at the same time. If you'd like to prevent that row from being
accessed while you're doing something to it, for example updating it with new values, you can use a row-level lock:

=== "Rust"
```rust
let transaction = Pool::transaction().await?;

let user = User::find(15)
.lock()
.fetch(&mut transaction)
.await?;

transaction.commit().await?;
```
=== "SQL"
```
BEGIN;
SELECT * FRON "users" WHERE "id" = $1 FOR UPDATE;
COMMIT;
```


The lock on the row(s) returned by a query last only for the duration of the transaction. It's common to use that time to update multiple tables that have some kind of
relationship to the row being locked. This mechanism allows to perform atomic operations (all or nothing) in a concurrent environment without data races or inconsistencies.
13 changes: 7 additions & 6 deletions docs/docs/models/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ struct User {
}
```

The same table in the database be created with this query:
The same table in the database can be created with this query[^1]:

```postgresql
CREATE TABLE users (
Expand All @@ -46,16 +46,17 @@ CREATE TABLE users (
);
```

### Naming conventions
The struct fields have the same name as the database columns, and the data types match the conversions associated Rust types. A row in a database
table containing the data for a model is called a record.

!!! note
The `id` column is using an optional Rust `i64` integer. This is because the struct will be used
for both inserting and selecting data from the table. When inserting, the `id` column should be `None` and will be automatically
assigned by the database. This ensures that all rows in your tables have a unique primary key.

The `macros::Model` macro automatically implements the database to Rust and vice versa types conversion
[^1]: See [migrations](migrations) to learn how to create tables in your database in a reliable way.

### Naming conventions
The struct fields have the same name as the database columns, and the data types match their respective Rust types. The table name in the database corresponds to the name of the struct, lowercase and pluralized. For example, `User` model will refer to the `"users"` table in the database.

A row in a database table which contains model data is called a record. The `macros::Model` macro automatically implements the database to Rust and vice versa types conversion
and maps the column values to the struct fields.

## Query data
Expand Down
26 changes: 23 additions & 3 deletions docs/docs/models/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,25 @@ cargo install rwf-cli

The CLI should be available globally. You can check if it's working correctly by running:

```
rwf-cli --help
```
=== "Command"
```
rwf-cli --help
```
=== "Output"
```
Rust Web Framework CLI

Usage: rwf-cli <COMMAND>

Commands:
migrate Manage migrations
setup Setup the project for Rwf
help Print this message or the help of the given subcommand(s)

Options:
-h, --help Print help
-V, --version Print version
```

## Run migrations

Expand Down Expand Up @@ -42,6 +58,8 @@ migration, run the following command:
created "migrations/1729119889028371278_unnamed.down.sql"
```

Migrations are placed inside the `<PROJECT_ROOT>/migrations` folder. If this folder doesn't exist, `rwf-cli` will create one automatically.

The migration name is optional, and by default the migration will be "unnamed", but it's nice to name it something recognizable, to help others
working on the project (and the future you) to know what's being changed.

Expand Down Expand Up @@ -104,3 +122,5 @@ In local development, it's sometimes useful to delete everything in your databas

!!! warning
Running `rwf-cli migrate flush` will delete all your data. Never run this command in production.
To protect against accidental misuse, the command will not do anything unless a `--yes` flag is
passed to it.
2 changes: 1 addition & 1 deletion docs/docs/models/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ For other values like column names, Rwf escapes them in order to avoid modifying
```
=== "SQL"
```postgresql
SELECT * FROM "users" WHERE """;DROP TABLE users;" = 5;
SELECT * FROM "users" WHERE """;DROP TABLE users;" = $1;
```
=== "Error"
```
Expand Down
Loading

0 comments on commit ee4e6d0

Please sign in to comment.