From 0a56e30251bc59c830ae00c4e04f956e42f39421 Mon Sep 17 00:00:00 2001 From: Trevor Manz Date: Tue, 7 Jan 2025 19:08:49 -0500 Subject: [PATCH] refactor: Add `assert` utility function Makes assertions more declarative throughout the codebase. --- packages/core/src/codecs.ts | 14 +++--- packages/core/src/codecs/bitround.ts | 5 +-- packages/core/src/codecs/json2.ts | 67 +++++++++++++--------------- packages/core/src/codecs/sharding.ts | 6 +-- packages/core/src/consolidated.ts | 8 ++-- packages/core/src/util.ts | 39 +++++++++++----- packages/storage/src/util.ts | 22 +++++++++ packages/storage/src/zip.ts | 20 ++++----- 8 files changed, 106 insertions(+), 75 deletions(-) diff --git a/packages/core/src/codecs.ts b/packages/core/src/codecs.ts index 2faea23b..83f5c24d 100644 --- a/packages/core/src/codecs.ts +++ b/packages/core/src/codecs.ts @@ -7,6 +7,7 @@ import { Crc32cCodec } from "./codecs/crc32c.js"; import { JsonCodec } from "./codecs/json2.js"; import { TransposeCodec } from "./codecs/transpose.js"; import { VLenUTF8 } from "./codecs/vlen-utf8.js"; +import { assert } from "./util.js"; type ChunkMetadata = { data_type: D; @@ -90,9 +91,7 @@ type BytesToBytesCodec = { async function load_codecs(chunk_meta: ChunkMetadata) { let promises = chunk_meta.codecs.map(async (meta) => { let Codec = await registry.get(meta.name)?.(); - if (!Codec) { - throw new Error(`Unknown codec: ${meta.name}`); - } + assert(Codec, `Unknown codec: ${meta.name}`); return { Codec, meta }; }); let array_to_array: ArrayToArrayCodec[] = []; @@ -112,11 +111,10 @@ async function load_codecs(chunk_meta: ChunkMetadata) { } } if (!array_to_bytes) { - if (!is_typed_array_like_meta(chunk_meta)) { - throw new Error( - `Cannot encode ${chunk_meta.data_type} to bytes without a codec`, - ); - } + assert( + is_typed_array_like_meta(chunk_meta), + `Cannot encode ${chunk_meta.data_type} to bytes without a codec`, + ); array_to_bytes = BytesCodec.fromConfig({ endian: "little" }, chunk_meta); } return { array_to_array, array_to_bytes, bytes_to_bytes }; diff --git a/packages/core/src/codecs/bitround.ts b/packages/core/src/codecs/bitround.ts index 7eb7863d..81997d6b 100644 --- a/packages/core/src/codecs/bitround.ts +++ b/packages/core/src/codecs/bitround.ts @@ -1,4 +1,5 @@ import type { Chunk, Float32, Float64 } from "../metadata.js"; +import { assert } from "../util.js"; /** * A codec for bit-rounding. @@ -20,9 +21,7 @@ export class BitroundCodec { kind = "array_to_array"; constructor(configuration: { keepbits: number }, _meta: { data_type: D }) { - if (configuration.keepbits < 0) { - throw new Error("keepbits must be zero or positive"); - } + assert(configuration.keepbits >= 0, "keepbits must be zero or positive"); } static fromConfig( diff --git a/packages/core/src/codecs/json2.ts b/packages/core/src/codecs/json2.ts index 9879f1ae..e67b06f3 100644 --- a/packages/core/src/codecs/json2.ts +++ b/packages/core/src/codecs/json2.ts @@ -1,6 +1,6 @@ // Adapted from https://github.com/hms-dbmi/vizarr/blob/5b0e3ea6fbb42d19d0e38e60e49bb73d1aca0693/src/utils.ts#L26 import type { Chunk, ObjectType } from "../metadata.js"; -import { get_strides, json_decode_object } from "../util.js"; +import { assert, get_strides, json_decode_object } from "../util.js"; type EncoderConfig = { encoding?: "utf-8"; @@ -24,23 +24,18 @@ type ReplacerFunction = (key: string | number, value: any) => any; // Reference: https://stackoverflow.com/a/21897413 function throw_on_nan_replacer(_key: string | number, value: number): number { - if (Number.isNaN(value)) { - throw new Error( - "JsonCodec allow_nan is false but NaN was encountered during encoding.", - ); - } - - if (value === Number.POSITIVE_INFINITY) { - throw new Error( - "JsonCodec allow_nan is false but Infinity was encountered during encoding.", - ); - } - - if (value === Number.NEGATIVE_INFINITY) { - throw new Error( - "JsonCodec allow_nan is false but -Infinity was encountered during encoding.", - ); - } + assert( + !Number.isNaN(value), + "JsonCodec allow_nan is false but NaN was encountered during encoding.", + ); + assert( + value !== Number.POSITIVE_INFINITY, + "JsonCodec allow_nan is false but Infinity was encountered during encoding.", + ); + assert( + value !== Number.NEGATIVE_INFINITY, + "JsonCodec allow_nan is false but -Infinity was encountered during encoding.", + ); return value; } @@ -117,17 +112,19 @@ export class JsonCodec { allow_nan, sort_keys, } = this.#encoder_config; - if (encoding !== "utf-8") { - throw new Error("JsonCodec does not yet support non-utf-8 encoding."); - } + assert( + encoding === "utf-8", + "JsonCodec does not yet support non-utf-8 encoding.", + ); const replacer_functions: ReplacerFunction[] = []; - if (!check_circular) { - // By default, for JSON.stringify, - // a TypeError will be thrown if one attempts to encode an object with circular references - throw new Error( - "JsonCodec does not yet support skipping the check for circular references during encoding.", - ); - } + + // By default, for JSON.stringify, + // a TypeError will be thrown if one attempts to encode an object with circular references + assert( + check_circular, + "JsonCodec does not yet support skipping the check for circular references during encoding.", + ); + if (!allow_nan) { // Throw if NaN/Infinity/-Infinity are encountered during encoding. replacer_functions.push(throw_on_nan_replacer); @@ -170,17 +167,15 @@ export class JsonCodec { decode(bytes: Uint8Array): Chunk { const { strict } = this.#decoder_config; - if (!strict) { - // (i.e., allowing control characters inside strings) - throw new Error("JsonCodec does not yet support non-strict decoding."); - } + // (i.e., allowing control characters inside strings) + assert(strict, "JsonCodec does not yet support non-strict decoding."); + const items = json_decode_object(bytes); const shape = items.pop(); items.pop(); // Pop off dtype (unused) - if (!shape) { - // O-d case - throw new Error("0D not implemented for JsonCodec."); - } + + // O-d case + assert(shape, "0D not implemented for JsonCodec."); const stride = get_strides(shape, "C"); const data = items; return { data, shape, stride }; diff --git a/packages/core/src/codecs/sharding.ts b/packages/core/src/codecs/sharding.ts index 3da6a24d..dc24fab2 100644 --- a/packages/core/src/codecs/sharding.ts +++ b/packages/core/src/codecs/sharding.ts @@ -1,6 +1,6 @@ import type { Location } from "../hierarchy.js"; import type { Chunk } from "../metadata.js"; -import type { ShardingCodecMetadata } from "../util.js"; +import { assert, type ShardingCodecMetadata } from "../util.js"; import type { Readable } from "@zarrita/storage"; import { create_codec_pipeline } from "../codecs.js"; @@ -13,9 +13,7 @@ export function create_sharded_chunk_getter( encode_shard_key: (coord: number[]) => string, sharding_config: ShardingCodecMetadata["configuration"], ) { - if (location.store.getRange === undefined) { - throw new Error("Store does not support range requests"); - } + assert(location.store.getRange, "Store does not support range requests"); let get_range = location.store.getRange.bind(location.store); let index_shape = shard_shape.map( (d, i) => d / sharding_config.chunk_shape[i], diff --git a/packages/core/src/consolidated.ts b/packages/core/src/consolidated.ts index ebc5b19b..f903b823 100644 --- a/packages/core/src/consolidated.ts +++ b/packages/core/src/consolidated.ts @@ -8,6 +8,7 @@ import type { GroupMetadataV2, } from "./metadata.js"; import { + assert, json_decode_object, json_encode_object, rethrow_unless, @@ -38,9 +39,10 @@ async function get_consolidated_metadata( }); } let meta: ConsolidatedMetadata = json_decode_object(bytes); - if (meta.zarr_consolidated_format !== 1) { - throw new Error("Unsupported consolidated format."); - } + assert( + meta.zarr_consolidated_format === 1, + "Unsupported consolidated format.", + ); return meta; } diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts index ae820789..7084e18b 100644 --- a/packages/core/src/util.ts +++ b/packages/core/src/util.ts @@ -73,10 +73,8 @@ export function get_ctr( ); } // @ts-expect-error - We've checked that the key exists - let ctr: TypedArrayConstructor = CONSTRUCTORS[data_type]; - if (!ctr) { - throw new Error(`Unknown or unsupported data_type: ${data_type}`); - } + let ctr: TypedArrayConstructor | undefined = CONSTRUCTORS[data_type]; + assert(ctr, `Unknown or unsupported data_type: ${data_type}`); return ctr; } @@ -136,9 +134,8 @@ function coerce_dtype( } let match = dtype.match(endian_regex); - if (!match) { - throw new Error(`Invalid dtype: ${dtype}`); - } + assert(match, `Invalid dtype: ${dtype}`); + let [, endian, rest] = match; let data_type = { @@ -155,9 +152,7 @@ function coerce_dtype( f8: "float64", }[rest] ?? (rest.startsWith("S") || rest.startsWith("U") ? `v2:${rest}` : undefined); - if (!data_type) { - throw new Error(`Unsupported or unknown dtype: ${dtype}`); - } + assert(data_type, `Unsupported or unknown dtype: ${dtype}`); if (endian === "|") { return { data_type } as { data_type: DataType }; } @@ -332,3 +327,27 @@ export function rethrow_unless>( throw error; } } + +/** + * Make an assertion. + * + * Usage + * @example + * ```ts + * const value: boolean = Math.random() <= 0.5; + * assert(value, "value is greater than than 0.5!"); + * value // true + * ``` + * + * @param expression - The expression to test. + * @param msg - The optional message to display if the assertion fails. + * @throws an {@link Error} if `expression` is not truthy. + */ +export function assert( + expression: unknown, + msg: string | undefined = "", +): asserts expression { + if (!expression) { + throw new Error(msg); + } +} diff --git a/packages/storage/src/util.ts b/packages/storage/src/util.ts index 85aa8e8a..9cdc6d1e 100644 --- a/packages/storage/src/util.ts +++ b/packages/storage/src/util.ts @@ -56,3 +56,25 @@ export function merge_init( }, }; } + +/** + * Make an assertion. + * + * Usage + * @example + * ```ts + * const value: boolean = Math.random() <= 0.5; + * assert(value, "value is greater than than 0.5!"); + * value // true + * ``` + * + * @param expression - The expression to test. + * @param msg - The optional message to display if the assertion fails. + * @throws an {@link Error} if `expression` is not truthy. + */ +export function assert( + expression: unknown, + msg: string | undefined = "", +): asserts expression { + if (!expression) throw new Error(msg); +} diff --git a/packages/storage/src/zip.ts b/packages/storage/src/zip.ts index 0aa2d9cc..8a69a6d0 100644 --- a/packages/storage/src/zip.ts +++ b/packages/storage/src/zip.ts @@ -1,5 +1,5 @@ import { unzip } from "unzipit"; -import { fetch_range, strip_prefix } from "./util.js"; +import { assert, fetch_range, strip_prefix } from "./util.js"; import type { Reader, ZipInfo } from "unzipit"; import type { AbsolutePath, AsyncReadable } from "./types.js"; @@ -35,11 +35,10 @@ export class HTTPRangeReader implements Reader { ...this.#overrides, method: "HEAD", }); - if (!req.ok) { - throw new Error( - `failed http request ${this.url}, status: ${req.status}: ${req.statusText}`, - ); - } + assert( + req.ok, + `failed http request ${this.url}, status: ${req.status}: ${req.statusText}`, + ); this.length = Number(req.headers.get("content-length")); if (Number.isNaN(this.length)) { throw Error("could not get length"); @@ -53,11 +52,10 @@ export class HTTPRangeReader implements Reader { return new Uint8Array(0); } const req = await fetch_range(this.url, offset, size, this.#overrides); - if (!req.ok) { - throw new Error( - `failed http request ${this.url}, status: ${req.status} offset: ${offset} size: ${size}: ${req.statusText}`, - ); - } + assert( + req.ok, + `failed http request ${this.url}, status: ${req.status} offset: ${offset} size: ${size}: ${req.statusText}`, + ); return new Uint8Array(await req.arrayBuffer()); } }