【免费下载链接】cel-rust
Common Expression Language interpreter written in Rust
cel-rust 是用 Rust 编写的 Common Expression Language(CEL)解释器,而Valtrait 正是它值系统的核心。本文将带你完整掌握如何在 cel-rust 中实现自定义类型:从最小可用的Val实现,到Env::add_type类型注册、as_*运算符能力、Indexer字段访问与错误处理,让你把自己的 Rust 结构体变成 CEL 表达式中可计算的值。
一、Val trait 是什么:cel-rust 值系统的入口 🧩
在 cel-rust 中,任何想在 CEL 表达式里"被求值"的 Rust 类型,都必须实现Valtrait。可以把它理解为一套"能力声明协议":
- 身份:
get_type()/cel_type()告诉解释器这个值属于哪个 CEL 类型; - 拷贝:
clone_as_boxed()让解释器能把引用变成独立拥有的值; - 相等:
equals()决定==/!=的结果(默认返回false,即任何值都不等于自身,务必实现); - 能力:一组
as_adder()、as_comparer()、as_indexer()等访问器,默认返回None。
关键设计:解释器不关心你的类型是什么,它只"询问"值的能力。表达式a + b会先调用a.as_adder(),若返回Some(self),再把b交给Adder::add去计算;若返回None,表达式直接报错。因此"支持哪些运算符"完全由你自己决定,与类型名无关。
二、最小可用示例:三步让 Rust 结构体进入 CEL
下面以一个 IP 地址类型为例(完整可运行版本见 value.rs 的文档示例):
use cel::common::types::Type; use cel::common::value::{StaticVal, Val}; use std::any::Any; static IP: Type = Type::new_unspecified_type("ip"); #[derive(Debug)] struct Ip(u32); impl Val for Ip { fn get_type(&self) -> &Type { Self::cel_type() } fn cel_type() -> &'static Type { &IP } fn equals(&self, other: &dyn Val) -> bool { other.downcast_ref::<Ip>().is_some_and(|o| o.0 == self.0) } fn clone_as_boxed<'v>(&self) -> Box<dyn Val + 'v> { Box::new(Ip(self.0)) } fn as_any(&self) -> Option<&dyn Any> { Some(self) } } impl StaticVal for Ip {}三个要点:
- 类型名用
static常量:Type::new_unspecified_type("ip")需要一个&'static str,这也是类型构造函数的约束; as_any返回Some(self):这让downcast_ref::<Ip>()能把dyn Val还原成你的具体类型;- 实现
StaticVal空标记:它承诺你的类型是'static且实现了as_any,是后续用add_overload!声明函数重载时的必备条件。
三、注册自定义类型:Env::add_type 的正确姿势
值本身不需要注册就能被求值,但注册类型之后,表达式才能按名字引用它,例如type(addr) == ip这类类型判断。
let mut env = Env::stdlib(); env.add_type(IP.to_owned()).unwrap(); let program = env.compile("type(addr) == ip").unwrap();相关 API 位于 Env::add_type:
- 名称冲突会返回
DeclarationError::TypeConflict(重复注册同一个类型是允许的); - 若想让 CEL 里能直接写
MyStruct{field: 1}这样的结构体字面量,请用StructDef并配合add_field注册字段定义; - 把值传给表达式用 Context::add_variable_as_val:
context.add_variable_as_val("addr", Box::new(Ip(0x7f000001)))。更多变量用法可参考示例 example/src/variables.rs。
四、为自定义类型添加运算符:as_* 能力访问器一览表 🔍
想让MyPoint + MyPoint或a < b在表达式里工作?实现对应能力 trait(都在 common/traits.rs 中),然后在Val里让对应的as_*返回Some(self):
| CEL 表达式 | Val 访问器 | 能力 trait |
|---|---|---|
a + b | as_adder | Adder::add |
a - b | as_subtractor | Subtractor::sub |
a * b | as_multiplier | Multiplier::mul |
a / b | as_divider | Divider::div |
a % b | as_modder | Modder::modulo |
-a | as_negator | Negator::negate |
a < b等排序比较 | as_comparer | Comparer::compare |
x in a(注意在右侧) | as_container | Container::contains |
a[i]、a.f、has(a.f) | as_indexer | Indexer::get |
a.all(...)等推演 | as_iterable | Iterable::iter |
size(a)(list/map) | as_sizer | Sizer::size |
例如让两个点相加,只需实现Adder,并在Val中重写:
fn as_adder<'b, 'v>(&'b self) -> Option<&'b (dyn Adder + 'v)> { Some(self) }⚠️ 注意两个细节:
- 右操作数自己决定接受类型:在
add里用rhs.downcast_ref::<Point>()判断,不匹配就返回错误; - 生命周期
'v:产生值的方法签名带'b/'v两个生命周期,允许结果借用操作数的数据(如返回字段引用)而不是强制拷贝,返回CowVal<'b, 'v>即可。
五、实现字段访问与 has() 检查:Indexer trait
对point.x、has(point.z)这类字段访问,实现Indexer并在as_indexer中返回Some(self):
impl Indexer for Point { fn get<'b, 'v>(&'b self, field: &dyn Val) -> Result<CowVal<'b, 'v>, ExecutionError> { match field.downcast_ref::<CelString>().map(CelString::inner) { Some("x") => Ok(CowVal::Borrowed(&self.x)), Some("y") => Ok(CowVal::Borrowed(&self.y)), _ => Err(ExecutionError::no_such_key("field")), } } fn steal<'v>(self: Box<Self>, field: &dyn Val) -> Result<Box<dyn Val + 'v>, ExecutionError> { self.get(field).map(CowVal::into_owned) } }这套写法有一个"免费午餐":当get对不存在的字段返回no_such_key错误时,has(p.z)会自动得到false而不是报错——这正是官方文档示例验证的行为(见 traits.rs 的 Indexer 示例)。另外steal只用于无人引用的内置容器,应用类型直接复用get即可。
六、相等性与错误处理最佳实践
equals必须对称:解释器只对左操作数调用它,所以若你的类型要与另一种类型相等,两边都要实现(参照内置类型1 == 1.0的双向实现);- 错误的标准姿势:不支持的右操作数,按语义返回
ExecutionError::UnsupportedBinaryOperator、ValuesNotComparable或NoSuchOverload。对比较、in、索引和字段访问,解释器会把这些错误统一报告为带运算符名的NoSuchOverload(与 cel-go 行为一致,见 traits.rs 模块说明); - 不要漏掉
Debug + Send + Sync:它们是Val的超 trait,自定义类型不满足就无法编译。
七、常见坑与排错清单 ✅
| 现象 | 原因与对策 |
|---|---|
==永远为false | 没重写equals(默认返回false) |
| 函数重载里拿不到具体类型 | 忘记as_any返回Some(self)或漏了impl StaticVal |
| 表达式里写类型名报未定义 | 类型没有通过Env::add_type注册到环境 |
运算符报NoSuchOverload | 对应的as_*仍返回默认的None |
type(x) == my_type不成立 | 值上的get_type()与注册到Env的类型常量不一致 |
小结
掌握 cel-rust 自定义类型的完整路径其实只有四步:实现Val(身份 + 拷贝 + 相等)→ 用StaticVal开启类型还原 →Env::add_type注册类型名 → 按需实现能力 trait 解锁运算符。值系统的核心源码集中在 cel/src/common/value.rs 与 cel/src/common/traits.rs,配合 Env 文档 中的结构体定义示例,足以覆盖绝大多数业务定制场景。
【免费下载链接】cel-rust
Common Expression Language interpreter written in Rust
相关推荐
cel-rust 类型系统深入剖析:Val trait 与 CowVal 零拷贝借用机制
cel rust 类型系统深入剖析:Val trait 与 CowVal 零拷贝借用机制 cel rust https://link.gitcode.com/i
如何自定义Booster转换器:面向开发者的高级配置指南
如何自定义Booster转换器:面向开发者的高级配置指南 🚀 Booster作为移动应用优化工具,其核心能力之一就是通过自定义转换器实现对字节码的灵活处理。本
移动开发开发工具构建工具质量保障性能剖析mall-app-web部署与发布指南:从开发环境到生产环境的完整流程
mall app web部署与发布指南:从开发环境到生产环境的完整流程 mall app web是一个基于uni app+Vue3实现的电商移动端项目,涵盖首页
前端移动开发小程序电商
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考