UNPKG

@bufbuild/protovalidate

Version:

Protocol Buffer Validation for ECMAScript

331 lines (330 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 { pathToString } from "@bufbuild/protobuf/reflect"; import { FieldPathElementSchema, FieldPathSchema, ViolationSchema, ViolationsSchema, } from "./gen/buf/validate/validate_pb.js"; import { create, ScalarType, } from "@bufbuild/protobuf"; import { FieldDescriptorProto_Type } from "@bufbuild/protobuf/wkt"; /** * A CompilationError is raised if a CEL expression cannot be compiled, or if * invalid standard rules are applied. */ export class CompilationError extends Error { constructor(message, options) { super(message, options); this.name = "CompilationError"; // see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#example Object.setPrototypeOf(this, new.target.prototype); } } /** * A RuntimeError is raised if a CEL expression errors or returns an * unexpected value, or if the schema and message provided to the * validator mismatch. */ export class RuntimeError extends Error { constructor(message, options) { super(message, options); this.name = "RuntimeError"; // see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#example Object.setPrototypeOf(this, new.target.prototype); } } /** * A ValidationError is raised if one or more rule violations were * detected. */ export class ValidationError extends Error { constructor(violations) { super(validationErrorMessage(violations)); this.name = "ValidationError"; // see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#example Object.setPrototypeOf(this, new.target.prototype); this.violations = violations; } } function validationErrorMessage(violations) { if (violations.length == 0) { return "validation failed"; } if (violations.length == 1) { return violations[0].toString(); } return (violations[0].toString() + `, and ${violations.length - 1} more violation${violations.length > 2 ? "s" : ""}`); } /** * Violation represents a single instance where a validation rule was not met. * It provides information about the field that caused the violation, the * specific unfulfilled rule, and a human-readable error message. */ export class Violation { constructor(message, ruleId, field, rule, forKey) { this.message = message; this.ruleId = ruleId; this.field = field; this.rule = rule; this.forKey = forKey; } toString() { let path = pathToString(this.field); if (path.length > 0) { path += ": "; } return path + `${this.message} [${this.ruleId}]`; } } /** * Convert an array of Violation[] to the Protobuf message buf.validate.Violations. */ export function violationsToProto(violation) { return create(ViolationsSchema, { violations: violation.map(violationToProto), }); } /** * Convert a Violation to the Protobuf message buf.validate.Violation. */ export function violationToProto(violation) { return create(ViolationSchema, { field: violation.field.length > 0 ? pathToProto(violation.field) : undefined, rule: violation.rule.length > 0 ? pathToProto(violation.rule) : undefined, ruleId: violation.ruleId, message: violation.message.length > 0 ? violation.message : undefined, forKey: violation.forKey, }); } /** * Convert a Protobuf message buf.validate.FieldPath to a Path. * * Raises an error if the Protobuf message cannot be converted because of a schema * mismatch, or because it is invalid. */ export function pathFromViolationProto(schema, proto, registry) { const path = []; let parent = schema; for (const [i, e] of proto.elements.entries()) { if (!parent) { throw errInvPathProto(i); } const field = parent.fields.find((f) => f.number === e.fieldNumber || f.name === e.fieldName); if (!field) { const oneof = parent.oneofs.find((o) => o.name === e.fieldName); if (oneof) { path.push(oneof); parent = undefined; continue; } if (registry) { const ext = registry.getExtensionFor(parent, e.fieldNumber); if (ext) { path.push(ext); parent = ext.message; continue; } } throw errInvPathProto(i); } path.push(field); parent = field.message; if (e.subscript.case == "index") { if (field.fieldKind != "list") { throw errInvPathProto(i); } path.push({ kind: "list_sub", index: Number(e.subscript.value), }); } else if (e.subscript.case != undefined) { if (field.fieldKind != "map") { throw errInvPathProto(i); } switch (e.subscript.case) { case "boolKey": if (field.mapKey != ScalarType.BOOL) { throw errInvPathProto(i); } break; case "stringKey": if (field.mapKey != ScalarType.STRING) { throw errInvPathProto(i); } break; case "uintKey": switch (field.mapKey) { case ScalarType.UINT32: case ScalarType.FIXED32: case ScalarType.UINT64: case ScalarType.FIXED64: // ok break; default: throw errInvPathProto(i); } break; case "intKey": switch (field.mapKey) { case ScalarType.INT32: case ScalarType.SINT32: case ScalarType.SFIXED32: case ScalarType.UINT32: case ScalarType.FIXED32: // ok break; default: throw errInvPathProto(i); } break; } path.push({ kind: "map_sub", key: e.subscript.value, }); } } return path; } function errInvPathProto(index) { return new Error(`invalid field path element ${index + 1}`); } /** * Convert a Path to the Protobuf message buf.validate.FieldPath. * * For an invalid or unsupported Path (buf.validate.FieldPath currently does not * support extensions, but Path does), this function will drop data instead of * throwing an error. */ function pathToProto(path) { const elements = []; for (const [i, e] of path.entries()) { switch (e.kind) { case "field": elements.push(create(FieldPathElementSchema, { fieldName: e.name, fieldNumber: e.number, fieldType: fieldType(e), })); break; case "extension": elements.push(create(FieldPathElementSchema, { fieldName: "[" + e.typeName + "]", fieldNumber: e.number, fieldType: e.proto.type, })); break; case "oneof": elements.push(create(FieldPathElementSchema, { fieldName: e.name, })); break; case "list_sub": { const prevProto = elements[elements.length - 1]; if (prevProto) { const prevPath = path[i - 1]; prevProto.subscript = getListSub(e.index, prevPath); } break; } case "map_sub": { const prevProto = elements[elements.length - 1]; if (prevProto) { const prevPath = path[i - 1]; setMapSub(prevProto, e.key, prevPath); } break; } } } return create(FieldPathSchema, { elements }); } function fieldType(field) { if (field.fieldKind == "message" && field.delimitedEncoding) { return FieldDescriptorProto_Type.GROUP; } return field.proto.type; } function getListSub(index, prevPath) { if (prevPath?.kind == "field" && prevPath.fieldKind == "list") { return { case: "index", value: BigInt(index), }; } return { case: undefined }; } function setMapSub(proto, key, prevPath) { if (prevPath?.kind != "field" || prevPath.fieldKind != "map") { return; } proto.keyType = prevPath.mapKey; switch (prevPath.mapKind) { case "scalar": proto.valueType = prevPath.scalar; break; case "enum": proto.valueType = FieldDescriptorProto_Type.ENUM; break; case "message": // map fields are always LENGTH_PREFIXED proto.valueType = FieldDescriptorProto_Type.MESSAGE; break; } switch (typeof key) { case "boolean": switch (prevPath.mapKey) { case ScalarType.BOOL: proto.subscript = { case: "boolKey", value: key, }; break; } break; case "string": switch (prevPath.mapKey) { case ScalarType.STRING: proto.subscript = { case: "stringKey", value: key, }; break; } break; case "number": case "bigint": switch (prevPath.mapKey) { case ScalarType.INT32: case ScalarType.SINT32: case ScalarType.SFIXED32: case ScalarType.INT64: case ScalarType.SINT64: case ScalarType.SFIXED64: proto.subscript = { case: "intKey", value: BigInt(key), }; break; case ScalarType.UINT32: case ScalarType.FIXED32: case ScalarType.UINT64: case ScalarType.FIXED64: proto.subscript = { case: "uintKey", value: BigInt(key), }; break; } break; } }