Files
suaveplan/packages/foundation/bitpacker/bitpacker-strategies/src/strategies/array-config/array-config.ts
2026-05-27 23:47:53 +01:00

191 lines
6.4 KiB
TypeScript

import type {
BitPackerSchema,
PackingContext,
PackingStrategy,
} from "@suaveplan/bitpacker";
import { BITPACKER_CODES, BitpackerError } from "@suaveplan/bitpacker";
function bitsRequired(maxValue: number): number {
if (maxValue <= 0) return 0;
return Math.floor(Math.log2(maxValue)) + 1;
}
export type BitPackerArrayConfig = {
type: "array";
maxLength: number;
} & (
| { elementType: "integer"; elementMax?: number }
| { elementType: "float"; elementMax?: number }
| { elementType: "string"; elementMax?: number }
| { elementType: "boolean" }
| { elementType: "schema"; elementSchema: BitPackerSchema }
);
export function isArrayConfig(schema: unknown): schema is BitPackerArrayConfig {
if (
typeof schema !== "object" ||
schema === null ||
Array.isArray(schema)
) {
return false;
}
const obj = schema as Record<string, unknown>;
if (obj["type"] !== "array") return false;
if (typeof obj["maxLength"] !== "number") return false;
if (
!["integer", "float", "string", "boolean", "schema"].includes(
obj["elementType"] as string
)
)
return false;
if (obj["elementType"] === "schema" && !("elementSchema" in obj))
return false;
return true;
}
export function validateArrayData(
data: unknown[],
config: BitPackerArrayConfig
): boolean {
if (!Array.isArray(data)) return false;
if (data.length > config.maxLength) return false;
return data.every((item) => {
switch (config.elementType) {
case "integer":
return (
typeof item === "number" &&
Number.isInteger(item) &&
item >= 0 &&
item <= (config.elementMax || 255)
);
case "float":
return (
typeof item === "number" &&
item >= 0 &&
item <= (config.elementMax || 100)
);
case "string":
return (
typeof item === "string" &&
item.length <= (config.elementMax || 100)
);
case "boolean":
return typeof item === "boolean";
case "schema":
return true;
default:
return false;
}
});
}
/**
* Strategy for packing array configurations.
*
* Schema shape: `{ type: "array", maxLength: number, elementType: "integer"|"float"|"string"|"boolean"|"schema", ... }`
*/
export class ArrayConfigPackingStrategy implements PackingStrategy {
canHandle(schema: BitPackerSchema): boolean {
return isArrayConfig(schema);
}
pack(
data: unknown,
schema: BitPackerSchema,
context: PackingContext
): void {
const arrayConfig = schema as BitPackerArrayConfig;
if (!Array.isArray(data)) {
throw new BitpackerError(
BITPACKER_CODES.INVALID_SCHEMA,
`Expected array data for array configuration but received ${typeof data}`,
{ context: { data } }
);
}
if (!validateArrayData(data, arrayConfig)) {
throw new BitpackerError(
BITPACKER_CODES.INVALID_SCHEMA,
`Invalid array data for array configuration with elementType "${arrayConfig.elementType}"`,
{ context: { data, elementType: arrayConfig.elementType } }
);
}
// Pack array length
const lengthBits = bitsRequired(arrayConfig.maxLength);
context.writer.writeUint(data.length, lengthBits);
// Pack elements
for (const item of data) {
switch (arrayConfig.elementType) {
case "integer": {
const max = arrayConfig.elementMax || 255;
context.writer.writeUint(item as number, bitsRequired(max));
break;
}
case "float": {
const max = arrayConfig.elementMax || 100;
context.writer.writeUint(item as number, bitsRequired(max));
break;
}
case "string": {
const maxLen = arrayConfig.elementMax || 100;
const bytes = new TextEncoder().encode(item as string);
const len = Math.min(bytes.length, maxLen);
for (let i = 0; i < maxLen; i++) {
context.writer.writeUint(i < len ? bytes[i]! : 0, 8);
}
break;
}
case "boolean":
context.writer.writeBool(item as boolean);
break;
case "schema":
context.packValue(item, arrayConfig.elementSchema);
break;
}
}
}
unpack(schema: BitPackerSchema, context: PackingContext): unknown {
const arrayConfig = schema as BitPackerArrayConfig;
const lengthBits = bitsRequired(arrayConfig.maxLength);
const length = context.reader.readUint(lengthBits);
const result: unknown[] = [];
for (let i = 0; i < length; i++) {
switch (arrayConfig.elementType) {
case "integer": {
const max = arrayConfig.elementMax || 255;
result.push(context.reader.readUint(bitsRequired(max)));
break;
}
case "float": {
const max = arrayConfig.elementMax || 100;
result.push(context.reader.readUint(bitsRequired(max)));
break;
}
case "string": {
const maxLen = arrayConfig.elementMax || 100;
let str = "";
for (let j = 0; j < maxLen; j++) {
const byte = context.reader.readUint(8);
if (byte !== 0) str += String.fromCharCode(byte);
}
result.push(str);
break;
}
case "boolean":
result.push(context.reader.readBool());
break;
case "schema":
result.push(context.unpackValue(arrayConfig.elementSchema));
break;
}
}
return result;
}
}