@bufbuild/protovalidate
Version:
Protocol Buffer Validation for ECMAScript
223 lines (222 loc) • 7.14 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 { AnyRulesSchema, EnumRulesSchema, FieldRulesSchema, } from "./gen/buf/validate/validate_pb.js";
/**
* The no-op evaluator.
*/
export class EvalNoop {
static get() {
return EvalNoop.instance;
}
eval() {
//
}
prune() {
return true;
}
}
EvalNoop.instance = new EvalNoop();
/**
* Evaluate many.
*/
export class EvalMany {
constructor(...evals) {
this.many = evals.flat();
}
add(...evals) {
this.many.push(...evals);
return this;
}
eval(val, cursor) {
for (const e of this.many) {
e.eval(val, cursor);
}
}
prune() {
this.many = this.many.filter((e) => !e.prune());
return this.many.length == 0;
}
}
/**
* Evaluated all items in a list.
*/
export class EvalListItems {
constructor(condition, pass) {
this.condition = condition;
this.pass = pass;
}
eval(val, cursor) {
for (let i = 0; i < val.size; i++) {
const t = val.get(i);
if (this.condition.check(t)) {
this.pass.eval(t, cursor.list(i));
}
}
}
prune() {
return this.pass.prune() || this.condition.never;
}
}
/**
* Evaluate key and value of all entries in a map.
*/
export class EvalMapEntries {
constructor(keyCondition, key, valueCondition, value) {
this.keyCondition = keyCondition;
this.key = key;
this.valueCondition = valueCondition;
this.value = value;
}
eval(val, cursor) {
if (this.keyCondition.never && this.valueCondition.never) {
return;
}
for (const [key, value] of val) {
const c = cursor.mapKey(key);
if (this.keyCondition.check(key)) {
this.key.eval(key, c);
}
if (this.valueCondition.check(value)) {
this.value.eval(value, c);
}
}
}
prune() {
const key = this.key.prune() || this.keyCondition.never;
const value = this.value.prune() || this.valueCondition.never;
return key && value;
}
}
/**
* Evaluate field. If the condition passes, evaluate the field's value.
*/
export class EvalField {
constructor(field, required, legacyRequired, condition, pass) {
this.field = field;
this.required = required;
this.legacyRequired = legacyRequired;
this.condition = condition;
this.pass = pass;
}
eval(val, cursor) {
if (this.required || this.legacyRequired) {
if (!val.isSet(this.field)) {
if (this.required) {
cursor
.field(this.field)
.violate("value is required", "required", [
FieldRulesSchema.field.required,
]);
}
else {
cursor
.field(this.field)
.violate("value is required", "legacy_required", []);
}
// Stop evaluation of field rules if the `required` rule is violated.
return;
}
}
if (this.condition.check(val)) {
const fieldVal = val.get(this.field);
this.pass.eval(fieldVal, cursor.field(this.field));
}
}
prune() {
return this.condition.never && !this.required && !this.legacyRequired;
}
}
export class EvalOneofRequired {
constructor(oneof) {
this.oneof = oneof;
}
eval(val, cursor) {
if (this.oneof.fields.some((f) => val.isSet(f))) {
return;
}
cursor
.oneof(this.oneof)
.violate("exactly one field is required in oneof", "required", []);
}
prune() {
return false;
}
}
/**
* buf.validate.EnumRules.defined_only does not use CEL expressions. This implements custom logic for this field.
*/
export class EvalEnumDefinedOnly {
constructor(descEnum, rulePath, rules) {
this.rulePath = rulePath;
this.definedOnly = rules.definedOnly
? new Set(descEnum.values.map((v) => v.number))
: undefined;
}
eval(val, cursor) {
if (this.definedOnly !== undefined && !this.definedOnly.has(val)) {
// value must be one of the defined enum values [enum.defined_only]
cursor.violate("value must be one of the defined enum values", "enum.defined_only", this.rulePath.clone().field(EnumRulesSchema.field.definedOnly).toPath());
}
}
prune() {
return this.definedOnly == undefined;
}
}
/**
* buf.validate.AnyRules.in and not_in do not use CEL expressions. This implements custom logic for these fields.
*/
export class EvalAnyRules {
constructor(rulePath, rules) {
this.rulePath = rulePath;
this.in = rules.in;
this.notIn = rules.notIn;
}
eval(val, cursor) {
const any = val.message;
if (this.in.length > 0 && !this.in.includes(any.typeUrl)) {
// type URL must be in the allow list [any.in]
cursor.violate("type URL must be in the allow list", "any.in", this.rulePath.clone().field(AnyRulesSchema.field.in).toPath());
}
if (this.notIn.length > 0 && this.notIn.includes(any.typeUrl)) {
// type URL must not be in the block list [any.not_in]
cursor.violate("type URL must not be in the block list", "any.not_in", this.rulePath.clone().field(AnyRulesSchema.field.notIn).toPath());
}
}
prune() {
return this.in.length + this.notIn.length == 0;
}
}
/**
* buf.validate.MessageOneofRule does not use CEL expressions. This implements custom logic for this rule.
*/
export class EvalMessageOneofRule {
constructor(fields, required) {
this.fields = fields;
this.required = required;
}
eval(val, cursor) {
const setFields = this.fields.filter((field) => val.isSet(field));
if (setFields.length > 1) {
const oneofNames = this.fields.map((f) => f.name).join(", ");
cursor.violate(`only one of ${oneofNames} can be set`, "message.oneof", []);
}
if (this.required && setFields.length == 0) {
const oneofNames = this.fields.map((f) => f.name).join(", ");
cursor.violate(`one of ${oneofNames} must be set`, "message.oneof", []);
}
}
prune() {
return this.fields.length == 0;
}
}