@bufbuild/protovalidate
Version:
Protocol Buffer Validation for ECMAScript
231 lines (230 loc) • 11.5 kB
JavaScript
// 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.
import { getExtension, getOption, isFieldSet, isMessage, ScalarType, } from "@bufbuild/protobuf";
import { FeatureSet_FieldPresence } from "@bufbuild/protobuf/wkt";
import { Ignore, field as ext_field, message as ext_message, oneof as ext_oneof, FieldRulesSchema, AnyRulesSchema, } from "./gen/buf/validate/validate_pb.js";
import { buildPath, } from "@bufbuild/protobuf/reflect";
import { EvalAnyRules, EvalEnumDefinedOnly, EvalListItems, EvalMany, EvalMapEntries, EvalNoop, EvalOneofRequired, EvalField, EvalMessageOneofRule, } from "./eval.js";
import { getEnumRules, getListRules, getMapRules, getMessageRules, getRuleDescriptor, getScalarRules, } from "./rules.js";
import { ignoreScalarValue, ignoreMessageField, ignoreListOrMapField, ignoreScalarOrEnumField, ignoreEnumValue, ignoreMessageValue, } from "./condition.js";
import { EvalCustomCel, EvalExtendedRulesCel, EvalStandardRulesCel, } from "./cel.js";
import { CompilationError } from "./error.js";
export 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 = getOption(message, ext_message);
const e = new 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 EvalMany(...oneofs
.filter((o) => getOption(o, ext_oneof).required)
.map((o) => new EvalOneofRequired(o)));
}
messageOneofs(message, oneofRules) {
return new EvalMany(...oneofRules.map((rule) => {
if (rule.fields.length == 0) {
throw new CompilationError(`at least one field must be specified in oneof rule for the ${message}`);
}
const seen = new Set();
return new EvalMessageOneofRule(rule.fields.map((fieldName) => {
if (seen.has(fieldName)) {
throw new 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 CompilationError(`field "${fieldName}" not found in ${message}`);
}
return found;
}), rule.required);
}));
}
fields(oneofs, fields) {
const evals = new EvalMany();
for (const field of fields) {
const fieldRules = getOption(field, ext_field);
let ignore = fieldRules.ignore;
if (!isFieldSet(fieldRules, FieldRulesSchema.field.ignore) &&
oneofs.some((oneof) => oneof.fields.includes(field.name))) {
ignore = Ignore.IF_ZERO_VALUE;
}
const required = fieldRules.required && ignore !== Ignore.ALWAYS;
const legacyRequired = this.legacyRequired &&
field.presence == FeatureSet_FieldPresence.LEGACY_REQUIRED;
const baseRulePath = buildPath(FieldRulesSchema);
switch (field.fieldKind) {
case "message": {
evals.add(new EvalField(field, required, legacyRequired, ignoreMessageField(field, ignore), this.message(field.message, fieldRules, baseRulePath, field)));
break;
}
case "list": {
evals.add(new EvalField(field, required, legacyRequired, ignoreListOrMapField(field, ignore), this.planList(field, fieldRules, baseRulePath)));
break;
}
case "map": {
evals.add(new EvalField(field, required, legacyRequired, ignoreListOrMapField(field, ignore), this.map(field, fieldRules, baseRulePath)));
break;
}
case "enum": {
evals.add(new EvalField(field, required, legacyRequired, ignoreScalarOrEnumField(field, ignore), this.enumeration(field.enum, fieldRules, baseRulePath, field)));
break;
}
case "scalar": {
evals.add(new EvalField(field, required, legacyRequired, ignoreScalarOrEnumField(field, ignore), this.scalar(field.scalar, fieldRules, baseRulePath, false, field)));
break;
}
}
}
return evals;
}
planList(field, fieldRules, baseRulePath) {
const evals = new EvalMany(this.fieldCel(fieldRules, baseRulePath, false, undefined));
const [rules, rulePath, rulePathItems] = 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 EvalListItems(ignoreEnumValue(field.enum, itemsRules?.ignore), this.enumeration(field.enum, itemsRules, rulePathItems, field)));
break;
}
case "scalar": {
evals.add(new EvalListItems(ignoreScalarValue(field.scalar, itemsRules?.ignore), this.scalar(field.scalar, itemsRules, rulePathItems, false, field)));
break;
}
case "message": {
evals.add(new EvalListItems(ignoreMessageValue(itemsRules?.ignore), this.message(field.message, itemsRules, rulePathItems, field)));
break;
}
}
return evals;
}
map(field, fieldRules, baseRulePath) {
const evals = new EvalMany(this.fieldCel(fieldRules, baseRulePath, false, undefined));
const [rules, rulePath, rulePathKeys, rulePathValues] = getMapRules(baseRulePath, fieldRules, field);
if (rules) {
evals.add(this.rules(rules, rulePath, false));
}
const ignoreKey = 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 EvalMapEntries(ignoreKey, evalKey, ignoreMessageValue(valuesRules?.ignore), this.message(field.message, valuesRules, rulePathValues, field)));
break;
}
case "enum": {
evals.add(new EvalMapEntries(ignoreKey, evalKey, ignoreEnumValue(field.enum, valuesRules?.ignore), this.enumeration(field.enum, valuesRules, rulePathValues, field)));
break;
}
case "scalar": {
evals.add(new EvalMapEntries(ignoreKey, evalKey, ignoreScalarValue(field.scalar, valuesRules?.ignore), this.scalar(field.scalar, valuesRules, rulePathValues, false, field)));
break;
}
}
return evals;
}
enumeration(descEnum, fieldRules, baseRulePath, fieldContext) {
const evals = new EvalMany(this.fieldCel(fieldRules, baseRulePath, false, ScalarType.INT32));
const [rules, rulePath] = getEnumRules(baseRulePath, fieldRules, fieldContext);
if (rules) {
evals.add(new EvalEnumDefinedOnly(descEnum, rulePath, rules));
evals.add(this.rules(rules, rulePath, false));
}
return evals;
}
scalar(scalar, fieldRules, baseRulePath, forMapKey, fieldContext) {
const evals = new EvalMany(this.fieldCel(fieldRules, baseRulePath, forMapKey, scalar));
const [rules, rulePath] = getScalarRules(scalar, baseRulePath, fieldRules, fieldContext);
if (rules) {
evals.add(this.rules(rules, rulePath, forMapKey));
}
return evals;
}
message(descMessage, fieldRules, baseRulePath, fieldContext) {
const evals = new EvalMany(this.fieldCel(fieldRules, baseRulePath, false, undefined));
evals.add(this.plan(descMessage));
const [rules, rulePath] = getMessageRules(descMessage, baseRulePath, fieldRules, fieldContext);
if (rules) {
if (isMessage(rules, AnyRulesSchema)) {
evals.add(new EvalAnyRules(rulePath, rules));
}
evals.add(this.rules(rules, rulePath, false));
}
return evals;
}
rules(rules, rulePath, forMapKey) {
const ruleDesc = getRuleDescriptor(rules.$typeName);
const prepared = this.celMan.compileRules(ruleDesc);
const evalStandard = new EvalStandardRulesCel(this.celMan, rules, forMapKey);
for (const plan of prepared.standard) {
if (!isFieldSet(rules, plan.field)) {
continue;
}
evalStandard.add(plan.compiled, rulePath.clone().field(plan.field).toPath());
}
const evalExtended = new 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 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(), getExtension(rules, plan.ext), plan.ext.fieldKind == "scalar" ? plan.ext.scalar : undefined);
}
}
}
return new EvalMany(evalStandard, evalExtended);
}
messageCel(messageRules) {
const e = new 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 EvalNoop.get();
}
const e = new EvalCustomCel(this.celMan, forMapKey, scalarType);
for (const [field, rules] of [
[FieldRulesSchema.field.cel, fieldRules.cel],
[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;
}
}