From 557223a87868b9df17be6a9a4de3745de7f09d10 Mon Sep 17 00:00:00 2001 From: Zuri Klaschka Date: Mon, 9 Sep 2024 14:29:46 +0200 Subject: [PATCH] Improved common APIs and testing --- deno.json | 2 +- .../additional-managed-namespaces.test.ts | 150 ++++++++++++- lib/common/additional-managed-namespaces.ts | 28 ++- lib/common/asn.test.ts | 199 ++++++++++++++++-- lib/common/asn.ts | 154 +++++++++++--- lib/common/namespaces.test.ts | 148 +++++++++++++ lib/common/namespaces.ts | 62 ++++-- lib/common/time-stats.test.ts | 14 +- 8 files changed, 679 insertions(+), 78 deletions(-) create mode 100644 lib/common/namespaces.test.ts diff --git a/deno.json b/deno.json index 75461fa..20eb719 100644 --- a/deno.json +++ b/deno.json @@ -10,7 +10,7 @@ "tasks": { "dev": "deno run --watch --env --unstable-kv -A main.ts", "run": "deno run --env --unstable-kv -A main.ts", - "test": "DATA_DIR=data-test ASN_PREFIX=ASN ASN_NAMESPACE_RANGE=50 deno test --unstable-kv -A", + "test": "DATA_DIR=data-test ASN_PREFIX=ASN ASN_NAMESPACE_RANGE=50 ASN_ENABLE_NAMESPACE_EXTANSION=1 deno test --unstable-kv -A", "lint": "deno lint", "check": "deno check main.ts", "version": "deno run --allow-read=. --allow-write=. --allow-run=git jsr:@utility/version" diff --git a/lib/common/additional-managed-namespaces.test.ts b/lib/common/additional-managed-namespaces.test.ts index 541611a..596ba9e 100644 --- a/lib/common/additional-managed-namespaces.test.ts +++ b/lib/common/additional-managed-namespaces.test.ts @@ -2,9 +2,21 @@ import { assertEquals } from "@std/assert"; import { deserializeAdditionalManagedNamespace, deserializeAdditionalManagedNamespaces, + isValidAdditionalManagedNamespace, serializeAdditionalManagedNamespace, serializeAdditionalManagedNamespaces, -} from "./additional-managed-namespaces.ts"; +} from "$common/additional-managed-namespaces.ts"; +import type { Config } from "$common/config.ts"; + +const TEST_CONFIG: Config = { + PORT: 0, + ASN_PREFIX: "ASN", + ASN_NAMESPACE_RANGE: 3, + ASN_ENABLE_NAMESPACE_EXTENSION: false, + ADDITIONAL_MANAGED_NAMESPACES: [], + ASN_LOOKUP_INCLUDE_PREFIX: false, + ASN_BARCODE_TYPE: "", +}; Deno.test("serializeAdditionalManagedNamespace", () => { assertEquals( @@ -87,3 +99,139 @@ Deno.test("deserializeAdditionalManagedNamespaces", () => { [], ); }); + +Deno.test("isValidAdditionalManagedNamespace", async (t) => { + await t.step("Single Digit Namespace", () => { + assertEquals( + isValidAdditionalManagedNamespace(5, { + ...TEST_CONFIG, + ADDITIONAL_MANAGED_NAMESPACES: [{ + namespace: 5, + label: "Test namespace", + }], + }), + true, + "Registered managed namespace (5) should be valid for range 3", + ); + assertEquals( + isValidAdditionalManagedNamespace(6, { ...TEST_CONFIG }), + true, + "Unregistered managed namespace (6) should be valid for range 3", + ); + assertEquals( + isValidAdditionalManagedNamespace(22, { ...TEST_CONFIG }), + false, + "Unregistered managed namespace (22) should be invalid for range 3 (too many digits)", + ); + assertEquals( + isValidAdditionalManagedNamespace(0, { ...TEST_CONFIG }), + false, + "Unregistered managed namespace (0) should be invalid for range 3 (too small)", + ); + assertEquals( + isValidAdditionalManagedNamespace(-1, { ...TEST_CONFIG }), + false, + "Unregistered managed namespace (-1) should be invalid for range 3 (negative)", + ); + }); + + await t.step("Double Digit Namespace", () => { + assertEquals( + isValidAdditionalManagedNamespace(50, { + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 20, + ADDITIONAL_MANAGED_NAMESPACES: [ + { namespace: 50, label: "Test namespace" }, + ], + }), + true, + "Registered managed namespace (50) should be valid for range 20", + ); + assertEquals( + isValidAdditionalManagedNamespace(63, { + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 20, + }), + true, + "Unregistered managed namespace (63) should be valid for range 20", + ); + assertEquals( + isValidAdditionalManagedNamespace(222, { + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 20, + }), + false, + "Unregistered managed namespace (222) should be invalid for range 20 (too many digits)", + ); + assertEquals( + isValidAdditionalManagedNamespace(1, { + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 20, + }), + false, + "Unregistered managed namespace (1) should be invalid for range 20 (too few digits)", + ); + assertEquals( + isValidAdditionalManagedNamespace(0, { + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 20, + }), + false, + "Unregistered managed namespace (0) should be invalid for range 20 (too small)", + ); + assertEquals( + isValidAdditionalManagedNamespace(-1, { + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 20, + }), + false, + "Unregistered managed namespace (-1) should be invalid for range 20 (negative)", + ); + }); + + await t.step( + "Generic Namespace Range isn't valid for additional managed namespaces", + () => { + assertEquals( + isValidAdditionalManagedNamespace(18, { + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 20, + }), + false, + "Generic namespace range should not be valid for additional managed namespaces", + ); + assertEquals( + isValidAdditionalManagedNamespace(2, { + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 3, + }), + false, + "Generic namespace range should not be valid for additional managed namespaces", + ); + }, + ); + + await t.step("ASN_ENABLE_NAMESPACE_EXTENSION", () => { + assertEquals( + isValidAdditionalManagedNamespace(9, { ...TEST_CONFIG }), + true, + 'Namespace "9" should be valid for range 3 without extension', + ); + assertEquals( + isValidAdditionalManagedNamespace(9, { + ...TEST_CONFIG, + ASN_ENABLE_NAMESPACE_EXTENSION: true, + }), + false, + 'Namespace "9" should be invalid for range 3 with extension', + ); + assertEquals( + isValidAdditionalManagedNamespace(91, { + ...TEST_CONFIG, + ASN_ENABLE_NAMESPACE_EXTENSION: true, + }), + true, + 'Namespace "91" should be valid for range 3 with extension', + ); + }); +}); diff --git a/lib/common/additional-managed-namespaces.ts b/lib/common/additional-managed-namespaces.ts index bd155c7..861c39f 100644 --- a/lib/common/additional-managed-namespaces.ts +++ b/lib/common/additional-managed-namespaces.ts @@ -1,4 +1,4 @@ -import { CONFIG } from "$common/mod.ts"; +import { CONFIG, isValidNamespace } from "$common/mod.ts"; /** * An additional managed namespace outside of the default range. @@ -87,18 +87,26 @@ export function deserializeAdditionalManagedNamespaces( return matches.map(deserializeAdditionalManagedNamespace); } +/** + * Checks if a namespace is a valid additional managed namespace according to the configuration. + * Note that "valid" does not necessarily mean "managed". + * A valid additional managed namespace is any namespace that fulfills the following conditions: + * - The namespace is a safe integer, and + * - The namespace has a compatible number of digits based on the configuration, and + * - The namespace is greater than or equal to the `ASN_NAMESPACE_RANGE` configuration parameter. + * + * Any managed namespace must be a valid namespace, but not all valid namespaces are managed. + * + * @param namespace the namespace to check + * @returns `true` if the namespace is a valid additional managed namespace, `false` otherwise + */ export function isValidAdditionalManagedNamespace( namespace: number, + config = CONFIG, ): boolean { - let sNamespace = namespace.toString(); - - if (CONFIG.ASN_ENABLE_NAMESPACE_EXTENSION) { - // strip leading 9s - sNamespace = sNamespace.replace(/^9+/, ""); + if (!isValidNamespace(namespace, config)) { + return false; } - return ( - sNamespace.length === CONFIG.ASN_NAMESPACE_RANGE.toString().length && - namespace >= CONFIG.ASN_NAMESPACE_RANGE - ); + return namespace >= config.ASN_NAMESPACE_RANGE; } diff --git a/lib/common/asn.test.ts b/lib/common/asn.test.ts index d7bca7b..22be309 100644 --- a/lib/common/asn.test.ts +++ b/lib/common/asn.test.ts @@ -1,16 +1,191 @@ -import { assertEquals } from "@std/assert"; -import { nthNinerExtensionRange } from "$common/asn.ts"; +import { assertEquals, assertObjectMatch, assertThrows } from "@std/assert"; +import { + formatASN, + isValidASN, + isValidCounter, + nthNinerExtensionRange, + parseASN, +} from "$common/asn.ts"; +import type { Config } from "$common/mod.ts"; -Deno.test("nthNinerExtensionRange()", () => { - assertEquals(nthNinerExtensionRange(1, 1), [90, 98]); - assertEquals(nthNinerExtensionRange(2, 1), [990, 998]); - assertEquals(nthNinerExtensionRange(3, 1), [9990, 9998]); +const TEST_CONFIG: Config = { + ASN_NAMESPACE_RANGE: 50, + ADDITIONAL_MANAGED_NAMESPACES: [{ + namespace: 60, + label: "Test namespace", + }], + PORT: 0, + ASN_PREFIX: "ASN", + ASN_ENABLE_NAMESPACE_EXTENSION: true, + ASN_LOOKUP_INCLUDE_PREFIX: true, + ASN_BARCODE_TYPE: "", +}; - assertEquals(nthNinerExtensionRange(1, 10), [900, 989]); - assertEquals(nthNinerExtensionRange(2, 10), [9900, 9989]); - assertEquals(nthNinerExtensionRange(3, 10), [99900, 99989]); +Deno.test("nthNinerExtensionRange()", async (t) => { + await t.step("Range 1", () => { + assertEquals(nthNinerExtensionRange(1, 1), [90, 98]); + assertEquals(nthNinerExtensionRange(2, 1), [990, 998]); + assertEquals(nthNinerExtensionRange(3, 1), [9990, 9998]); + }); - assertEquals(nthNinerExtensionRange(1, 100), [9000, 9899]); - assertEquals(nthNinerExtensionRange(2, 100), [99000, 99899]); - assertEquals(nthNinerExtensionRange(3, 100), [999000, 999899]); + await t.step("Range 10", () => { + assertEquals(nthNinerExtensionRange(1, 10), [900, 989]); + assertEquals(nthNinerExtensionRange(2, 10), [9900, 9989]); + assertEquals(nthNinerExtensionRange(3, 10), [99900, 99989]); + }); + + await t.step("Range 100", () => { + assertEquals(nthNinerExtensionRange(1, 100), [9000, 9899]); + assertEquals(nthNinerExtensionRange(2, 100), [99000, 99899]); + assertEquals(nthNinerExtensionRange(3, 100), [999000, 999899]); + }); +}); + +Deno.test("parseASN()", async (t) => { + assertObjectMatch(parseASN(`ASN10001`, { ...TEST_CONFIG }), { + namespace: 10, + counter: 1, + }, "Parse ASN with prefix"); + assertObjectMatch(parseASN(`10001`, { ...TEST_CONFIG }), { + namespace: 10, + counter: 1, + }, "Parse ASN without prefix"); + + await t.step("Handle invalid ASNs", () => { + assertThrows( + () => parseASN(`ASN100A11`, { ...TEST_CONFIG }), + "Throw on invalid ASN", + ); + }); + + await t.step("Namespace extension", () => { + assertObjectMatch( + parseASN(`ASN90111`, { + ...TEST_CONFIG, + ASN_ENABLE_NAMESPACE_EXTENSION: false, + }), + { + namespace: 90, + counter: 111, + }, + "Use 'raw' namespace even with leading '9' when extension is disabled", + ); + assertObjectMatch( + parseASN(`ASN90111`, { + ...TEST_CONFIG, + ASN_ENABLE_NAMESPACE_EXTENSION: true, + }), + { + namespace: 901, + counter: 11, + }, + "Use 'extended' namespace when extension is enabled", + ); + assertObjectMatch( + parseASN(`990111`, { + ...TEST_CONFIG, + ASN_ENABLE_NAMESPACE_EXTENSION: true, + }), + { + namespace: 9901, + counter: 11, + }, + "Use 'extended' namespace when extension is enabled, allowing for multiple '9's", + ); + }); +}); + +Deno.test("isValidASN()", async (t) => { + assertEquals( + isValidASN(`ASN10001`, { ...TEST_CONFIG }), + true, + "ASNs are valid with prefix", + ); + assertEquals( + isValidASN(`10001`, { ...TEST_CONFIG }), + true, + "ASNs are valid without prefix", + ); + assertEquals( + isValidASN(`ASN100A11`, { ...TEST_CONFIG }), + false, + "Invalid if the overall format is incorrect", + ); + await t.step("Prefix configuration", () => { + assertEquals( + isValidASN(`XXX10011`, { ...TEST_CONFIG, ASN_PREFIX: "XXX" }), + true, + "Valid if the prefix matches the configuration", + ); + assertEquals( + isValidASN(`XXX10011`, { ...TEST_CONFIG }), + false, + "Invalid if the prefix doesn't match the configuration", + ); + }); +}); + +Deno.test("isValidCounter()", () => { + assertEquals( + isValidCounter(Number.MAX_SAFE_INTEGER), + true, + "Maximum safe integer is valid", + ); + assertEquals(isValidCounter(1), true, "Positive integers are valid"); + assertEquals(isValidCounter(0), true, "Zero is valid"); + assertEquals(isValidCounter(-1), false, "Negative integers are invalid"); + assertEquals( + isValidCounter(Number.MAX_VALUE), + false, + "Unsafely large integers are invalid", + ); +}); + +Deno.test("formatASN()", async (t) => { + assertEquals(formatASN(10, 1, { ...TEST_CONFIG }), "ASN10001"); + assertEquals(formatASN(10, 1111, { ...TEST_CONFIG }), "ASN101111"); + assertEquals( + formatASN(911, 11, { + ...TEST_CONFIG, + ASN_ENABLE_NAMESPACE_EXTENSION: true, + }), + "ASN911011", + ); + assertEquals( + formatASN(10, 1, { ...TEST_CONFIG, ASN_PREFIX: "XXX" }), + "XXX10001", + ); + + // Invalid namespace + await t.step("Invalid namespace", () => { + assertThrows( + () => formatASN(100, 1, { ...TEST_CONFIG, ASN_NAMESPACE_RANGE: 10 }), + "Throw on out-of-range namespace (too large)", + ); + assertThrows( + () => formatASN(1, 1, { ...TEST_CONFIG, ASN_NAMESPACE_RANGE: 10 }), + "Throw on out-of-range namespace (too small)", + ); + assertThrows( + () => formatASN(-1, 1, { ...TEST_CONFIG, ASN_NAMESPACE_RANGE: 10 }), + "Throw on invalid namespace (negative)", + ); + assertThrows( + () => + formatASN(911, 11, { + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 10, + ASN_ENABLE_NAMESPACE_EXTENSION: false, + }), + "Throw on out-of-range namespace (too large) with extension disabled", + ); + }); + + // Invalid counter + await t.step("Invalid counter", () => { + assertThrows( + () => formatASN(10, -1, { ...TEST_CONFIG, ASN_NAMESPACE_RANGE: 10 }), + "Should throw on negative counter", + ); + }); }); diff --git a/lib/common/asn.ts b/lib/common/asn.ts index 3fab623..8ca942f 100644 --- a/lib/common/asn.ts +++ b/lib/common/asn.ts @@ -5,6 +5,7 @@ import { getCounterPath, getMaximumGenericRangeNamespace, getMinimumGenericRangeNamespace, + isValidNamespace, performAtomicTransaction, } from "$common/mod.ts"; @@ -37,10 +38,10 @@ export interface ASNData { metadata: Record; } -function getCurrentNamespace(): number { +function getCurrentNamespace(config = CONFIG): number { const date = Date.now(); - const range = getMinimumGenericRangeNamespace(); - return range + date % (CONFIG.ASN_NAMESPACE_RANGE - range); + const range = getMinimumGenericRangeNamespace(config); + return range + date % (config.ASN_NAMESPACE_RANGE - range); } /** @@ -108,9 +109,7 @@ export async function generateASN( }); const asnData = { - asn: `${CONFIG.ASN_PREFIX}${namespace}${ - counter.toString().padStart(3, "0") - }`, + asn: formatASN(namespace, counter), namespace, prefix: CONFIG.ASN_PREFIX, counter: counter, @@ -127,40 +126,85 @@ export async function generateASN( return asnData; } +/** + * Validates a counter to ensure it is a valid counter to use in an ASN. + * A valid counter is a safe integer greater than or equal to 0. + * @param counter the counter to validate + * @param config the configuration to use for validation. Defaults to the global configuration. + * @returns `true` if the counter is valid, `false` otherwise + */ +export function isValidCounter(counter: number): boolean { + return counter >= 0 && Number.isSafeInteger(counter); +} + +/** + * Formats a namespace and a counter into a full ASN string. + * @param namespace the namespace number + * @param counter the counter number + * @param config The configuration to use for formatting. Defaults to the global configuration. + * @returns the full ASN string including the configured prefix + */ +export function formatASN( + namespace: number, + counter: number, + config = CONFIG, +): string { + if (!isValidNamespace(namespace, config)) { + throw new Error("Invalid namespace: " + namespace); + } + + if (!isValidCounter(counter)) { + throw new Error( + "Invalid counter: " + counter + + " (must be a safe integer >= 0)", + ); + } + + return `${config.ASN_PREFIX}${namespace}${ + counter.toString().padStart(3, "0") + }`; +} + /** * Returns a human-readable description of the ASN format. + * @param config The configuration to use for the description. Defaults to the global configuration. * @returns a human-readable description of the ASN format that explains the prefix, the namespace, and the counter. * @remark The description is intended to be used in the console output or other monospaced text. */ -export function getFormatDescription(): string { +export function getFormatDescription(config = CONFIG): string { + const { + ASN_PREFIX, + ASN_NAMESPACE_RANGE, + ASN_ENABLE_NAMESPACE_EXTENSION, + } = config; + const minimumGenericNamespace = getMinimumGenericRangeNamespace(config); + const maximumGenericNamespace = getMaximumGenericRangeNamespace(config); + + const paddedPrefix = ASN_PREFIX.padEnd(4); + const paddedNamespace = minimumGenericNamespace + .toString() + .padEnd(4); + + const manualNamespaceEnd = minimumGenericNamespace * 10 - 1 - + (ASN_ENABLE_NAMESPACE_EXTENSION ? minimumGenericNamespace : 0); + + const namespaceExtensionRanges = ASN_ENABLE_NAMESPACE_EXTENSION + ? ",\n" + + ` - ${nthNinerExtensionRange(1, ASN_NAMESPACE_RANGE).join("-")},\n` + + ` - ${nthNinerExtensionRange(2, ASN_NAMESPACE_RANGE).join("-")},\n` + + ` - ${ + nthNinerExtensionRange(3, ASN_NAMESPACE_RANGE).join("-") + }, etc., are` + : " is"; + return `Configured ASN Format:\n` + - `${CONFIG.ASN_PREFIX.padEnd(4)} - ${ - getMinimumGenericRangeNamespace().toString().padEnd(4) - } - 001\n` + + `${paddedPrefix} - ${paddedNamespace} - 001\n` + `(1) - (2) - (3)\n` + `\n` + - `(1) Prefix specified in configuration (${CONFIG.ASN_PREFIX}).\n` + + `(1) Prefix specified in configuration (${ASN_PREFIX}).\n` + `(2) Numeric Namespace, whereas\n` + - ` - ${getMinimumGenericRangeNamespace()}-${getMaximumGenericRangeNamespace()} is reserved for automatic generation, and\n` + - ` - ${CONFIG.ASN_NAMESPACE_RANGE}-${ - getMinimumGenericRangeNamespace() * 10 - 1 - - (CONFIG.ASN_ENABLE_NAMESPACE_EXTENSION - ? getMinimumGenericRangeNamespace() - : 0) - }${ - CONFIG.ASN_ENABLE_NAMESPACE_EXTENSION - ? ",\n" + - ` - ${ - nthNinerExtensionRange(1, CONFIG.ASN_NAMESPACE_RANGE).join("-") - },\n` + - ` - ${ - nthNinerExtensionRange(2, CONFIG.ASN_NAMESPACE_RANGE).join("-") - },\n` + - ` - ${ - nthNinerExtensionRange(3, CONFIG.ASN_NAMESPACE_RANGE).join("-") - }, etc., are` - : " is" - } reserved for user defined namespaces.\n` + + ` - ${minimumGenericNamespace}-${maximumGenericNamespace} is reserved for automatic generation, and\n` + + ` - ${ASN_NAMESPACE_RANGE}-${manualNamespaceEnd}${namespaceExtensionRanges} reserved for user defined namespaces.\n` + ` The user defined namespace can be used for pre-printed ASN barcodes and the like.\n` + `(3) Counter, starting from 001, incrementing with each new ASN in the namespace.\n` + ` After 999, another digit is added.`; @@ -197,12 +241,54 @@ export function nthNinerExtensionRange( /** * Validates an ASN to ensure it matches the format specified by the current configuration. * @param asn ASN to validate + * @param config The configuration to use for validation. Defaults to the global configuration. * @returns `true` if the ASN is valid, `false` otherwise */ -export function isValidASN(asn: string): boolean { +export function isValidASN(asn: string, config = CONFIG): boolean { return new RegExp( - `^(${CONFIG.ASN_PREFIX})?(\\d{${ - `${CONFIG.ASN_NAMESPACE_RANGE}`.length + `^(${config.ASN_PREFIX})?(\\d{${ + `${config.ASN_NAMESPACE_RANGE}`.length }})(\\d{3})\\d*$`, ).test(asn); } + +/** + * Parses an ASN string into an {@link ASNData} object. + * The ASN string must match the format specified by the current configuration. + * The ASN prefix is optional and can be omitted. + * + * @throws Error if the ASN is invalid + * @param asn the ASN to parse + * @param config The configuration to use for parsing. Defaults to the global configuration. + * @returns An {@link ASNData} object with the parsed ASN data + */ +export function parseASN(asn: string, config = CONFIG): ASNData { + if (!isValidASN(asn, config)) { + throw new Error("Invalid ASN"); + } + + if (asn.startsWith(config.ASN_PREFIX)) { + asn = asn.slice(config.ASN_PREFIX.length); + } + + let strNamespace = ""; + + while (config.ASN_ENABLE_NAMESPACE_EXTENSION && asn.startsWith("9")) { + strNamespace += asn[0]; + asn = asn.slice(1); + } + + strNamespace += asn.slice(0, `${config.ASN_NAMESPACE_RANGE}`.length); + asn = asn.slice(`${config.ASN_NAMESPACE_RANGE}`.length); + + const namespace = Number.parseInt(strNamespace); + const counter = Number.parseInt(asn); + + return { + asn: formatASN(namespace, counter, config), + prefix: CONFIG.ASN_PREFIX, + namespace, + counter, + metadata: {}, + }; +} diff --git a/lib/common/namespaces.test.ts b/lib/common/namespaces.test.ts new file mode 100644 index 0000000..a3aa777 --- /dev/null +++ b/lib/common/namespaces.test.ts @@ -0,0 +1,148 @@ +import { assertEquals } from "@std/assert"; +import type { Config } from "$common/mod.ts"; +import { + allManagedNamespaces, + getMaximumGenericRangeNamespace, + getMinimumGenericRangeNamespace, + isManagedNamespace, + isValidNamespace, +} from "$common/namespaces.ts"; + +const TEST_CONFIG: Config = { + ASN_NAMESPACE_RANGE: 3, + ADDITIONAL_MANAGED_NAMESPACES: [{ + namespace: 5, + label: "Test namespace", + }], + PORT: 0, + ASN_PREFIX: "ASN", + ASN_ENABLE_NAMESPACE_EXTENSION: true, + ASN_LOOKUP_INCLUDE_PREFIX: true, + ASN_BARCODE_TYPE: "", +}; + +Deno.test("getMaximumGenericRangeNamespace()", () => { + assertEquals(getMaximumGenericRangeNamespace({ ...TEST_CONFIG }), 2); + assertEquals( + getMaximumGenericRangeNamespace({ + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 50, + }), + 49, + ); +}); + +Deno.test("getMinimumGenericRangeNamespace()", () => { + assertEquals(getMinimumGenericRangeNamespace({ ...TEST_CONFIG }), 1); + assertEquals( + getMinimumGenericRangeNamespace({ + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 50, + }), + 10, + ); +}); + +Deno.test("isValidNamespace()", async (t) => { + await t.step("Valid", () => { + assertEquals( + isValidNamespace(1, { ...TEST_CONFIG }), + true, + "1 is valid for range 3", + ); + assertEquals( + isValidNamespace(10, { ...TEST_CONFIG, ASN_NAMESPACE_RANGE: 20 }), + true, + "10 is valid for range 20", + ); + }); + + await t.step("Invalid", () => { + assertEquals( + isValidNamespace(10, { ...TEST_CONFIG }), + false, + "10 is invalid for range 3", + ); + assertEquals( + isValidNamespace(100, { ...TEST_CONFIG }), + false, + "100 is invalid for range 3", + ); + }); + + await t.step("Namespace extension", () => { + assertEquals( + isValidNamespace(91, { + ...TEST_CONFIG, + ASN_ENABLE_NAMESPACE_EXTENSION: true, + }), + true, + "91 is valid for range 3 with namespace extension enabled", + ); + assertEquals( + isValidNamespace(91, { + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 50, + ASN_ENABLE_NAMESPACE_EXTENSION: false, + }), + true, + "91 is valid for range 50 with namespace extension disabled", + ); + assertEquals( + isValidNamespace(9, { + ...TEST_CONFIG, + ASN_ENABLE_NAMESPACE_EXTENSION: true, + }), + false, + "9 is invalid for range 3 with namespace extension enabled", + ); + }); +}); + +Deno.test("isManagedNamespace()", () => { + assertEquals( + isManagedNamespace(1, { ...TEST_CONFIG }), + true, + "Generic namespace is managed", + ); + assertEquals( + isManagedNamespace(2, { ...TEST_CONFIG }), + true, + "Generic namespace is managed", + ); + assertEquals( + isManagedNamespace(3, { ...TEST_CONFIG }), + false, + "Unregistered non-generic namespace is not managed", + ); + assertEquals( + isManagedNamespace(5, { ...TEST_CONFIG }), + true, + "Registered non-generic namespace is managed", + ); +}); + +Deno.test("allManagedNamespaces()", () => { + assertEquals( + allManagedNamespaces({ ...TEST_CONFIG }), + [1, 2, 5], + "Include generic and additional managed namespaces", + ); + assertEquals( + allManagedNamespaces({ + ...TEST_CONFIG, + ADDITIONAL_MANAGED_NAMESPACES: [], + }), + [1, 2], + "Include only generic managed namespaces if no additional namespaces are registered", + ); + assertEquals( + allManagedNamespaces({ + ...TEST_CONFIG, + ASN_NAMESPACE_RANGE: 50, + ADDITIONAL_MANAGED_NAMESPACES: [], + }), + new Array(40).fill(0).map((_, i) => i + 10), + "Also works with larger ranges", + ); +}); diff --git a/lib/common/namespaces.ts b/lib/common/namespaces.ts index 5041c20..4d62208 100644 --- a/lib/common/namespaces.ts +++ b/lib/common/namespaces.ts @@ -5,16 +5,16 @@ import { CONFIG } from "$common/mod.ts"; * This includes the generic namespaces and the additional managed namespaces. * @returns all managed namespaces */ -export function allManagedNamespaces(): number[] { - const minGeneric = getMinimumGenericRangeNamespace(); - const maxGeneric = getMaximumGenericRangeNamespace(); +export function allManagedNamespaces(config = CONFIG): number[] { + const minGeneric = getMinimumGenericRangeNamespace(config); + const maxGeneric = getMaximumGenericRangeNamespace(config); return [ ...Array.from( - { length: maxGeneric - minGeneric }, + { length: maxGeneric - minGeneric + 1 }, (_, i) => i + minGeneric, ), - ...CONFIG.ADDITIONAL_MANAGED_NAMESPACES.map((v) => v.namespace), + ...config.ADDITIONAL_MANAGED_NAMESPACES.map((v) => v.namespace), ]; } @@ -23,8 +23,8 @@ export function allManagedNamespaces(): number[] { * This is the maximum value smaller than the `ASN_NAMESPACE_RANGE` configuration parameter. * @returns the maximum namespace value for the generic range */ -export function getMaximumGenericRangeNamespace(): number { - return CONFIG.ASN_NAMESPACE_RANGE - 1; +export function getMaximumGenericRangeNamespace(config = CONFIG): number { + return config.ASN_NAMESPACE_RANGE - 1; } /** @@ -33,9 +33,42 @@ export function getMaximumGenericRangeNamespace(): number { * the `ASN_NAMESPACE_RANGE` configuration parameter. * @returns the minimum namespace value for the generic range */ -export function getMinimumGenericRangeNamespace(): number { +export function getMinimumGenericRangeNamespace(config = CONFIG): number { return Number.parseInt( - "1" + "0".repeat(CONFIG.ASN_NAMESPACE_RANGE.toString().length - 1), + "1" + "0".repeat(config.ASN_NAMESPACE_RANGE.toString().length - 1), + ); +} + +/** + * Checks if a namespace is a valid namespace according to the configuration. + * Note that "valid" does not necessarily mean "managed". + * A valid namespace is any namespace that fulfills the following conditions: + * - The namespace is a safe integer, and + * - either: + * - The namespace has the same number of digits as the `ASN_NAMESPACE_RANGE` + * configuration parameter, or + * - The namespace has the same number of digits as the `ASN_NAMESPACE_RANGE` + * configuration parameter without leading nines if `ASN_ENABLE_NAMESPACE_EXTENSION` + * is enabled. + * + * @param namespace the namespace to check + * @returns `true` if the namespace is a valid namespace, `false` otherwise + */ +export function isValidNamespace(namespace: number, config = CONFIG): boolean { + if ( + !Number.isSafeInteger(namespace) || + namespace < getMinimumGenericRangeNamespace(config) + ) { + return false; + } + + let sNamespace = namespace.toString(); + if (config.ASN_ENABLE_NAMESPACE_EXTENSION) { + sNamespace = sNamespace.replace(/^9+/, ""); + } + + return ( + sNamespace.length === `${config.ASN_NAMESPACE_RANGE}`.length ); } @@ -47,14 +80,17 @@ export function getMinimumGenericRangeNamespace(): number { * @param namespace the namespace to check * @returns `true` if the namespace is managed by the system, `false` otherwise */ -export function isManagedNamespace(namespace: number): boolean { - if (namespace < getMinimumGenericRangeNamespace()) { +export function isManagedNamespace( + namespace: number, + config = CONFIG, +): boolean { + if (namespace < getMinimumGenericRangeNamespace(config)) { return false; } - if (namespace <= getMaximumGenericRangeNamespace()) { + if (namespace <= getMaximumGenericRangeNamespace(config)) { return true; } - return CONFIG.ADDITIONAL_MANAGED_NAMESPACES.some((v) => + return config.ADDITIONAL_MANAGED_NAMESPACES.some((v) => v.namespace === namespace ); } diff --git a/lib/common/time-stats.test.ts b/lib/common/time-stats.test.ts index 8f63710..e7948fc 100644 --- a/lib/common/time-stats.test.ts +++ b/lib/common/time-stats.test.ts @@ -6,14 +6,14 @@ Deno.test("TimeStats", () => { assertEquals(stats.getHighestRate(2), 0); const stats2 = stats.withNewTimestamp(); assertAlmostEquals(stats2.avg, 10000, 100); - assertAlmostEquals(stats2.sd, 0, 100); + assertAlmostEquals(stats2.sd, 0, 10); assertEquals(stats2.count, 1); const stats3 = stats2.withNewTimestamp(); - assertAlmostEquals(stats3.avg, 5000, 100); - assertAlmostEquals(stats3.sd, 5000, 10); + assertAlmostEquals(stats3.avg, 5000, 50); + assertAlmostEquals(stats3.sd, 5000, 50); const stats4 = stats3.withNewTimestamp(); - assertAlmostEquals(stats4.avg, 3333, 100); - assertAlmostEquals(stats4.sd, 4714, 10); + assertAlmostEquals(stats4.avg, 3333, 33); + assertAlmostEquals(stats4.sd, 4714, 47); const stats5 = stats4 .withNewTimestamp() .withNewTimestamp() @@ -22,6 +22,6 @@ Deno.test("TimeStats", () => { .withNewTimestamp() .withNewTimestamp() .withNewTimestamp(); - assertAlmostEquals(stats5.avg, 1000, 100); - assertAlmostEquals(stats5.sd, 3000, 10); + assertAlmostEquals(stats5.avg, 1000, 10); + assertAlmostEquals(stats5.sd, 3000, 30); });