Custom Api Server

The CustomApi trait defines three associated types, Request, Response, and Error. A backend "dispatches" Requests and expects a Result<Response, Error> in return.

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

This example defines a Request and a Response type, but uses BonsaiDb's Infallible type for the error:

#[derive(Serialize, Deserialize, Actionable, Debug)]
#[actionable(actionable = bonsaidb::core::actionable)]
pub enum Request {
    #[actionable(protection = "none")]
    Ping,
    #[actionable(protection = "simple")]
    DoSomethingSimple { some_argument: u32 },
    #[actionable(protection = "custom")]
    DoSomethingCustom { some_argument: u32 },
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Response {
    Pong,
    DidSomething,
}

impl CustomApi for ExampleApi {
    type Request = Request;
    type Response = Response;
    type Error = Infallible;
}

To implement the server, we must first implement a custom Backend that ties the server to the CustomApi. We also must define a CustomApiDispatcher, which gives an opportunity for the dispatcher to gain access to the ConnectedClient and/or `CustomServer instances if they are needed to handle requests.

Finally, either Dispatcher must be implemented manually or actionable can be used to derive an implementation that uses individual traits to handle each request. The example uses actionable:

impl Backend for ExampleBackend {
    type CustomApi = ExampleApi;
    type CustomApiDispatcher = ExampleDispatcher;
    type ClientData = ();
}

/// Dispatches Requests and returns Responses.
#[derive(Debug, Dispatcher)]
#[dispatcher(input = Request, actionable = bonsaidb::core::actionable)]
pub struct ExampleDispatcher {
    // While this example doesn't use the server reference, this is how a custom
    // API can gain access to the running server to perform database operations
    // within the handlers. The `ConnectedClient` can also be cloned and stored
    // in the dispatcher if handlers need to interact with clients outside of a
    // simple Request/Response exchange.
    _server: CustomServer<ExampleBackend>,
}

impl CustomApiDispatcher<ExampleBackend> for ExampleDispatcher {
    fn new(
        server: &CustomServer<ExampleBackend>,
        _client: &ConnectedClient<ExampleBackend>,
    ) -> Self {
        Self {
            _server: server.clone(),
        }
    }
}

#[async_trait]
impl RequestDispatcher for ExampleDispatcher {
    type Output = Response;
    type Error = BackendError<Infallible>;
}

/// 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 PingHandler for ExampleDispatcher {
    async fn handle(
        &self,
        _permissions: &Permissions,
    ) -> Result<Response, BackendError<Infallible>> {
        Ok(Response::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: &Client<ExampleApi>,
    client_name: &str,
) -> Result<(), bonsaidb::core::Error> {
    match client.send_api_request(Request::Ping).await {
        Ok(Response::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 was defined with protection = "none" which skips all permission validation. However, DoSomethingSimple uses the "simple" protection model, and DoSomethingCustom uses the "custom" protection model. The comments in the example below should help explain the rationale:

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

/// With `protection = "simple"`, `actionable` will generate a trait that allows
/// you to return a `ResourceName` and an `Action`, and the handler will
/// automatically confirm that the connected user has been granted the ability
/// to perform `Action` against `ResourceName`.
#[async_trait]
impl DoSomethingSimpleHandler for ExampleDispatcher {
    type Action = ExampleActions;

    async fn resource_name<'a>(
        &'a self,
        _some_argument: &'a u32,
    ) -> Result<ResourceName<'a>, BackendError<Infallible>> {
        Ok(ResourceName::named("example"))
    }

    fn action() -> Self::Action {
        ExampleActions::DoSomethingSimple
    }

    async fn handle_protected(
        &self,
        _permissions: &Permissions,
        _some_argument: u32,
    ) -> Result<Response, BackendError<Infallible>> {
        // The permissions have already been checked.
        Ok(Response::DidSomething)
    }
}

/// With `protection = "custom"`, `actionable` will generate a trait with two
/// functions: one to verify the permissions are valid, and one to do the
/// protected action. This is useful if there are multiple actions or resource
/// names that need to be checked, or if permissions change based on the
/// arguments passed.
#[async_trait]
impl DoSomethingCustomHandler for ExampleDispatcher {
    async fn verify_permissions(
        &self,
        permissions: &Permissions,
        some_argument: &u32,
    ) -> Result<(), BackendError<Infallible>> {
        if *some_argument == 42 {
            Ok(())
        } else {
            permissions.check(
                ResourceName::named("example"),
                &ExampleActions::DoSomethingCustom,
            )?;

            Ok(())
        }
    }

    async fn handle_protected(
        &self,
        _permissions: &Permissions,
        _some_argument: u32,
    ) -> Result<Response, BackendError<Infallible>> {
        // `verify_permissions` has already been executed, so no permissions
        // logic needs to live here.
        Ok(Response::DidSomething)
    }
}

This example uses authenticated_permissions to grant access to ExampleAction::DoSomethingSimple and ExampleAction::DoSomethingCustom to all users who have logged in:

    let server = CustomServer::<ExampleBackend>::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(
                Statement::for_any()
                    .allowing(&ExampleActions::DoSomethingSimple)
                    .allowing(&ExampleActions::DoSomethingCustom),
            )),
    )
    .await?;

For more information on managing permissions, see Administration/Permissions