4 releases (breaking)

new 0.4.0 Jan 9, 2025
0.3.0 Jan 5, 2025
0.2.0 Dec 31, 2024
0.1.0 Dec 30, 2024

#131 in Programming languages

Download history 214/week @ 2024-12-28 198/week @ 2025-01-04

412 downloads per month

MIT license

330KB
9K SLoC

Cellang

Cellang is an implementation of the CEL language interpreter in Rust.

Motivation

Motivation behind this project is to provide a way to evaluate CEL expressions in Rust, while allowing easier way to provide custom functions. This project is built for BountyHub project, but is open-source and can be used by anyone.

There is a great rust project called CEL Interpreter which I initially used.

However, I found that the project is not flexible enough for my needs. I needed to be able to:

  • Inspect the AST of the program during validations
  • Add slightly more complex functions on types.

Therefore, the library exposes lower-level primitives that would allow you to do that.

Getting started

This library aims to be as simple as possible to use. You build up an environment, and then you evaluate the expression with it.

The environment is built using environment builder. The reason is that you can mutate it. Once the environment is done, you can build() it. Build takes the reference to the builder, so it is tied to it.

Let's show more complicated example (user_role). Check-out the examples directory for more examples, or consider contributing one!

use std::sync::Arc;

use cellang::{Environment, EnvironmentBuilder, TokenTree, Value};
use miette::Error;
use serde::{Deserialize, Serialize};

fn main() {
    // Creates a root environment
    let mut env = EnvironmentBuilder::default();

    // Fetches the required variables from the database
    let users = list_users().unwrap();

    // Adds the users to the environment
    env.set_variable("users", users).unwrap();

    // Add a custom function to the environment
    env.set_function("has_role", Arc::new(has_role));

    // Let's say the program tries to get the number of users with particular role
    let program = "size(users.filter(u, u.has_role(role)))";

    // Now, we want to calculate users with role 'admin'
    env.set_variable("role", "admin").unwrap();

    // Get number of admin users
    let n: i64 = cellang::eval(&env.build(), program)
        .expect("Failed to evaluate the expression")
        .into();

    println!("Number of admin users: {}", n);

    // Or role 'user'
    env.set_variable("role", "user").unwrap();

    // Get number of admin users
    let n: i64 = cellang::eval(&env.build(), program)
        .expect("Failed to evaluate the expression")
        .into();

    println!("Number of users: {}", n);
}

#[derive(Debug, Serialize, Deserialize)]
pub struct User {
    pub name: String,
    pub roles: Vec<String>,
}

fn list_users() -> Result<Vec<User>, Error> {
    Ok(vec![
        User {
            name: "Alice".into(),
            roles: vec!["admin".into()],
        },
        User {
            name: "Bob".into(),
            roles: vec!["user".into()],
        },
        User {
            name: "Charlie".into(),
            roles: vec!["admin".into(), "user".into()],
        },
        User {
            name: "David".into(),
            roles: vec!["user".into()],
        },
    ])
}

fn has_role(env: &Environment, tokens: &[TokenTree]) -> Result<Value, Error> {
    if tokens.len() != 2 {
        miette::bail!("Expected 2 arguments, got {}", tokens.len());
    }

    let user: User = match cellang::eval_ast(env, &tokens[0])?.to_value()? {
        Value::Map(m) => m.try_into()?,
        _ => miette::bail!("Expected a map, got something else"),
    };

    let role = match cellang::eval_ast(env, &tokens[1])?.to_value()? {
        Value::String(s) => s,
        _ => miette::bail!("Expected a string, got something else"),
    };

    Ok(user.roles.contains(&role).into())
}

Dependencies

~5–6.5MB
~118K SLoC