Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[typeid-js] Add explicitly defined error types #430

Merged
merged 1 commit into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions typeid/typeid-js/src/typeid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getType,
fromString,
} from "./unboxed/typeid";
import { TypeIDConversionError } from "./unboxed/error";

export class TypeID<const T extends string> {
constructor(private prefix: T, private suffix: string = "") {
Expand All @@ -27,9 +28,7 @@ export class TypeID<const T extends string> {
public asType<const U extends string>(prefix: U): TypeID<U> {
const self = this as unknown as TypeID<U>;
if (self.prefix !== prefix) {
throw new Error(
`Cannot convert TypeID of type ${self.prefix} to type ${prefix}`
);
throw new TypeIDConversionError(self.prefix, prefix);
}
return self;
}
Expand Down
41 changes: 41 additions & 0 deletions typeid/typeid-js/src/unboxed/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export class InvalidPrefixError extends Error {
constructor(prefix: string) {
super(`Invalid prefix "${prefix}". Must be at most 63 ASCII letters [a-z_]`);
this.name = "InvalidPrefixError";
}
}

export class PrefixMismatchError extends Error {
constructor(expected: string, actual: string) {
super(`Invalid TypeId. Prefix mismatch. Expected ${expected}, got ${actual}`);
this.name = "PrefixMismatchError";
}
}

export class EmptyPrefixError extends Error {
constructor(typeId: string) {
super(`Invalid TypeId. Prefix cannot be empty when there's a separator: ${typeId}`);
this.name = "EmptyPrefixError";
}
}

export class InvalidSuffixLengthError extends Error {
constructor(length: number) {
super(`Invalid length. Suffix should have 26 characters, got ${length}`);
this.name = "InvalidSuffixLengthError";
}
}

export class InvalidSuffixCharacterError extends Error {
constructor(firstChar: string) {
super(`Invalid suffix. First character "${firstChar}" must be in the range [0-7]`);
this.name = "InvalidSuffixCharacterError";
}
}

export class TypeIDConversionError extends Error {
constructor(actualPrefix: string, expectedPrefix: string) {
super(`Cannot convert TypeID of type ${actualPrefix} to type ${expectedPrefix}`);
this.name = "TypeIDConversionError";
}
}
27 changes: 13 additions & 14 deletions typeid/typeid-js/src/unboxed/typeid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import { stringify, v7 } from "uuid";
import { parseUUID } from "../parse_uuid";
import { encode, decode } from "../base32";
import { isValidPrefix } from "../prefix";
import {
EmptyPrefixError,
InvalidPrefixError,
InvalidSuffixCharacterError,
InvalidSuffixLengthError,
PrefixMismatchError,
} from "./error";

export type TypeId<T> = string & { __type: T };

Expand All @@ -10,7 +17,7 @@ export function typeidUnboxed<T extends string>(
suffix: string = ""
): TypeId<T> {
if (!isValidPrefix(prefix)) {
throw new Error("Invalid prefix. Must be at most 63 ascii letters [a-z_]");
throw new InvalidPrefixError(prefix);
}

let finalSuffix: string;
Expand All @@ -23,15 +30,11 @@ export function typeidUnboxed<T extends string>(
}

if (finalSuffix.length !== 26) {
throw new Error(
`Invalid length. Suffix should have 26 characters, got ${finalSuffix.length}`
);
throw new InvalidSuffixLengthError(finalSuffix.length);
}

if (finalSuffix[0] > "7") {
throw new Error(
"Invalid suffix. First character must be in the range [0-7]"
);
throw new InvalidSuffixCharacterError(finalSuffix[0]);
}

// Validate the suffix by decoding it. If it's invalid, an error will be thrown.
Expand Down Expand Up @@ -73,20 +76,16 @@ export function fromString<T extends string>(
s = typeId.substring(underscoreIndex + 1);

if (!p) {
throw new Error(
`Invalid TypeId. Prefix cannot be empty when there's a separator: ${typeId}`
);
throw new EmptyPrefixError(typeId);
}
}

if (!s) {
throw new Error(`Invalid TypeId. Suffix cannot be empty`);
throw new InvalidSuffixLengthError(0);
}

if (prefix && p !== prefix) {
throw new Error(
`Invalid TypeId. Prefix mismatch. Expected ${prefix}, got ${p}`
);
throw new PrefixMismatchError(prefix, p);
}

return typeidUnboxed(p, s);
Expand Down
26 changes: 12 additions & 14 deletions typeid/typeid-js/test/typeid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { describe, expect, it } from "@jest/globals";
import { typeid, TypeID } from "../src/typeid";
import validJson from "./valid";
import invalidJson from "./invalid";
import {
InvalidPrefixError,
InvalidSuffixCharacterError,
InvalidSuffixLengthError,
PrefixMismatchError,
} from "../src/unboxed/error";

describe("TypeID", () => {
describe("constructor", () => {
Expand All @@ -28,15 +34,11 @@ describe("TypeID", () => {
it("should throw an error if prefix is not lowercase", () => {
expect(() => {
typeid("TEST", "00041061050r3gg28a1c60t3gf");
}).toThrowError(
"Invalid prefix. Must be at most 63 ascii letters [a-z_]"
);
}).toThrowError(new InvalidPrefixError("TEST"));

expect(() => {
typeid(" ", "00041061050r3gg28a1c60t3gf");
}).toThrowError(
"Invalid prefix. Must be at most 63 ascii letters [a-z_]"
);
}).toThrowError(new InvalidPrefixError(" "));
});

it("should throw an error if suffix length is not 26", () => {
Expand Down Expand Up @@ -111,29 +113,25 @@ describe("TypeID", () => {

expect(() => {
TypeID.fromString(invalidStr);
}).toThrowError(
new Error(`Invalid suffix. First character must be in the range [0-7]`)
);
}).toThrowError(new InvalidSuffixCharacterError("u"));
});
it("should throw an error with wrong prefix", () => {
const str = "prefix_00041061050r3gg28a1c60t3gf";
expect(() => {
TypeID.fromString(str, "wrong");
}).toThrowError(
new Error(`Invalid TypeId. Prefix mismatch. Expected wrong, got prefix`)
);
}).toThrowError(new PrefixMismatchError("wrong", "prefix"));
});
it("should throw an error for empty TypeId string", () => {
const invalidStr = "";
expect(() => {
TypeID.fromString(invalidStr);
}).toThrowError(new Error(`Invalid TypeId. Suffix cannot be empty`));
}).toThrowError(new InvalidSuffixLengthError(0));
});
it("should throw an error for TypeId string with empty suffix", () => {
const invalidStr = "prefix_";
expect(() => {
TypeID.fromString(invalidStr);
}).toThrowError(new Error(`Invalid TypeId. Suffix cannot be empty`));
}).toThrowError(new InvalidSuffixLengthError(0));
});
});

Expand Down
21 changes: 10 additions & 11 deletions typeid/typeid-js/test/typeid_unboxed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import {
} from "../src/unboxed/typeid";
import validJson from "./valid";
import invalidJson from "./invalid";
import {
InvalidPrefixError,
InvalidSuffixCharacterError,
InvalidSuffixLengthError,
} from "../src/unboxed/error";

describe("TypeId Functions", () => {
describe("typeidUnboxed", () => {
Expand All @@ -38,15 +43,11 @@ describe("TypeId Functions", () => {
it("should throw an error if prefix is not lowercase", () => {
expect(() => {
typeidUnboxed("TEST", "00041061050r3gg28a1c60t3gf");
}).toThrowError(
"Invalid prefix. Must be at most 63 ascii letters [a-z_]"
);
}).toThrowError(new InvalidPrefixError("TEST"));

expect(() => {
typeidUnboxed(" ", "00041061050r3gg28a1c60t3gf");
}).toThrowError(
"Invalid prefix. Must be at most 63 ascii letters [a-z_]"
);
}).toThrowError(new InvalidPrefixError(" "));
});

it("should throw an error if suffix length is not 26", () => {
Expand Down Expand Up @@ -80,25 +81,23 @@ describe("TypeId Functions", () => {

expect(() => {
fromString(invalidStr);
}).toThrowError(
new Error(`Invalid suffix. First character must be in the range [0-7]`)
);
}).toThrowError(new InvalidSuffixCharacterError("u"));
});

it("should throw an error for empty TypeId string", () => {
const invalidStr = "";

expect(() => {
fromString(invalidStr);
}).toThrowError(new Error(`Invalid TypeId. Suffix cannot be empty`));
}).toThrowError(new InvalidSuffixLengthError(0));
});

it("should throw an error for TypeId string with empty suffix", () => {
const invalidStr = "prefix_";

expect(() => {
fromString(invalidStr);
}).toThrowError(new Error(`Invalid TypeId. Suffix cannot be empty`));
}).toThrowError(new InvalidSuffixLengthError(0));
});
});

Expand Down
Loading