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.