blob: 939cb707069f60b863b3c9f67e5ef16a237ccc63 [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 { Revision } from '@maka/core/runtime-policy';
import { codecError, type CodecSource } from './errors.js';
export function record(
value: unknown,
context: string,
source: CodecSource,
allowed: readonly string[],
required: readonly string[] = allowed,
): Record<string, unknown> {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw codecError(source, `${context} must be an object`);
}
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
throw codecError(source, `${context} must be a plain object`);
}
const candidate = value as Record<string, unknown>;
const allowedSet = new Set(allowed);
for (const key of Object.keys(candidate)) {
if (!allowedSet.has(key))
throw codecError(source, `${context} contains unknown field '${key}'`);
}
for (const key of required) {
if (!Object.hasOwn(candidate, key)) throw codecError(source, `${context} is missing '${key}'`);
}
return candidate;
}
export function revision(value: unknown, context: string, source: CodecSource): Revision {
return integer(value, context, 0, Number.MAX_SAFE_INTEGER, source);
}
export function integer(
value: unknown,
context: string,
min: number,
max: number,
source: CodecSource,
): number {
if (!Number.isSafeInteger(value) || (value as number) < min || (value as number) > max) {
throw codecError(source, `${context} must be an integer between ${min} and ${max}`);
}
return value as number;
}
export function unique(values: readonly string[], context: string, source: CodecSource): void {
if (new Set(values).size !== values.length) {
throw codecError(source, `${context} values must be unique`);
}
}
export function nextRevision(value: Revision): Revision {
if (value >= Number.MAX_SAFE_INTEGER) {
throw codecError('invalid_document', 'Revision space is exhausted');
}
return value + 1;
}
export function deepFreeze<T>(value: T): T {
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
Object.freeze(value);
for (const item of Object.values(value)) deepFreeze(item);
return value;
}