@ telorun/cel
通用表达式语言,作为一个库:读取表达式,将其保存为树,检查它,运行它, 把它编译成JavaScript,再写回来。
import { parseExpression, qualifiedCalls, rootReferences, serializeTree } from "@telorun/cel";
const expression = parseExpression("Billing.total(cart.items)", { namespaces: ["Billing"] });
expression.diagnostics; // [] — nothing it could not read
rootReferences(expression.root); // ["cart"] — what it reads from its environment
qualifiedCalls(expression.root); // one call: Billing.total, arity 1, with its range
serializeTree(expression.root); // "Billing.total(cart.items)"
答案是什么?
- 整个表达语法,变成一棵树,其每个节点都带有
[start, end]张成的空间 它涵盖的来源:int64,u-后缀uint64和double文字; single-、double-和 三引号字符串,原始字符串,b"…"和br"…"字节,每个转义CEL定义;true,false,null; 列表和映射文字;操作符优先级表;成员访问和索引访问 在他们的平原和可选的形式 (.,.?,[],[?]),反勾选之间的成员名称,以及 一个名字一个点打开(.y); 全局调用和接收方调用。 - 合格的电话。
Alias.fn(x)和obj.method(x)是相同的语法,所以唯一的 可以区分它们的是一组表示名称空间的名称。将该集合传递为namespaces和 每次调用这些名字都会变成qcall节点。一个表达式记录了它的集合 下已解决,因此消费者可以判断它是否正在查看为自己的站点解析的树。 - 错误恢复。 读书从不摔。畸形的或半类型的表达式返回树
对于读取的最长前缀,加上一个具有阻止它的范围的诊断-一个成员
还没有名字(
request.) 是一个带有空字段的select,这是一个点后的完成 的需求。 - 往返。
serializeTree将读取为相等表达式的任何树作为源写回 (treesEqual),括号是从优先顺序放置的,而不是从文本中记住的。 - 两个关于树的问题:
rootReferences-每个访问链的名字,不包括 多么好的理解啊!cel.bind绑定和排除一个命名空间;和qualifiedCalls——每 按源顺序对命名空间调用。 - 输入的限制。 100000个节点,250个嵌套级别,1000个列表元素,1000个映射条目和32
调用参数,每个都报告为普通诊断而不是异常,因此不友好的输入
行为像任何其他不可读的输入。
DEFAULT_PARSE_LIMITS是那些数字; 通过limits来 改变他们。
正在检查
import { CelEnvironment } from "@telorun/cel";
const environment = new CelEnvironment({ enableOptionalTypes: true })
.registerVariable("request", { schema: requestSchema }) // typed to full depth
.registerFunction("sha256(string): string", { hostBacked: true, throws: ["ERR_DIGEST_FAILED"] })
.registerNamespace("Billing", ["total(int, int): int"]);
const result = environment.check("request.query.limti");
result.valid; // false
result.diagnostics; // [{ code: "CEL_UNKNOWN_FIELD", message: "…(declared: limit, tags)", range: [14, 19] }]
result.calls; // which signature each call resolved to, with its metadata
environment.check("request.query.limit").typeName; // "int"
- 注册没有特权。 CEL自己的库寄存器也是如此
registerFunction, 调度键是调用的名称,形式和参数类型-而不是其返回类型。所以registerFunction("duration(string): Money")替换 图书馆的duration(string),removeFunctionsNamed("duration")呼叫它CEL_UNKNOWN_FUNCTION.clone()继承 然后发散。 - JSON模式是本机输入,完全深入 嵌套对象、元素类型、联合保存为
工会,
allOf相交,然后是参考。平面场地图 ({ fields: { columns: "map" } }) 仍然被接受,因为主机的输入必须和它要替换的东西一样浅。 - 没有一个模式说落入
dyn在沉默中。 一、#/$defs/…引用被解析为 节点所属的文档;离开文档的一个由主机自己的解析器应答, 返回已注册的类型名称 或者要在该节点的位置读取的文档。一个节点 引擎对is没有规则 报道 通过JSON指针而不是类型dyn静静地-environment.schemaReports(),因此知道模式在哪里编写的消费者可以锚定一个 诊断在那条线上。在下降时重新进入的引用是一个递归模式:一个深思熟虑的模式dyn,这样宣布。 - 命名类型不是它的基。
registerType({ name: "Money", base: "int" })给出一个自己的类型 运算符、比较、转换、成员和不变类型参数; 普通int被拒绝了 一个Money槽,Holder<string>哪里Holder<int>被通缉是CEL_TYPE_ARGUMENT_MISMATCH. - 每个判决都有一个范围,由检查员决定 - - - - - -
CEL_SYNTAX_ERROR,CEL_TYPE_ERROR,CEL_UNKNOWN_IDENTIFIER,CEL_UNKNOWN_FIELD,CEL_UNKNOWN_FUNCTION,CEL_WRONG_CALL_FORM,CEL_TYPE_ARGUMENT_MISMATCH,CEL_NULLABLE_ACCESS,CEL_INVALID_ARGUMENT,FUNCTION_UNRESOLVED/FUNCTION_ARITY_MISMATCH/FUNCTION_ARGUMENT_MISMATCH用于名称空间调用。 一个修正,当一个被提供,是整个修正的来源。 - 名称空间调用仅根据您声明的内容进行判断。
registerNamespace("Billing", […], { open: true })使未声明的名称成为类型dyn什么也没报告-它仍然是 列出的 在result.calls所以你要用自己的词汇和自己的判断来解决这个问题。和一个 声明写{ name: "total", returns: "double" }而不是{ signature }扣留 形参列表:结果是类型化的,实参的数量和类型由你来判断。都是给主人的 其分辨率或其签名语法比CEL的更丰富; 两者都不能半声明,因为 没有办法提供参数并要求忽略它们。 - 三名警卫清除了可能为空的内容 没有其他人:
?:,&&,||. - 可选库输入整个 哪里
enableOptionalTypes是:.?,[?],[?x],{?k: v},optional.of,none,ofNonZeroValue,hasValue,value,or,orValue,optMap,optFlatMap,平等优于期权,type()和类型名optional_type. - 名称不是标识符的成员将在反号之间读取 - - - - - -
request.headers.``内容类型``, typed against the schema's属性exactly as a plain member is, so a typo in a dashed or dotted key is stillCEL_UNKNOWN_FIELD '。仅其中成员为 阅读: 任何其他地方反勾是一个语法错误。 - 带点的声明是一个名称。 声明
a.b.c,或者a.b或两者兼而有之;a.b.c读取的变量 声明的名称和地图的条目,其中只有a.bIs -最长的前缀胜出。 .y针对环境的解决方案,永远不要反对一个名字的表达式绑定:它是唯一的 外部名称的拼写,其中一个理解变量共享它。convertsToString(type)回答该类型的值是否可以由 环境本身string().
标准库是data - src/signatures/standard-library.json,描述于
文档/ signature-data.md. 它是 CEL's 图书馆: CEL本身的声明
不定义进位 "spec": false 并说明为什么它在这里,等价物在哪里以及如何存在
这个成员与它不同-十九个这样做。
功能目录
在CEL自己的图书馆旁边还有第二个-Telo的方言,67个功能超过86个签名,也
数据(src/signatures/function-catalog.json):
import { CelEnvironment, functionCatalog, registerFunctionCatalog } from "@telorun/cel";
const environment = new CelEnvironment({ enableOptionalTypes: true });
registerFunctionCatalog(environment, {
handlers: { sha256: hashHex, json: writeJson, joinPath: (base, rest) => join(base, rest) },
});
environment.evaluate("formatDuration(510, 480)"); // "1d 30m"
functionCatalog(); // the one listing surface: name, signature, category, summary, flags
- 它通过公共表面注册 - - - - - -
registerFunction,每个签名一次-所以它可以 删除、按函数替换函数或按名称删除。默认环境没有它。 - 主机有九种功能:
sha256,md5,sha1,sha512,hmac,base64Encode,base64Decode,json和joinPath,每个需要这个包裹可能无法到达的设施。左一 Out仍然注册,因此只检查的消费者仍然对调用进行类型检查;评估它的答案unbound_function命名函数。 - 拒绝是目录自己的话,
<function>: <what is wrong>-从来没有图书馆的措辞。 无效模式以RE2自己的解析错误类型之一结束,之后没有任何内容,并且parseJson单词自己的偏移量 (parseJson: invalid JSON at offset 3)。 - 在检查时拒绝文字参数,不仅仅是当表达式运行时:
fixed(1.0, 11)是CEL_INVALID_ARGUMENT命名调用,因为签名限制类型和十进制计数 范围是一个值。十二个函数带有这样的保护,每个函数都运行评估的代码 运行。 - 两个家庭通过申报到达接待设施 -时钟 (
now,nowIso,today,nowMillis,nowSeconds)和UUID函数背后的随机源,这是什么deterministic: false意思是。两者都和节点一样存在于浏览器中。
评估
const program = environment.compile("request.query.limit != null ? int(request.query.limit) : 25");
program.evaluate({ request: { query: { limit: "50" } } }); // 50n
program.evaluate({ request: { query: {} } }); // 25n
- 每个运算符和函数的行为都符合cel-spec的要求。
int是一个bigint溢出是 错误而不是包装;uint和double是不同的类型;除法和模数为0 两个不同的错误;字节和字符串是不同的,都是有序的;时间戳是 纳秒-精确,它的getter接受一个区域(一个IANA名称或一个固定的HH:MM偏移量);matches是 RE2,所以主机自己的引擎会接受而RE2不会拒绝的模式。 - 持续时间是其总纳秒的有符号64位范围 - - - - - -
-9223372036.854775808s … 9223372036.854775807s大约是±292年,这是cell -spec自己的限制。 它在构建持续时间的任何地方进行检查: 转换,持续时间+和-,timestamp - timestamp的持续时间操作数timestamp ± duration。主机自己的持续时间格式可以是 更广泛的,google.protobuf.Duration是 ± 10,000年-而CEL是它的一个子范围,所以持续时间a 运输所能承载的并不总是一个CEL所能建造的。 - 八个字符串成员不是CEL的,并在各自的声明中如此表示(
"spec": false与一个 原因):indexOf,lastIndexOf和substring指数 UTF-16代码单元 而不是通过代码 点,lowerAscii和upperAscii应用主机语言的大小写而不是ASCII 单独的信件,和trim,split和join类型中的等效字符串扩展不同 他们的理由名称。size,startsWith,endsWith,contains,matches和串联是 CEL自己的. - 错误是参与短路的一个值。
false && <missing key>是false和true || <missing key>是true,从任何一方; 一个错误,幸存到一个 评估成为一个抛出CelEvaluationError携带其中一个代码CEL_EVALUATION_CODES(no_such_key,numeric_overflow,division_by_zero,modulo_by_zero,index_out_of_range,invalid_regular_expression,invalid_conversion,optional_value_missing,async_value_unsupported,…)以及它所涉及的文本范围。 - 评估是同步的,而必须等待的价值则被拒绝 它来的每一扇门
通过:激活读取,注册实现的结果,以任何形式读取的成员,以及
元素进入推导体,值
cel.bind绑定可选对象的保存值和列表 会员资格。每个答案async_value_unsupported,从来没有一个值传递,从来没有一个答案 决定一个没有触及的值: 元素在哪里被拒绝 绑定,而那个拒绝就是 终端- - - - - -xs.all(e, false)和xs.exists(e, true)在一张单子上写着承诺,而不是拒绝 回答一个可读的元素,而一个普通的no_such_key仍然短路为 之前。一个容器是故意不走的,所以size(xs)和xs + [3]随身携带它和每一个 读出元素的方法被拒绝。 - 一个值通过字符串类型的键表示它是什么 下
Symbol.for("telo.cel.value"),从来没有。 构造函数,因此引擎的两个副本同意其中任何一个生成的值。null,布尔值; 一根弦,一根number(双)bigint(int), aUint8Array(bytes),数组(list)和plain 对象 (字符串键映射) 不携带键; uint,时间戳,持续时间,类型值,可选, 带有键入键和错误键的映射。带有字符串键的普通物体是 数据,因为它必须。 - 每个读取的成员都经过一次查找 在值本身的项上
a.b,a['b'],a[expr],.?,[?]而宏的字段读起来是一样的——所以无论如何计算,没有一个键能达到a 原型,方法或函数属性。CEL地图是无原型构建的,因此__proto__,constructor和prototype作为数据的往返。 - 否
eval,没有new Function,没有磁盘。compile在运行时上构建闭包树 图书馆,这是每个操作员只住一次的地方。进程内缓存-编译的表达式 每个环境、编译模式、调用站点的解析重载都是有边界的,每个都有一个 申报能力。 evaluateToValue对于携带错误的调用方,回答错误值而不是抛出 本身。使用名称空间的调用通过namespaceFunction,每次评估提供;一个 没有什么绑定是unbound_function.
发射JavaScript
相同的表达式编译为 JavaScript源,作为一组它们的一个模块:
const module = environment.emit(["a + b * 2", "xs.map(e, e * 2)"]);
module.key; // 64 hexadecimal characters: what a host names its stored copy by
module.text; // the module's source, which imports nothing
module.header; // { format, engine, environment, key, body } — the header line's own fields
// A host loads it however it loads a module, then:
const programs = programsFromEmittedModule(loaded, module, environment.emitterRuntime());
programs[0].evaluate({ a: 3n, b: 4n }); // 11n
- 两个后端答案完全相同 -相同的值,或相同的错误代码和 相同的范围。这是布线而不是测试的属性: 每个操作员,理解, 成员读取、名称读取、bool操作数和调用站点调度是两个后端调用的一个函数,因此 两者编译的唯一不同之处在于控制如何从一个调用传递到下一个调用。
- 运行时是工厂的参数,从来没有导入。 模块的默认导出采用
运行时支持库并回答每个表达式一个同步函数,并且文本名称为no
说明符-它从
data:URL,从装载在主机下的任意位置的缓存目录 它的解析器不是Node的。导入包的模块也会接受另一个包的运行时 版本默默地,关键看不到。 - 关键在于环境,而不仅仅是源头。 它散列发射器的格式生成, 引擎版本、环境摘要和表达式源的有序列表。文摘结束了 环境的 解决清单 -每个幸存的函数签名,每个命名类型,每个 变量、每个名称空间、每个选项——所以两个环境是由不同的注册顺序构建的 是一个键,替换或删除标准功能的主机是另一个键。
- 报头在运行任何内容之前进行验证,并且它是五个字段而不是三个。
format,engine和environment是来源,并且它们对于每个模块一个引擎都是字节相同的 针对一个环境写入-因此key(模块自己的标识) 和body的摘要 标题行之后的一切) 是什么抓住了一个回答错误查找的商店,一个文本 在其标题之后被截断,并在编写后进行编辑的文本。body涵盖了integrity导出,所以不能住在它: 标题行携带所有五个,导出携带四个a加载 对象可以持有。每一个不匹配都被存储 文本 显示的是一个重新编译,将自己命名为refused; 一个加载 模块 声明什么都没有,声明另一个键或回答错误的数字 的函数被拒绝与CelEngineError(emitted_module_rejected)。 - 一个商店接缝,没有装载机。
environment.emittedModule(sources, store)读写模块 通过主机提供的接口按键进行文本; 引擎不接触文件系统,并且命中解析 什么都没有。商店的写作应该是这样的 原子的 -写在别处,然后重命名-因为几个主机 共享一个缓存根; 这就是如何避免半写入的条目,并且body消化就是人的本质 如果不是,则检测到。 - 排放是确定的: 针对相同环境的相同顺序的相同表达式是 相同的字节。
- 它是 更快,没有不同的行为 -只有一件事: 闭包称之为成本。 两个后端都在下面支付相同的共享运行时,因此表达式的成本是一个点链 搜索和转换是一个洗,而一个调用许多小操作的收益。
它故意不做什么
- 在读取时不展开任何内容。
has(x),xs.map(i, i)和cel.bind(x, 1, x)是普通的 调用树中的节点; 检查器降低它们,因此源保持可从树写入。 - 它不懂主人的词汇。 没有类型名,没有注释,没有清单字:一个主机寄存器
它自己的通过
registerType还有一个模式解析器。上面的函数编目是一组 CEL自己的类型上的函数-没有签名命名主机类型-这就是方言。 - 它在求值时从不进行类型检查。 检查员决定每一个判决和一个消费者 要求他们; 编译只拒绝根本无法编译的内容-未阅读的源代码 整体。
- 语言没有到达主机设施。 没有文件系统,没有时钟,没有网络,没有内置的Node 在浏览器中不变地运行,编目也是如此:它的时钟和UUID函数读取工具 每个主机都有,它的九个主机支持的功能本身根本无法实现。
保留字
CEL保留了21个单词,其中没有一个可以读作名称: as break const continue else false for function if import in let loop namespace null package return true var void while。17人被拒绝
彻底的; true, false 和 null 是它们所处位置的文字,并且 in 是成员运算符,
这就是为什么一个名字位置永远无法到达他们。
保留字仍然是合法的 成员 的名字, {'let': 1}.let, a.while(), a.in, a.true -哪个
是CEL自己的规则:成员在表达式的作用域中命名一个值的条目,而不是一个名称。
主人没有任何保留: __proto__, prototype 和 constructor 是普通的名字。
将主机自己的属性保留在CEL值之外是成员读取的工作,而不是单词列表的-a
成员键可以计算(a[request.query.k]),没有名单可以判断。
cel 和 optional 是普通标识符,但永远不能注册为名称空间:
标准宏写在它们上面。