impulse-utils 1.0.0-alpha.9

Bunch of fullstack utils
Documentation
# impulse-utils

A bunch of fullstack utils.

## MessagePack support

With `impulse-utils`, you can:

- parse MsgPack at backend with `impulse_utils::requests::MsgPackParser` extension trait (`salvo::Request`)
- send MsgPack from backend with `msgpack!(data)` macro
- parse MsgPack at client with `impulse_utils::responses::MsgPackResponse` extension trait (`reqwest::Response`)
- send MsgPack from client with `impulse_utils::requests::MsgPackRequest` extension trait (`reqwest::RequestBuilder`)

Example:

```rust
/// server-side

#[handler]
async fn change_text(req: &mut Request) -> MResult<MsgPack<FooData>> {
  let mut foo = req.parse_msgpack::<FooData>().await?;  // parse
  foo.text = String::from("Gotcha!");
  msgpack!(foo)                                         // send
}

/// client-side

async fn change_text_at_backend(old_data: &FooData) -> CResult<FooData> {
  let new_data = reqwest::Client::new()
    .post("127.0.0.1:8080/change-text")
    .msgpack(old_data)?    // send
    .send()
    .await?
    .msgpack::<FooData>()  // parse
    .await?;

  Ok(new_data)
}
```

For MsgPack ser/de, `impulse-utils` uses `rmp_serde::to_vec` and `rmp_serde::from_slice` methods.

## SIMD JSON support by `sonic-rs`

`impulse-utils` provide:

- parse JSON at backend with `impulse_utils::requests::SimdJsonParser` extension trait (`salvo::Request`)
- send JSON from backend with `json!(data)` macro

> [!NOTE]
> To enable SIMD ser/de for JSON, compile your project with compiler option `-C target-cpu=native`.

## Unified responses and error handling at the backend

In Salvo and Server Kit, you're writing backend routes by one of three ways:

```rust
/// like this...
#[handler]
async fn route(req: &mut Request, res: &mut Response) { .. }

/// or like this
#[handler]
async fn route(req: &mut Request) -> impl salvo::Writer + use<> { .. }

/// or like this
#[handler]
async fn route(req: &mut Request) -> Result<MyType, MyError> { .. }
```

Only the last one provides you a `?` way to expose server errors.

`impulse-utils` provide unified type `MResult<T>` for specifying response type while using `ServerError` for errors.

### `impulse_utils::results::MResult`

`MResult` allows you to specify these response types:

- just OK 200 empty response: `MResult<OK>` and `ok!()` at the end of a route
- plain text: `MResult<Plain>` and `plain!(text)`
- HTML from string: `MResult<Html>` and `html!(text)`
- any file: `MResult<File>` and `file_upload!(pathbuf, filename)` (supports file caching with `ETag` and `Cache-Control`)
- JSON: `MResult<Json<T>>` and `json!(object)` (implemented by `sonic-rs` with SIMD support)
- MsgPack: `MResult<MsgPack<T>>` and `msgpack!(object)`

Resulting macros are used in `TRACE` log level to show you what you're sending as a response and from what function.

Examples:

```rust
#[handler]
async fn frontend(req: &Request) -> MResult<Html> {
  let filepath = get_filepath_from_dist("index.html").await?;
  let site = tokio::fs::read_to_string(&filepath)
    .await
    .map_err(|e| ServerError::from_private(e).with_500())?;
  html!(site)
}


#[handler]
async fn json_to_msgpack(req: &mut Request, depot: &mut Depot) -> MResult<MsgPack<HelloData>> {
  let hello = req.parse_json_simd::<HelloData>().await?;
  let app_name = depot.obtain::<Setup>()?.generic_values().app_name.as_str();
  msgpack!(HelloData { text: format!("From `{}` application: {}", app_name, hello.text) })
}
```

### `impulse_utils::errors::ServerError`

`ServerError` also implements `salvo::Writer`, but it does more. Internally it contains HTTP status code to return with (`500` by default), public error message and the list of private error messages. `ServerError` is designed to hide implementation details exposed with errors by separation into two types: public errors and private errors.

`ServerError` on response converts into simple JSON:

```json
{"err":"Public error text"}
```

This is how your clients will get error messages.

If `public_msg` field is set, your server will respond with value of this field. You can set `public_msg` several times, and client will get only one:

```rust
ServerError::from_public("Bad data inside SQL")
  .with_public("Database error")
  .with_public("Internal server error, call 911")
  .with_500()
  .bail()?;

/// Client will see:
/// 
/// ```json
/// {"err":"Internal server error, call 911"}
/// ```
/// 
/// Backend logs:
/// 
/// ```
/// 2025-05-11T00:24:49.474405Z ERROR Error: `500` status code
///   Error message: "Internal server error, call 911"
///     Caused by: Database error
///     Caused by: Bad data inside SQL
/// ```
```

Example with any error type that implements `std::error::Error` trait:

```rust
verify_sign(&challenge, sign, &public_key)
  .map_err(|e| ServerError::from_private(e).with_public("Invalid sign!").with_401())?;
```

Or `Option`:

```rust
let user_data = kv
  .get::<UserData>(&key)
  .await?
  .ok_or(ServerError::from_public("This user isn't exist!").with_404())?;
```

`ServerError` is used by `MResult<T>` type and supports both Salvo and Server Kit frameworks.

## `ExplicitServerWrite` trait

This trait is designed to use only `&mut Response` on write instead of a full signature of `salvo::Writer` trait:

```rust
async fn write(self, req: &mut Request, depot: &mut Depot, res: &mut Response) { .. }
```

You can use it, for example, when you're using any reference to the depot data and responding with this data:

```rust
// Usually you responds by `HTTP 200 OK`, but sometimes need to also include some data that referring to `depot`.
// Of course, you can just `.clone()` your value, but `ExplicitServerWrite` needs no allocations compared to cloning.

#[handler]
async fn maybe_token(depot: &mut Depot, res: &mut Response) -> MResult<OK> {
  let state_ref = depot.obtain::<State>()?;
  
  if state_ref.allow_send_token && let Some(token) = &state_ref.token {
    msgpack!(token).unwrap().explicit_write(res).await;
  }

  ok!()
}
```

This trait is implemented to all exposed by `impulse-utils` response types above except `file_upload!` (internally it needs `req: &mut Request` to compare `ETag` inside headers to allow file caching).

## `impulse_utils::results::CResult` and `impulse_utils::errors::ClientError`

`CResult<T>` is a `Result<T, ClientError>`. `ClientError` was designed to be simple client error which you can just `console.log` at client-side.

Example:

```rust
let resp = reqwest::Client::new()
  .post("127.0.0.1:8080/post-data")
  .json(&data)
  .send()
  .await
  .map_err(|e| ClientError::from(e).context("Can't send request!"))?
  .error_for_status()
  .map_err(|e| ClientError::from(e).context("Server error!"))?;
```

## Redirect or collect server errors

If you have several backends and communicate between them by `reqwest`, you may be considering of usage `redirect_server_error` method on `reqwest::Response` to directly redirect any error, especially `ErrorResponse` (public part of `ServerError`):

```rust
let resp = reqwest::Client::new()
  .post("127.0.0.1:8080/post-data")
  .json(&data)
  .send()
  .await
  .redirect_server_error() // if status >= 400, it will throw `Err::<ServerError>`; if no, returns `Ok::<reqwest::Response>`
  .await?
  .json::<MyResponse>()
  .await?;
```

But, if you're using `reqwest` on the client-side, you may want to use `collect_server_error` to collect `CResult` instead of `MResult`:

```rust
let resp = reqwest::Client::new()
  .post("127.0.0.1:8080/post-data")
  .json(&data)
  .send()
  .await
  .collect_server_error() // if status >= 400, it will throw `Err::<ClientError>`; if no, returns `Ok::<reqwest::Response>`
  .await?
  .json::<MyResponse>()
  .await?;
```

> [!NOTE]
> `redirect_server_error` method is available with `reqwest` and `mresult` features.
> 
> `collect_server_error` method is available with `reqwest` and `cresult` features.