Something went wrong. Try again.
A simple self-host service providing API keys for tightly scoped alterations to your atproto records blinkers.dev
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176//! CEL conditions and computed template values.
use std::fmt;use std::sync::{Arc, LazyLock};
use cel::{Context, Env, Program, Value};use chrono::{DateTime, Utc};use serde_json::Value as Json;
static ENV: LazyLock<Arc<Env>> = LazyLock::new(|| Arc::new(Env::stdlib()));
/// Per-request values every expression can see as `now` and `rkey`.pub struct EvalCtx { pub rkey: String, pub now: DateTime<Utc>,}
#[derive(Debug)]pub enum ExprError { Compile { expr: String, reason: String }, Eval { expr: String, reason: String }, NotAtprotoData { expr: String, kind: String },}
impl fmt::Display for ExprError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Compile { expr, reason } => write!(f, "cannot compile `{expr}`: {reason}"), Self::Eval { expr, reason } => write!(f, "cannot evaluate `{expr}`: {reason}"), Self::NotAtprotoData { expr, kind } => { write!( f, "`{expr}` produced a {kind}, which atproto data cannot hold" ) } } }}
impl std::error::Error for ExprError {}
fn compile(src: &str) -> Result<Program, ExprError> { ENV.compile(src).map_err(|e| ExprError::Compile { expr: src.into(), reason: e.to_string(), })}
fn base_context(ctx: &EvalCtx) -> Context<'static, 'static> { let mut cel_ctx = Context::with_env(ENV.clone()); cel_ctx .add_variable("now", cel::Timestamp(ctx.now.fixed_offset())) .expect("timestamps convert to CEL values"); cel_ctx .add_variable("rkey", &ctx.rkey) .expect("strings convert to CEL values"); cel_ctx}
/// A boolean CEL expression over `record`, `rkey` and `now`.pub struct Condition { program: Program,}
impl Condition { pub fn compile(src: &str) -> Result<Self, ExprError> { compile(src).map(|program| Self { program }) }
/// Evaluation errors and non-boolean results count as `false`. pub fn holds(&self, record: &Json, ctx: &EvalCtx) -> bool { let mut cel_ctx = base_context(ctx); cel_ctx.add_variable("record", record).is_ok() && matches!(self.program.execute(&cel_ctx), Ok(Value::Bool(true))) }}
enum Node { Literal(Json), Computed { src: String, program: Program }, List(Vec<Node>), Object(Vec<(String, Node)>),}
/// JSON in which strings written as `{% expr %}` are replaced by their CEL result.pub struct Template(Node);
impl Template { pub fn compile(value: &Json) -> Result<Self, ExprError> { compile_node(value).map(Self) }
pub fn render(&self, ctx: &EvalCtx) -> Result<Json, ExprError> { render_node(&self.0, &base_context(ctx)) }}
fn compile_node(value: &Json) -> Result<Node, ExprError> { Ok(match value { Json::String(s) => match s .strip_prefix("{%") .and_then(|rest| rest.strip_suffix("%}")) { Some(inner) => { let src = inner.trim().to_string(); Node::Computed { program: compile(&src)?, src, } } None => Node::Literal(value.clone()), }, Json::Array(items) => Node::List(items.iter().map(compile_node).collect::<Result<_, _>>()?), Json::Object(map) => Node::Object( map.iter() .map(|(k, v)| Ok((k.clone(), compile_node(v)?))) .collect::<Result<_, ExprError>>()?, ), _ => Node::Literal(value.clone()), })}
fn render_node(node: &Node, cel_ctx: &Context) -> Result<Json, ExprError> { Ok(match node { Node::Literal(v) => v.clone(), Node::Computed { src, program } => { let result = program.execute(cel_ctx).map_err(|e| ExprError::Eval { expr: src.clone(), reason: e.to_string(), })?; to_atproto_json(&result).map_err(|kind| ExprError::NotAtprotoData { expr: src.clone(), kind, })? } Node::List(items) => Json::Array( items .iter() .map(|n| render_node(n, cel_ctx)) .collect::<Result<_, _>>()?, ), Node::Object(members) => Json::Object( members .iter() .map(|(k, n)| Ok((k.clone(), render_node(n, cel_ctx)?))) .collect::<Result<_, ExprError>>()?, ), })}
/// Errs with the name of the first CEL type atproto data cannot hold.fn to_atproto_json(value: &Value) -> Result<Json, String> { Ok(match value { Value::Null => Json::Null, Value::Bool(b) => (*b).into(), Value::Int(i) => (*i).into(), Value::UInt(u) => i64::try_from(*u) .map_err(|_| value.type_of().to_string())? .into(), Value::String(s) => s.as_str().into(), Value::List(items) => Json::Array( items .iter() .map(to_atproto_json) .collect::<Result<_, _>>()?, ), Value::Map(map) => Json::Object( map.map .iter() .map(|(k, v)| Ok((k.to_string(), to_atproto_json(v)?))) .collect::<Result<_, String>>()?, ), other => return Err(other.type_of().to_string()), })}