UNPKG

@bufbuild/protovalidate

Version:

Protocol Buffer Validation for ECMAScript

231 lines (230 loc) 11.5 kB
// 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; } }