blob: 4cfb8497fd9f83a3ea28e83cba9e3ebb81e11b3f [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 type { EntityId, Revision } from '../runtime-policy.js';
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
export class RuntimePolicyDomainDecodeError extends Error {
constructor(message: string) {
super(message);
this.name = 'RuntimePolicyDomainDecodeError';
}
}
export function exactRecord(
value: unknown,
context: string,
allowed: readonly string[],
required: readonly string[] = allowed,
): Record<string, unknown> {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw domainError(`${context} must be an object`);
}
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
throw domainError(`${context} must be a plain object`);
}
const item = value as Record<string, unknown>;
const allowedSet = new Set(allowed);
for (const key of Object.keys(item)) {
if (!allowedSet.has(key)) throw domainError(`${context} contains unknown field '${key}'`);
}
for (const key of required) {
if (!Object.hasOwn(item, key)) throw domainError(`${context} is missing '${key}'`);
}
return item;
}
export function booleanValue(value: unknown, context: string): boolean {
if (typeof value !== 'boolean') throw domainError(`${context} must be a boolean`);
return value;
}
export function integerValue(value: unknown, context: string, min: number, max: number): number {
if (!Number.isSafeInteger(value) || (value as number) < min || (value as number) > max) {
throw domainError(`${context} must be an integer between ${min} and ${max}`);
}
return value as number;
}
export function revisionValue(value: unknown, context: string): Revision {
return integerValue(value, context, 0, Number.MAX_SAFE_INTEGER);
}
export function positiveRevisionValue(value: unknown, context: string): Revision {
return integerValue(value, context, 1, Number.MAX_SAFE_INTEGER);
}
export function stringValue(value: unknown, context: string, maxLength: number): string {
if (typeof value !== 'string' || value.length > maxLength) {
throw domainError(`${context} must be a string no longer than ${maxLength} characters`);
}
return value;
}
export function nonEmptyStringValue(value: unknown, context: string, maxLength: number): string {
const parsed = stringValue(value, context, maxLength);
if (parsed.length === 0) throw domainError(`${context} must not be empty`);
return parsed;
}
export function stringArrayValue(value: unknown, context: string, maxEntries: number): string[] {
if (!Array.isArray(value) || value.length > maxEntries) {
throw domainError(`${context} must be a bounded string array`);
}
const parsed = value.map((item, index) => nonEmptyStringValue(item, `${context}[${index}]`, 512));
if (new Set(parsed).size !== parsed.length) {
throw domainError(`${context} values must be unique`);
}
return parsed;
}
export function entityIdValue(value: unknown, context: string): EntityId {
const parsed = nonEmptyStringValue(value, context, 64);
if (!UUID_PATTERN.test(parsed)) throw domainError(`${context} must be a UUID`);
return parsed;
}
export function decodeRuntimePolicyEntityId(value: unknown): EntityId {
return entityIdValue(value, 'runtime policy entity id');
}
export function assertCanonicalValue(original: unknown, canonical: unknown, context: string): void {
if (!sameValue(original, canonical)) throw domainError(`${context} must be canonical`);
}
export function domainError(message: string): RuntimePolicyDomainDecodeError {
return new RuntimePolicyDomainDecodeError(message);
}
function sameValue(left: unknown, right: unknown): boolean {
if (Object.is(left, right)) return true;
if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') {
return false;
}
if (Array.isArray(left) || Array.isArray(right)) {
return (
Array.isArray(left) &&
Array.isArray(right) &&
left.length === right.length &&
left.every((value, index) => sameValue(value, right[index]))
);
}
const leftRecord = left as Record<string, unknown>;
const rightRecord = right as Record<string, unknown>;
const leftKeys = Object.keys(leftRecord);
const rightKeys = Object.keys(rightRecord);
return (
leftKeys.length === rightKeys.length &&
leftKeys.every(
(key) => Object.hasOwn(rightRecord, key) && sameValue(leftRecord[key], rightRecord[key]),
)
);
}