Initial commit

This commit is contained in:
Endeavorance 2025-03-25 16:15:04 -04:00
commit 1baf20f6c0
9 changed files with 825 additions and 0 deletions

246
src/index.ts Normal file
View file

@ -0,0 +1,246 @@
declare const __parsed_brand: unique symbol;
export type Brand<T, B> = T & { [__parsed_brand]: B };
export type Parser<T> = (value: unknown) => T;
export class ParseError extends Error {
name = "ParseError";
shape: string;
path: string;
constructor(message: string, shape = "unknown", path = "") {
super(message);
this.shape = shape;
this.path = path;
}
}
const MismatchError = (expectedType: string, receivedValue: unknown) => {
return new ParseError(
`Expected: ${expectedType}, Received: ${typeof receivedValue} ${JSON.stringify(receivedValue)}`,
expectedType,
);
};
type TypeofType = "number" | "string" | "boolean" | "bigint" | "symbol";
const assertTypeof = <T>(val: unknown, expectedType: TypeofType): T => {
// biome-ignore lint/suspicious/useValidTypeof: Safe through types
if (typeof val !== expectedType) {
throw MismatchError(expectedType, val);
}
return val as T;
};
export const Parse = {
unknown(val: unknown): unknown {
return val;
},
number(val: unknown): number {
return assertTypeof<number>(val, "number");
},
string(val: unknown): string {
return assertTypeof<string>(val, "string");
},
boolean(val: unknown): boolean {
return assertTypeof<boolean>(val, "boolean");
},
bigint(val: unknown): bigint {
return assertTypeof<bigint>(val, "bigint");
},
symbol(val: unknown): symbol {
return assertTypeof<symbol>(val, "symbol");
},
null(val: unknown): null {
if (val === null) {
return val;
}
throw MismatchError("null", val);
},
undefined(val: unknown): undefined {
if (val === undefined) {
return val;
}
throw MismatchError("undefined", val);
},
literal<
T extends string | number | boolean | bigint | symbol | null | undefined,
>(literal: T): (val: unknown) => T {
return (val: unknown) => {
if (val === literal) {
return val as T;
}
throw MismatchError(String(literal), val);
};
},
int(val: unknown): number {
const asNum = Parse.number(val);
if (!Number.isInteger(asNum)) {
throw MismatchError("integer", asNum);
}
return asNum;
},
nan(val: unknown): number {
if (typeof val === "number" && Number.isNaN(val)) {
return val;
}
throw MismatchError("NaN", val);
},
instanceOf<T>(classDef: { new(): T }): (val: unknown) => T {
return (val: unknown) => {
if (val instanceof classDef) {
return val;
}
throw MismatchError(`instanceof ${classDef.name}`, val);
};
},
arrayOf<T>(
parser: (val: unknown) => T,
shapeName = "Array",
): (val: unknown) => T[] {
return (val: unknown) => {
if (Array.isArray(val)) {
return val.map((entry, index) => {
try {
return parser(entry);
} catch (error) {
if (error instanceof ParseError) {
error.path += `${shapeName}[${index}]`;
throw error;
}
throw error;
}
});
}
throw MismatchError(shapeName, val);
};
},
// biome-ignore lint/suspicious/noExplicitAny: Could be fixed; intentionally kept simpler
shape<T extends Record<string, Parser<any>>>(
parsers: T,
shapeName = "Shape",
): (val: unknown) => { [K in keyof T]: ReturnType<T[K]> } {
return (val: unknown) => {
if (val === null || typeof val !== "object") {
throw MismatchError("object", val);
}
const result = {} as { [K in keyof T]: ReturnType<T[K]> };
for (const key in parsers) {
if (!(key in val)) {
throw new ParseError(`Missing prop: ${key}`, shapeName);
}
const value = (val as Record<string, unknown>)[key];
try {
// biome-ignore lint/style/noNonNullAssertion: Logically guaranteed
result[key] = parsers[key]!(value);
} catch (error) {
if (error instanceof ParseError) {
error.path += `${shapeName}.${key}`;
throw error;
}
throw error;
}
}
return result;
};
},
regex<T extends string>(
regex: RegExp,
shapeName = "Regex String",
): (val: unknown) => T {
return (val: unknown) => {
const asStr = Parse.string(val);
if (regex.test(asStr)) {
return val as T;
}
throw MismatchError(`${shapeName} (${regex.toString()})`, asStr);
};
},
enum<E extends string>(
enumDef: readonly E[],
shapeName = "Enum",
): (val: unknown) => E {
return (val: unknown) => {
if (typeof val !== "string") {
throw MismatchError("string", val);
}
if (!enumDef.includes(val as E)) {
throw MismatchError(shapeName, val);
}
return val as E;
};
},
optional<T>(parser: (val: unknown) => T): (val: unknown) => T | undefined {
return (val: unknown) => {
if (val === undefined) {
return undefined;
}
return parser(val);
};
},
nullable<T>(parser: (val: unknown) => T): (val: unknown) => T | null {
return (val: unknown) => {
if (val === null) {
return null;
}
return parser(val);
};
},
oneOf<T extends Parser<unknown>[]>(
parsers: T,
shapeName = "Union",
): (value: unknown) => ReturnType<T[number]> {
return (value: unknown): ReturnType<T[number]> => {
const errors: Error[] = [];
for (const parser of parsers) {
try {
return parser(value) as ReturnType<T[number]>;
} catch (error) {
errors.push(
error instanceof Error ? error : new Error(String(error)),
);
}
}
throw MismatchError(shapeName, value);
};
},
} as const;

228
src/test/parse.test.ts Normal file
View file

@ -0,0 +1,228 @@
import { describe, expect, test } from "bun:test";
import { Parse, ParseError } from "../index.ts";
describe(Parse.unknown, () => {
test("returns the value as provided", () => {
const val = Parse.unknown("hello");
expect(val).toBe("hello");
});
});
describe(Parse.string, () => {
test("Parses strings", () => {
expect(Parse.string("hello")).toBe("hello");
});
test("Throws an error when parsing a non-string", () => {
const INVALID_VALUES = [null, undefined, 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.string(val)).toThrow(ParseError);
}
});
});
describe(Parse.number, () => {
test("Parses numbers", () => {
expect(Parse.number(123)).toBe(123);
});
test("Throws an error when parsing a non-number", () => {
const INVALID_VALUES = [null, undefined, "hello", true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.number(val)).toThrow(ParseError);
}
});
});
describe(Parse.boolean, () => {
test("Parses booleans", () => {
expect(Parse.boolean(true)).toBe(true);
expect(Parse.boolean(false)).toBe(false);
});
test("Throws an error when parsing a non-boolean", () => {
const INVALID_VALUES = [null, undefined, "hello", 123, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.boolean(val)).toThrow(ParseError);
}
});
});
describe(Parse.bigint, () => {
test("Parses bigints", () => {
expect(Parse.bigint(BigInt(123))).toBe(BigInt(123));
});
test("Throws an error when parsing a non-bigint", () => {
const INVALID_VALUES = [null, undefined, "hello", 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.bigint(val)).toThrow(ParseError);
}
});
});
describe(Parse.symbol, () => {
test("Parses symbols", () => {
const symbol = Symbol("hello");
expect(Parse.symbol(symbol)).toBe(symbol);
});
test("Throws an error when parsing a non-symbol", () => {
const INVALID_VALUES = [null, undefined, "hello", 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.symbol(val)).toThrow(ParseError);
}
});
});
describe(Parse.null, () => {
test("Parses null", () => {
expect(Parse.null(null)).toBe(null);
});
test("Throws an error when parsing a non-null", () => {
const INVALID_VALUES = [undefined, "hello", 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.null(val)).toThrow(ParseError);
}
});
});
describe(Parse.undefined, () => {
test("Parses undefined", () => {
expect(Parse.undefined(undefined)).toBe(undefined);
});
test("Throws an error when parsing a non-undefined", () => {
const INVALID_VALUES = [null, "hello", 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.undefined(val)).toThrow(ParseError);
}
});
});
describe(Parse.arrayOf, () => {
test("Parses arrays of a specific type", () => {
const arr = [1, 2, 3];
expect(Parse.arrayOf(Parse.number)(arr)).toEqual(arr);
});
test("Throws an error when parsing a non-array", () => {
expect(() => Parse.arrayOf(Parse.number)("hello")).toThrow(ParseError);
});
test("Throws an error when parsing an array with invalid values", () => {
const INVALID_VALUES = [null, undefined, "hello", true, false, {}];
for (const val of INVALID_VALUES) {
const arr = [1, val, 3];
expect(() => Parse.arrayOf(Parse.number)(arr)).toThrow(ParseError);
}
});
});
describe(Parse.shape, () => {
test("Parses objects", () => {
const obj = { name: "hello", age: 123 };
const shape = { name: Parse.string, age: Parse.number };
expect(Parse.shape(shape)(obj)).toEqual(obj);
});
test("Throws an error when parsing a non-object", () => {
expect(() => Parse.shape({})("hello")).toThrow(ParseError);
});
test("Throws an error when parsing an object with invalid values", () => {
const obj = { name: "hello", age: "world" };
const shape = { name: Parse.string, age: Parse.number };
expect(() => Parse.shape(shape)(obj)).toThrow(ParseError);
});
});
describe(Parse.enum, () => {
test("Parses valid enum values", () => {
const enumValues = ["hello", "world"] as const;
expect(Parse.enum(enumValues)("hello")).toBe("hello");
});
test("Throws an error when parsing an invalid enum value", () => {
const enumValues = ["hello", "world"] as const;
expect(() => Parse.enum(enumValues)("invalid")).toThrow(ParseError);
});
});
describe(Parse.regex, () => {
test("Parses valid regex values", () => {
expect(Parse.regex(/hello/)("hello")).toBe("hello");
});
test("Throws an error when parsing an invalid regex value", () => {
expect(() => Parse.regex(/hello/)("world")).toThrow(ParseError);
});
});
describe(Parse.optional, () => {
test("Parses undefined values", () => {
expect(Parse.optional(Parse.string)(undefined)).toBeUndefined();
});
});
describe(Parse.nullable, () => {
test("Parses null values", () => {
expect(Parse.nullable(Parse.string)(null)).toBeNull();
});
});
describe(Parse.nan, () => {
test("Parses NaN", () => {
expect(Parse.nan(Number.NaN)).toBe(Number.NaN);
});
test("Throws an error when parsing a non-NaN", () => {
const INVALID_VALUES = [null, undefined, "hello", 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.nan(val)).toThrow(ParseError);
}
});
});
class TestClass { }
describe(Parse.instanceOf, () => {
test("Parses instances of a class", () => {
const testClass = new TestClass();
expect(Parse.instanceOf(TestClass)(testClass)).toBe(testClass);
});
test("Throws an error when parsing a non-instance", () => {
const INVALID_VALUES = [null, undefined, "hello", 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.instanceOf(TestClass)(val)).toThrow(ParseError);
}
});
test("Throws an error when parsing an instance of a different class", () => {
const testClass = new TestClass();
expect(() => Parse.instanceOf(class AnotherClass { })(testClass)).toThrow(
ParseError,
);
});
});
describe(Parse.oneOf, () => {
test("Parses values that match one of the parsers", () => {
const parsers = [Parse.string, Parse.number];
expect(Parse.oneOf(parsers)("hello")).toEqual("hello");
});
test("Throws an error when parsing a value that doesn't match any parser", () => {
const parsers = [Parse.string, Parse.number];
const INVALID_VALUES = [null, undefined, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.oneOf(parsers)(val)).toThrow(ParseError);
}
});
});