188 lines
7.1 KiB
TypeScript
188 lines
7.1 KiB
TypeScript
import { DeltaID, PointerTarget, DeltaNetworkImageV1, DeltaNetworkImageV2, PointersV2 } from "../core/delta";
|
|
import { CreatorID, HostID, Timestamp } from "../core/types";
|
|
|
|
// Custom error types for delta operations
|
|
export class DeltaValidationError extends Error {
|
|
constructor(message: string, public field?: string) {
|
|
super(message);
|
|
this.name = "DeltaValidationError";
|
|
}
|
|
}
|
|
|
|
export class InvalidDeltaFormatError extends DeltaValidationError {
|
|
constructor(message: string, field?: string) {
|
|
super(message, field);
|
|
this.name = "InvalidDeltaFormatError";
|
|
}
|
|
}
|
|
|
|
export class MissingRequiredFieldError extends DeltaValidationError {
|
|
constructor(field: string) {
|
|
super(`Missing required field: ${field}`, field);
|
|
this.name = "MissingRequiredFieldError";
|
|
}
|
|
}
|
|
|
|
export class InvalidPointerError extends DeltaValidationError {
|
|
constructor(message: string, pointerIndex?: number) {
|
|
super(message, pointerIndex !== undefined ? `pointer[${pointerIndex}]` : undefined);
|
|
this.name = "InvalidPointerError";
|
|
}
|
|
}
|
|
|
|
// Validation functions
|
|
export function validateDeltaId(id: unknown): id is DeltaID {
|
|
if (typeof id !== "string" || id.length === 0) {
|
|
throw new InvalidDeltaFormatError("Delta ID must be a non-empty string", "id");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function validateTimestamp(timestamp: unknown, field: string): timestamp is Timestamp {
|
|
if (typeof timestamp !== "number" || timestamp <= 0) {
|
|
throw new InvalidDeltaFormatError(`${field} must be a positive number`, field);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function validateHostId(host: unknown): host is HostID {
|
|
if (typeof host !== "string" || host.length === 0) {
|
|
throw new InvalidDeltaFormatError("Host ID must be a non-empty string", "host");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function validateCreatorId(creator: unknown): creator is CreatorID {
|
|
if (typeof creator !== "string" || creator.length === 0) {
|
|
throw new InvalidDeltaFormatError("Creator ID must be a non-empty string", "creator");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function validatePointerTarget(target: unknown): target is PointerTarget {
|
|
if (target !== null && typeof target !== "string" && typeof target !== "number" && typeof target !== "boolean") {
|
|
throw new InvalidPointerError("Pointer target must be string, number, boolean, or null");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function validatePointerV1(pointer: unknown, index: number): pointer is { localContext: string; target: PointerTarget; targetContext?: string } {
|
|
if (!pointer || typeof pointer !== "object" || Array.isArray(pointer)) {
|
|
throw new InvalidPointerError(`Pointer at index ${index} must be an object`, index);
|
|
}
|
|
|
|
const p = pointer as Record<string, unknown>;
|
|
|
|
if (typeof p.localContext !== "string" || p.localContext.length === 0) {
|
|
throw new InvalidPointerError(`Pointer at index ${index} must have a non-empty localContext`, index);
|
|
}
|
|
|
|
validatePointerTarget(p.target);
|
|
|
|
if (p.targetContext !== undefined &&
|
|
(typeof p.targetContext !== "string" || p.targetContext.length === 0)) {
|
|
throw new InvalidPointerError(`Pointer at index ${index} targetContext must be a non-empty string if present`, index);
|
|
}
|
|
|
|
// Validate pointer consistency: if targetContext exists, target must be a string (reference)
|
|
if (p.targetContext && typeof p.target !== "string") {
|
|
throw new InvalidPointerError(`Pointer at index ${index} with targetContext must have string target (reference)`, index);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function validatePointersV1(pointers: unknown): pointers is Array<{ localContext: string; target: PointerTarget; targetContext?: string }> {
|
|
if (!Array.isArray(pointers)) {
|
|
throw new InvalidDeltaFormatError("Pointers must be an array", "pointers");
|
|
}
|
|
|
|
if (pointers.length === 0) {
|
|
throw new InvalidDeltaFormatError("Delta must have at least one pointer", "pointers");
|
|
}
|
|
|
|
(pointers as unknown[]).forEach((pointer, index) => validatePointerV1(pointer, index));
|
|
return true;
|
|
}
|
|
|
|
export function validatePointersV2(pointers: unknown): pointers is PointersV2 {
|
|
if (!pointers || typeof pointers !== "object" || Array.isArray(pointers)) {
|
|
throw new InvalidDeltaFormatError("Pointers must be an object", "pointers");
|
|
}
|
|
|
|
const keys = Object.keys(pointers);
|
|
if (keys.length === 0) {
|
|
throw new InvalidDeltaFormatError("Delta must have at least one pointer", "pointers");
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(pointers)) {
|
|
if (key.length === 0) {
|
|
throw new InvalidPointerError("Pointer key must be a non-empty string");
|
|
}
|
|
|
|
if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" && typeof value !== "object") {
|
|
throw new InvalidPointerError(`Invalid pointer value for key '${key}'`);
|
|
}
|
|
|
|
// If value is an object (Reference), validate it
|
|
if (value && typeof value === "object") {
|
|
const refKeys = Object.keys(value);
|
|
if (refKeys.length !== 1) {
|
|
throw new InvalidPointerError(`Reference pointer '${key}' must have exactly one key-value pair`);
|
|
}
|
|
const [refKey, refValue] = Object.entries(value)[0];
|
|
if (typeof refKey !== "string" || refKey.length === 0) {
|
|
throw new InvalidPointerError(`Reference key in pointer '${key}' must be a non-empty string`);
|
|
}
|
|
if (typeof refValue !== "string" || refValue.length === 0) {
|
|
throw new InvalidPointerError(`Reference value in pointer '${key}' must be a non-empty string`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function validateDeltaNetworkImageV1(delta: unknown): delta is DeltaNetworkImageV1 {
|
|
if (!delta || typeof delta !== "object" || Array.isArray(delta)) {
|
|
throw new InvalidDeltaFormatError("Delta must be an object");
|
|
}
|
|
|
|
// Check required fields
|
|
if (!("id" in delta)) throw new MissingRequiredFieldError("id");
|
|
if (!("timeCreated" in delta)) throw new MissingRequiredFieldError("timeCreated");
|
|
if (!("host" in delta)) throw new MissingRequiredFieldError("host");
|
|
if (!("creator" in delta)) throw new MissingRequiredFieldError("creator");
|
|
if (!("pointers" in delta)) throw new MissingRequiredFieldError("pointers");
|
|
|
|
// Validate field types
|
|
validateDeltaId(delta.id);
|
|
validateTimestamp(delta.timeCreated, "timeCreated");
|
|
validateHostId(delta.host);
|
|
validateCreatorId(delta.creator);
|
|
validatePointersV1(delta.pointers);
|
|
|
|
return true;
|
|
}
|
|
|
|
export function validateDeltaNetworkImageV2(delta: unknown): delta is DeltaNetworkImageV2 {
|
|
if (!delta || typeof delta !== "object" || Array.isArray(delta)) {
|
|
throw new InvalidDeltaFormatError("Delta must be an object");
|
|
}
|
|
|
|
// Check required fields
|
|
if (!("id" in delta)) throw new MissingRequiredFieldError("id");
|
|
if (!("timeCreated" in delta)) throw new MissingRequiredFieldError("timeCreated");
|
|
if (!("host" in delta)) throw new MissingRequiredFieldError("host");
|
|
if (!("creator" in delta)) throw new MissingRequiredFieldError("creator");
|
|
if (!("pointers" in delta)) throw new MissingRequiredFieldError("pointers");
|
|
|
|
// Validate field types
|
|
validateDeltaId(delta.id);
|
|
validateTimestamp(delta.timeCreated, "timeCreated");
|
|
validateHostId(delta.host);
|
|
validateCreatorId(delta.creator);
|
|
validatePointersV2(delta.pointers);
|
|
|
|
return true;
|
|
} |