@bufbuild/protovalidate
Version:
Protocol Buffer Validation for ECMAScript
247 lines (246 loc) • 8.9 kB
JavaScript
"use strict";
// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.EvalStandardRulesCel = exports.EvalExtendedRulesCel = exports.EvalCustomCel = exports.CelManager = void 0;
const protobuf_1 = require("@bufbuild/protobuf");
const wkt_1 = require("@bufbuild/protobuf/wkt");
const cel_1 = require("@bufbuild/cel");
const validate_pb_js_1 = require("./gen/buf/validate/validate_pb.js");
const error_js_1 = require("./error.js");
const rules_js_1 = require("./rules.js");
const func_js_1 = require("./func.js");
const strings_1 = require("@bufbuild/cel/ext/strings");
const reflect_1 = require("@bufbuild/protobuf/reflect");
class CelManager {
constructor(registry, regexMatcher) {
this.registry = registry;
this.rulesCache = new Map();
this.bindings = {};
this.env = (0, cel_1.celEnv)({
registry,
funcs: [...strings_1.STRINGS_EXT_FUNCS, ...(0, func_js_1.createCustomFuncions)(regexMatcher)],
});
this.bindings.now = (0, wkt_1.timestampNow)();
}
/**
* Update the CEL variable "now" to the current time.
*/
updateCelNow() {
this.bindings.now = (0, wkt_1.timestampNow)();
}
setEnv(key, value) {
if (value === undefined) {
delete this.bindings[key];
return;
}
this.bindings[key] = value;
}
eval(compiled) {
if (compiled.kind == "compilation_error") {
throw compiled.error;
}
const rule = compiled.rule;
const result = compiled.interpretable(this.bindings);
if (typeof result == "string" || typeof result == "boolean") {
const success = typeof result == "boolean" ? result : result.length == 0;
if (success) {
return undefined;
}
// From field buf.validate.Rule.message:
// > If a non-empty message is provided, any strings resulting from the CEL
// > expression evaluation are ignored.
let message = rule.message;
if (message === "") {
message =
typeof result === "string"
? result
: `"${rule.expression}" returned false`;
}
return {
message,
ruleId: rule.id,
};
}
if ((0, cel_1.isCelError)(result)) {
throw new error_js_1.RuntimeError(result.message, { cause: result });
}
throw new error_js_1.RuntimeError(`expression ${rule.id} outputs ${typeof result}, wanted either bool or string`);
}
compileRule(ruleOrExpr) {
const rule = typeof ruleOrExpr == "string"
? (0, protobuf_1.create)(validate_pb_js_1.RuleSchema, { id: ruleOrExpr, expression: ruleOrExpr })
: ruleOrExpr;
try {
return {
kind: "interpretable",
interpretable: (0, cel_1.plan)(this.env, (0, cel_1.parse)(rule.expression)),
rule,
};
}
catch (cause) {
return {
kind: "compilation_error",
error: new error_js_1.CompilationError(`failed to compile ${rule.id}: ${String(cause)}`, { cause }),
};
}
}
compileRules(descMessage) {
let compiled = this.rulesCache.get(descMessage.typeName);
if (!compiled) {
compiled = this.compileRulesUncached(descMessage);
this.rulesCache.set(descMessage.typeName, compiled);
}
return compiled;
}
compileRulesUncached(descMessage) {
const standard = [];
const extensions = new Map();
for (const field of descMessage.fields) {
if (!(0, protobuf_1.hasOption)(field, validate_pb_js_1.predefined)) {
continue;
}
for (const rule of (0, protobuf_1.getOption)(field, validate_pb_js_1.predefined).cel) {
standard.push({
field,
rule,
compiled: this.compileRule(rule),
});
}
}
for (const ext of registryGetExtensionsFor(this.registry, descMessage)) {
if (!(0, protobuf_1.hasOption)(ext, validate_pb_js_1.predefined)) {
continue;
}
let list = extensions.get(ext.number);
if (!list) {
list = [];
extensions.set(ext.number, list);
}
for (const rule of (0, protobuf_1.getOption)(ext, validate_pb_js_1.predefined).cel) {
list.push({
ext,
rule,
compiled: this.compileRule(rule),
});
}
}
return { standard, extensions };
}
}
exports.CelManager = CelManager;
function registryGetExtensionsFor(registry, extendee) {
const result = [];
for (const type of registry) {
if (type.kind == "extension" &&
type.extendee.typeName == extendee.typeName) {
result.push(type);
}
}
return result;
}
class EvalCustomCel {
constructor(celMan, forMapKey, thisScalarType) {
this.celMan = celMan;
this.forMapKey = forMapKey;
this.thisScalarType = thisScalarType;
this.children = [];
}
add(compiled, rulePath) {
this.children.push({ compiled, rulePath });
}
eval(val, cursor) {
this.celMan.setEnv("this", reflectToCel(val, this.thisScalarType));
this.celMan.setEnv("rules", undefined);
this.celMan.setEnv("rule", undefined);
for (const child of this.children) {
const vio = this.celMan.eval(child.compiled);
if (vio) {
cursor.violate(vio.message, vio.ruleId, child.rulePath, this.forMapKey);
}
}
}
prune() {
return this.children.length == 0;
}
}
exports.EvalCustomCel = EvalCustomCel;
class EvalExtendedRulesCel {
constructor(celMan, rules, forMapKey) {
this.celMan = celMan;
this.rules = rules;
this.forMapKey = forMapKey;
this.children = [];
this.thisScalarType = (0, rules_js_1.getRuleScalarType)(rules);
}
add(compiled, rulePath, ruleValue, ruleScalarType) {
this.children.push({
compiled,
rulePath,
ruleCelValue: reflectToCel(ruleValue, ruleScalarType),
});
}
eval(val, cursor) {
this.celMan.setEnv("rules", this.rules);
this.celMan.setEnv("this", reflectToCel(val, this.thisScalarType));
for (const child of this.children) {
this.celMan.setEnv("rule", child.ruleCelValue);
const vio = this.celMan.eval(child.compiled);
if (vio) {
cursor.violate(vio.message, vio.ruleId, child.rulePath, this.forMapKey);
}
}
}
prune() {
return this.children.length == 0;
}
}
exports.EvalExtendedRulesCel = EvalExtendedRulesCel;
class EvalStandardRulesCel {
constructor(celMan, rules, forMapKey) {
this.celMan = celMan;
this.rules = rules;
this.forMapKey = forMapKey;
this.children = [];
this.thisScalarType = (0, rules_js_1.getRuleScalarType)(rules);
}
add(compiled, rulePath) {
this.children.push({ compiled, rulePath });
}
eval(val, cursor) {
this.celMan.setEnv("this", reflectToCel(val, this.thisScalarType));
this.celMan.setEnv("rules", this.rules);
this.celMan.setEnv("rule", undefined);
for (const child of this.children) {
const vio = this.celMan.eval(child.compiled);
if (vio) {
cursor.violate(vio.message, vio.ruleId, child.rulePath, this.forMapKey);
}
}
}
prune() {
return this.children.length == 0;
}
}
exports.EvalStandardRulesCel = EvalStandardRulesCel;
function reflectToCel(val, scalarType) {
// Wrappers are treated as scalars in standard rules so we let CEL handle them.
if ((0, reflect_1.isReflectMessage)(val)) {
return val;
}
if (scalarType) {
return (0, cel_1.celFromScalar)(scalarType, val);
}
return val;
}