@bufbuild/protovalidate
Version:
Protocol Buffer Validation for ECMAScript
235 lines (234 loc) • 12.1 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.Planner = void 0;
const protobuf_1 = require("@bufbuild/protobuf");
const wkt_1 = require("@bufbuild/protobuf/wkt");
const validate_pb_js_1 = require("./gen/buf/validate/validate_pb.js");
const reflect_1 = require("@bufbuild/protobuf/reflect");
const eval_js_1 = require("./eval.js");
const rules_js_1 = require("./rules.js");
const condition_js_1 = require("./condition.js");
const cel_js_1 = require("./cel.js");
const error_js_1 = require("./error.js");
class Planner {
constructor(celMan, legacyRequired) {
this.celMan = celMan;
this.legacyRequired = legacyRequired;
this.messageCache = new Map();
}
plan(message) {
const existing = this.messageCache.get(message);
if (existing) {
return existing;
}
const messageRules = (0, protobuf_1.getOption)(message, validate_pb_js_1.message);
const e = new eval_js_1.EvalMany();
this.messageCache.set(message, e);
e.add(this.fields(messageRules.oneof, message.fields));
e.add(this.messageCel(messageRules));
e.add(this.messageOneofs(message, messageRules.oneof));
e.add(this.oneofs(message.oneofs));
e.prune();
return e;
}
oneofs(oneofs) {
return new eval_js_1.EvalMany(...oneofs
.filter((o) => (0, protobuf_1.getOption)(o, validate_pb_js_1.oneof).required)
.map((o) => new eval_js_1.EvalOneofRequired(o)));
}
messageOneofs(message, oneofRules) {
return new eval_js_1.EvalMany(...oneofRules.map((rule) => {
if (rule.fields.length == 0) {
throw new error_js_1.CompilationError(`at least one field must be specified in oneof rule for the ${message}`);
}
const seen = new Set();
return new eval_js_1.EvalMessageOneofRule(rule.fields.map((fieldName) => {
if (seen.has(fieldName)) {
throw new error_js_1.CompilationError(`duplicate ${fieldName} in oneof rule for the ${message}`);
}
seen.add(fieldName);
const found = message.fields.find((descField) => descField.name === fieldName);
if (!found) {
throw new error_js_1.CompilationError(`field "${fieldName}" not found in ${message}`);
}
return found;
}), rule.required);
}));
}
fields(oneofs, fields) {
const evals = new eval_js_1.EvalMany();
for (const field of fields) {
const fieldRules = (0, protobuf_1.getOption)(field, validate_pb_js_1.field);
let ignore = fieldRules.ignore;
if (!(0, protobuf_1.isFieldSet)(fieldRules, validate_pb_js_1.FieldRulesSchema.field.ignore) &&
oneofs.some((oneof) => oneof.fields.includes(field.name))) {
ignore = validate_pb_js_1.Ignore.IF_ZERO_VALUE;
}
const required = fieldRules.required && ignore !== validate_pb_js_1.Ignore.ALWAYS;
const legacyRequired = this.legacyRequired &&
field.presence == wkt_1.FeatureSet_FieldPresence.LEGACY_REQUIRED;
const baseRulePath = (0, reflect_1.buildPath)(validate_pb_js_1.FieldRulesSchema);
switch (field.fieldKind) {
case "message": {
evals.add(new eval_js_1.EvalField(field, required, legacyRequired, (0, condition_js_1.ignoreMessageField)(field, ignore), this.message(field.message, fieldRules, baseRulePath, field)));
break;
}
case "list": {
evals.add(new eval_js_1.EvalField(field, required, legacyRequired, (0, condition_js_1.ignoreListOrMapField)(field, ignore), this.planList(field, fieldRules, baseRulePath)));
break;
}
case "map": {
evals.add(new eval_js_1.EvalField(field, required, legacyRequired, (0, condition_js_1.ignoreListOrMapField)(field, ignore), this.map(field, fieldRules, baseRulePath)));
break;
}
case "enum": {
evals.add(new eval_js_1.EvalField(field, required, legacyRequired, (0, condition_js_1.ignoreScalarOrEnumField)(field, ignore), this.enumeration(field.enum, fieldRules, baseRulePath, field)));
break;
}
case "scalar": {
evals.add(new eval_js_1.EvalField(field, required, legacyRequired, (0, condition_js_1.ignoreScalarOrEnumField)(field, ignore), this.scalar(field.scalar, fieldRules, baseRulePath, false, field)));
break;
}
}
}
return evals;
}
planList(field, fieldRules, baseRulePath) {
const evals = new eval_js_1.EvalMany(this.fieldCel(fieldRules, baseRulePath, false, undefined));
const [rules, rulePath, rulePathItems] = (0, rules_js_1.getListRules)(baseRulePath, fieldRules, field);
if (rules) {
evals.add(this.rules(rules, rulePath, false));
}
const itemsRules = rules?.items;
switch (field.listKind) {
case "enum": {
evals.add(new eval_js_1.EvalListItems((0, condition_js_1.ignoreEnumValue)(field.enum, itemsRules?.ignore), this.enumeration(field.enum, itemsRules, rulePathItems, field)));
break;
}
case "scalar": {
evals.add(new eval_js_1.EvalListItems((0, condition_js_1.ignoreScalarValue)(field.scalar, itemsRules?.ignore), this.scalar(field.scalar, itemsRules, rulePathItems, false, field)));
break;
}
case "message": {
evals.add(new eval_js_1.EvalListItems((0, condition_js_1.ignoreMessageValue)(itemsRules?.ignore), this.message(field.message, itemsRules, rulePathItems, field)));
break;
}
}
return evals;
}
map(field, fieldRules, baseRulePath) {
const evals = new eval_js_1.EvalMany(this.fieldCel(fieldRules, baseRulePath, false, undefined));
const [rules, rulePath, rulePathKeys, rulePathValues] = (0, rules_js_1.getMapRules)(baseRulePath, fieldRules, field);
if (rules) {
evals.add(this.rules(rules, rulePath, false));
}
const ignoreKey = (0, condition_js_1.ignoreScalarValue)(field.mapKey, rules?.keys?.ignore);
const evalKey = this.scalar(field.mapKey, rules?.keys, rulePathKeys, true, field);
const valuesRules = rules?.values;
switch (field.mapKind) {
case "message": {
evals.add(new eval_js_1.EvalMapEntries(ignoreKey, evalKey, (0, condition_js_1.ignoreMessageValue)(valuesRules?.ignore), this.message(field.message, valuesRules, rulePathValues, field)));
break;
}
case "enum": {
evals.add(new eval_js_1.EvalMapEntries(ignoreKey, evalKey, (0, condition_js_1.ignoreEnumValue)(field.enum, valuesRules?.ignore), this.enumeration(field.enum, valuesRules, rulePathValues, field)));
break;
}
case "scalar": {
evals.add(new eval_js_1.EvalMapEntries(ignoreKey, evalKey, (0, condition_js_1.ignoreScalarValue)(field.scalar, valuesRules?.ignore), this.scalar(field.scalar, valuesRules, rulePathValues, false, field)));
break;
}
}
return evals;
}
enumeration(descEnum, fieldRules, baseRulePath, fieldContext) {
const evals = new eval_js_1.EvalMany(this.fieldCel(fieldRules, baseRulePath, false, protobuf_1.ScalarType.INT32));
const [rules, rulePath] = (0, rules_js_1.getEnumRules)(baseRulePath, fieldRules, fieldContext);
if (rules) {
evals.add(new eval_js_1.EvalEnumDefinedOnly(descEnum, rulePath, rules));
evals.add(this.rules(rules, rulePath, false));
}
return evals;
}
scalar(scalar, fieldRules, baseRulePath, forMapKey, fieldContext) {
const evals = new eval_js_1.EvalMany(this.fieldCel(fieldRules, baseRulePath, forMapKey, scalar));
const [rules, rulePath] = (0, rules_js_1.getScalarRules)(scalar, baseRulePath, fieldRules, fieldContext);
if (rules) {
evals.add(this.rules(rules, rulePath, forMapKey));
}
return evals;
}
message(descMessage, fieldRules, baseRulePath, fieldContext) {
const evals = new eval_js_1.EvalMany(this.fieldCel(fieldRules, baseRulePath, false, undefined));
evals.add(this.plan(descMessage));
const [rules, rulePath] = (0, rules_js_1.getMessageRules)(descMessage, baseRulePath, fieldRules, fieldContext);
if (rules) {
if ((0, protobuf_1.isMessage)(rules, validate_pb_js_1.AnyRulesSchema)) {
evals.add(new eval_js_1.EvalAnyRules(rulePath, rules));
}
evals.add(this.rules(rules, rulePath, false));
}
return evals;
}
rules(rules, rulePath, forMapKey) {
const ruleDesc = (0, rules_js_1.getRuleDescriptor)(rules.$typeName);
const prepared = this.celMan.compileRules(ruleDesc);
const evalStandard = new cel_js_1.EvalStandardRulesCel(this.celMan, rules, forMapKey);
for (const plan of prepared.standard) {
if (!(0, protobuf_1.isFieldSet)(rules, plan.field)) {
continue;
}
evalStandard.add(plan.compiled, rulePath.clone().field(plan.field).toPath());
}
const evalExtended = new cel_js_1.EvalExtendedRulesCel(this.celMan, rules, forMapKey);
if (rules.$unknown) {
for (const uf of rules.$unknown) {
const plans = prepared.extensions.get(uf.no);
if (!plans) {
throw new error_js_1.CompilationError(`Unknown extension for ${rules.$typeName} with number ${uf.no}. If this is a predefined rule, register the extension with a registry in createValidator().`);
}
for (const plan of plans) {
evalExtended.add(plan.compiled, rulePath.clone().extension(plan.ext).toPath(), (0, protobuf_1.getExtension)(rules, plan.ext), plan.ext.fieldKind == "scalar" ? plan.ext.scalar : undefined);
}
}
}
return new eval_js_1.EvalMany(evalStandard, evalExtended);
}
messageCel(messageRules) {
const e = new cel_js_1.EvalCustomCel(this.celMan, false, undefined);
for (const rule of [...messageRules.celExpression, ...messageRules.cel]) {
e.add(this.celMan.compileRule(rule), []);
}
return e;
}
fieldCel(fieldRules, baseRulePath, forMapKey, scalarType) {
if (!fieldRules) {
return eval_js_1.EvalNoop.get();
}
const e = new cel_js_1.EvalCustomCel(this.celMan, forMapKey, scalarType);
for (const [field, rules] of [
[validate_pb_js_1.FieldRulesSchema.field.cel, fieldRules.cel],
[validate_pb_js_1.FieldRulesSchema.field.celExpression, fieldRules.celExpression],
]) {
for (const [index, rule] of rules.entries()) {
const rulePath = baseRulePath.clone().field(field).list(index).toPath();
e.add(this.celMan.compileRule(rule), rulePath);
}
}
return e;
}
}
exports.Planner = Planner;