UNPKG

@bufbuild/protovalidate

Version:

Protocol Buffer Validation for ECMAScript

247 lines (246 loc) 8.9 kB
"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; }