Custom Api Server

The Api trait defines two associated types, Response, and Error. The Api type is akin to a "request" that the server receives. The server will invoke a Handler, expecting a result with the associated Response and Error types.

All code on this page comes from this example: examples/basic-server/examples/custom-api.rs.

This example shows how to derive the Api trait. Because an error type isn't specified, the derive macro will use BonsaiDb's Infallible type as the error type.

#[derive(Serialize, Deserialize, Debug, Api)]
#[api(name = "ping", response = Pong)]
pub struct Ping;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Pong;

#[derive(Serialize, Deserialize, Debug, Api)]
#[api(name = "increment", response = Counter)]
pub struct IncrementCounter {
    amount: u64,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Counter(pub u64);

To implement the server, we must define a Handler, which is invoked each time the Api type is received by the server.

/// Dispatches Requests and returns Responses.
#[derive(Debug)]
pub struct ExampleHandler;

/// The Request::Ping variant has `#[actionable(protection = "none")]`, which
/// causes `PingHandler` to be generated with a single method and no implicit
/// permission handling.
#[async_trait]
impl Handler<Ping> for ExampleHandler {
    async fn handle(_session: HandlerSession<'_>, _request: Ping) -> HandlerResult<Ping> {
        Ok(Pong)
    }
}

Finally, the client can issue the API call and receive the response, without needing any extra steps to serialize. This works regardless of whether the client is connected via QUIC or WebSockets.

async fn ping_the_server(
    client: &AsyncClient,
    client_name: &str,
) -> Result<(), bonsaidb::core::Error> {
    match client.send_api_request(&Ping).await {
        Ok(Pong) => {
            println!("Received Pong from server on {client_name}");
        }
        other => println!("Unexpected response from API call on {client_name}: {other:?}"),
    }

    Ok(())
}

Permissions

One of the strengths of using BonsaiDb's custom api functionality is the ability to tap into the permissions handling that BonsaiDb uses. The Ping request has no permissions, but let's add permission handling to our IncrementCounter API. We will do this by creating an increment_counter function that expects two parameters: a connection to the storage layer with unrestricted permissions, and a second connection to the storage layer which has been restricted to the permissions the client invoking it is authorized to perform:

/// The permissible actions that can be granted for this example api.
#[derive(Debug, Action)]
#[action(actionable = bonsaidb::core::actionable)]
pub enum ExampleActions {
    Increment,
    DoSomethingCustom,
}

pub async fn increment_counter<S: AsyncStorageConnection<Database = C>, C: AsyncKeyValue>(
    storage: &S,
    as_client: &S,
    amount: u64,
) -> Result<u64, bonsaidb::core::Error> {
    as_client.check_permission([Identifier::from("increment")], &ExampleActions::Increment)?;
    let database = storage.database::<()>("counter").await?;
    database.increment_key_by("counter", amount).await
}

#[async_trait]
impl Handler<IncrementCounter> for ExampleHandler {
    async fn handle(
        session: HandlerSession<'_>,
        request: IncrementCounter,
    ) -> HandlerResult<IncrementCounter> {
        Ok(Counter(
            increment_counter(session.server, &session.as_client, request.amount).await?,
        ))
    }
}

The Handler is provided a HandlerSession as well as the Api type, which provides all the context information needed to verify the connected client's authenticated identity and permissions. Additionally, it provides two ways to access the storage layer: with unrestricted permissions or restricted to the permissions granted to the client.

Let's finish configuring the server to allow all unauthenticated users the ability to Ping, and all authenticated users the ability to Increment the counter:

    let server = Server::open(
        ServerConfiguration::new("custom-api.bonsaidb")
            .default_permissions(Permissions::from(
                Statement::for_any()
                    .allowing(&BonsaiAction::Server(ServerAction::Connect))
                    .allowing(&BonsaiAction::Server(ServerAction::Authenticate(
                        AuthenticationMethod::PasswordHash,
                    ))),
            ))
            .authenticated_permissions(Permissions::from(vec![
                Statement::for_any().allowing(&ExampleActions::Increment)
            ]))
            .with_api::<ExampleHandler, Ping>()?
            .with_api::<ExampleHandler, IncrementCounter>()?
            .with_schema::<()>()?,
    )
    .await?;

For more information on managing permissions, see Administration/Permissions.

The full example these snippets are taken from is available in the repository.