Using Capabilities to Design Safer, More Expressive APIs

I write a lot of software that interact with databases. Especially in the case of web applications, I often find myself writing a couple of generic traits or interfaces to wrap a database connection with a generic API.


trait Model {
    fn save(&mut self) -> Result<(), APIError>;
    fn update(&mut self) -> Result<(), APIError>;
    // ...
}

When I am writing models like this, one of the most common problems I think about is how I can effectively implement access controls. My goal has been to design an API to my database that is guaranteed by my program's compiler to restrict database operations in specific contexts. By this I mean that my API should let me express with types precisely what kinds of database operations my function wants to perform. My program's compiler should then be able to use that information to tell me if I have mistakenly tried to perform an action that I would prefer to design to avoid. Users of my API (including myself) would then be able to use the same information to control how my API can interact with resources like a database.


fn leave_comment<Database>(db: &Database, comment: Comment, thread: &Thread) -> Result<(), APIError>
    where Database: CanSave<Comment>
{
    let save_data = SaveComment {
        comment,
        thread,
    };
    db.save(save_data).map(|_| ())
}

Using this imaginary CanSave trait, I can declare in my function or method's type signature that my function is going to try to save a Comment to the Database. While expressiveness is certainly a virtue in its own right, where this approach becomes interesting is in how it could prevent bugs from creeping into one's code.


trait CanSave<QueryParams> {
    type SaveResult;

    fn save(&self, QueryParams) -> Result<Self::SaveResult, APIError>
}

fn leave_comment<Database>(db: &Database, comment: Comment, mut thread: Thread) -> Result<(), APIError> 
    where Database: CanSave<Comment>
{
    // ...
    // A fair bit of code.
    // ...
    let save_data = SaveComment {
        comment,
        thread,
    };
    db.save(save_data);
    // ...
    // Some mode code.
    // ...

    // Weren't we supposed to also increment the thread's comment count?
    thread.comment_count += 1;
    db.save(thread); // ERROR: Database does not implement method `save(Thread)`.
}

Imagine a scenario in which you encounter a bug in your commenting functionality at some point. By now, there are a few things going on in this top-level function, and it might be hard to remember how you're supposed to update your models. This scenario is obviously contrived, and there are plenty of good ways we can avoid such a situation, but I find it appealing to be able to rely on my compiler to help guide the design of my API. This approach allows us to determine ahead of time what kinds of state-mutating operations our code needs to perform, and to specify them with types.


trait ____<INPUT> {
    type OUTPUT;

    fn ___(&self, INPUT) -> __<Self::OUTPUT, ERROR>;
}

If you stare at it long enough, you realize that the trait we specified earlier was really just an incredibly generic "call a function" trait. There are, however, a couple of interesting properties that this trait has, which I would like to explore and experiment with.

I've written a lot already about the concept of capability-based security and how I've interpreted it in some of my previous work. If you aren't already at least somewhat familiar with the subject, I suggest at least skimming through the Wikipedia article about it before continuing.


trait Capability<Operation> {
    type Data;
    type Error;

    fn perform(&self, Operation) -> Result<Self::Data, Self::Error>;
}

Here, I've given the trait we took apart earlier a name-- Capability--, which very simply captures the idea of "having the ability to perform an operation." An Operation is now something that we can represent as a type, and can compose using the Capability trait.


struct SQLite {
    db: Connection,
}

struct Save<T>(pub T);
struct Update<T>(pub T);

struct User {
   pub email_address: String,
   pub password_hash: String,
   pub username: String,
}

impl Capability<Save<User>> for SQLite {
    type Data = User;
    type Error = DatabaseError;

    fn perform(&self, save_user: Save<User>) -> Result<User, DatabaseError> {
        // ...
        // Execute a SQL query.
        // ...
    }
}

// For demonstrative purposes.
fn handle_user_registration<DB>(db: &DB, user: User) -> Result<User, DatabaseError>
    where DB: Capability<Save<User>>
{
    db.perform(Save(user))
}

We can now use types to specify database operations we want to perform, and similarly to provide us with an expressive way of doing database operations. That is, instead of having to write various methods with arguments ordered in a clever way or something like that, we can actually just make our APIs type-based, giving us the expressive power that struct initialization syntax has.


trait CanChangeUserData: Capability<Find<User>, Data = User, Error = DBError> + Capability<Update<User>, Data = User, Error = DBError> {}

impl CanChangeUserData for SQLite {}

// Give the capability to find users to (a) SQLite (wrapper).
impl Capability<Find<User>> for SQLite {
    type Data = User;
    type Error = DBError;

    fn perform(&self, find_user: Find<User>) -> Result<User, DBError> {
        // Execute a SQL query on a wrapped Connection.
    }
}

impl Capability<Update<User>> for SQLite {
    // Has a similar shape to the impl. for `Find<User>`.
}

We can even compose capabilities we've written to implement a new capability that encompasses multiple already-implemented capabilities.


fn change_user_password<DB>(db: &DB, user_email: String, new_pwd_hash: String) -> Result<User, DBError>
    where DB: CanChangeUserData
{
    let mut user = db.perform(Find(user_email))?;
    user.password_hash = new_pwd_hash;
    db.perform(Update(user))
}

This way, we can write functions that can request multiple capabilities without having to change any data parameters, which are more frequently relevant to callers.


macro_rules! capability {
    ($name:ident for $type:ty,
     composing $({$operations:ty, $d:ty, $e:ty}),+) => {
        trait $name: $(Capability<$operations, Data = $d, Error = $e>+)+ {}

        impl $name for $type {}
    };
}

capability!(CanChangeAndDeleteUserData for SQLite,
            composing { Save<User>,   User, DBError },
                      { Update<User>, (),   DBError },
                      { Delete<User>, (),   DBError });

I went ahead and wrote a macro to make it easier for me to write functions like change_user_password without having to write a long trait definition and empty implementation every time I want to do this kind of composition. Instead I get to write this pleasant little macro invocation!


fn render_dashboard_page<DB, Cache>(db: &DB, cache: &Cache, user_id: Id) -> Result<String, DBError>
    where DB: CanFindUser,
          Cache: CanReadAndUpdateUserDashboard
{
    if let Ok(cached_page) = cache.perform(FindUserDashboard(user_id)) {
        return Ok(cached_page);
    }
    let user = db.perform(Find(user_id))?;
    let dashboard = render_dashboard_for_user(&user);
    cache.perform(UpdateUserDashboard(user_id, dashboard))?;
    Ok(dashboard)
}

Even as we start writing more complicated functions, I find this way of specifying our dependencies is really elegant.

It's worth noting that, while I've been writing code until now that assumes that database operations will be implemented in capabiltiy implementations, this isn't strictly required to be the case. You could think of the capability implementations as another layer that you can implement on top of whatever you're already using. Thinking of it this way, we could imagine definining new types to attach specific capabilities to them, leaving us with no restrictions being imposed on how we build any other components of our software.

To summarize, here is a brief recap of some of the value we derive from building APIs on the Capability trait:

I hope that this exploratory style of post has been interesting, and that you might find value in adopting some of the ideas explored here in your own work.

If you'd like to discuss this subject, please leave a comment on the Reddit thread that I created.

Thank you for reading!