Skip to content
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
12 changes: 6 additions & 6 deletions src/ensure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { typeChecker } from "./typeChecker.ts";
import type { TypeDefinition } from "./types.ts";

/**
* It ensures that the input matches the definition and returns the input.
* The returned value is guaranteed to be of the type defined by the definition.
* Ensures that the input matches the definition and returns it.
* The returned value is guaranteed to match the defined type.
* @example
* ```typescript
* const isProduct = typeChecker({
Expand All @@ -14,13 +14,13 @@ import type { TypeDefinition } from "./types.ts";
* const response = await fetch("https://api.example.com/product/1");
* const product = ensure(await response.json(), isProduct);
* ```
* @param input value to be checked.
* @param definition definition of the type.
* @returns input value as the defined type.
* @param input The value to check.
* @param definition The type definition.
* @returns The input value as the defined type.
*/
export const ensure = <const T>(
input: unknown,
definition: TypeDefinition<T>,
definition: TypeDefinition<NoInfer<T>>,
): T => {
const error = typeChecker(definition).test(input);
if (error) {
Expand Down
2 changes: 1 addition & 1 deletion src/is/DomainName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const isDomainName: TypeChecker<DomainName> = typeChecker(
if (!isString(input)) {
return false;
}
/** Initial value is hyphen to return false for .example.com */
/** Initialize with a hyphen so `.example.com` is rejected. */
let lastCodePoint = HYPHEN_MINUS;
let currentLabelIsValid = false;
let labelCount = 1;
Expand Down
2 changes: 1 addition & 1 deletion src/is/Ipv4Address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const isIpv4Address: TypeChecker<Ipv4Address> = typeChecker(
const result = parseIpv4Address(input);
return result.end === input.length;
} catch {
// do nothing
// Ignore parse errors.
}
}
return false;
Expand Down
2 changes: 1 addition & 1 deletion src/is/Ipv6Address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const isIpv6Address: TypeChecker<Ipv6Address> = typeChecker(
const result = parseIpv6Address(input);
return result.end === input.length;
} catch {
// do nothing
// Ignore parse errors.
}
}
return false;
Expand Down
20 changes: 16 additions & 4 deletions src/parseIpv4Address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,33 @@ export interface Ipv4AddressParseResult {
end: number;
}

/**
* Argument tuple for `parseIpv4Address`.
*/
export type ParseIpv4AddressArgs = [
input: string,
...rest: [start: number] | [],
];

const isIpv4OctetTuple = (
octets: Array<number>,
): octets is Ipv4AddressParseResult["octets"] => octets.length === 4;

/**
* Parses an IPv4 address.
* @param input - The input string.
* @param start - The index to start from.
* @returns The result of parsing an IPv4 address.
*/
export const parseIpv4Address = (
input: string,
start = 0,
...[input, start = 0]: ParseIpv4AddressArgs
): Ipv4AddressParseResult => {
const octets: Array<number> = [];
for (const octet of listIpv4Octets(input, start)) {
if (octets.push(octet.value) === 4) {
octets.push(octet.value);
if (isIpv4OctetTuple(octets)) {
return {
octets: octets as [number, number, number, number],
octets,
start,
end: octet.end,
};
Expand Down
30 changes: 25 additions & 5 deletions src/parseIpv6Address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,26 @@ export interface Ipv6AddressParseResult {
end: number;
}

/**
* Argument tuple for `parseIpv6Address`.
*/
export type ParseIpv6AddressArgs = [
input: string,
...rest: [start: number] | [],
];

const isIpv6AddressGroups = (
groups: Array<number>,
): groups is Ipv6AddressGroups => groups.length === 8;

/**
* Parses an IPv6 address.
* @param input - The input string.
* @param start - The index to start from.
* @returns The result of parsing an IPv6 address.
*/
export const parseIpv6Address = (
input: string,
start = 0,
...[input, start = 0]: ParseIpv6AddressArgs
): Ipv6AddressParseResult => {
const groups: Array<number> = [];
let compressorIndex = -1;
Expand All @@ -146,15 +157,21 @@ export const parseIpv6Address = (
const length = groups.push(value);
if (0 <= compressorIndex && length === 7) {
groups.splice(compressorIndex, 0, 0);
if (!isIpv6AddressGroups(groups)) {
throw new Error(`InvalidIpv6Address: ${input.substr(start, end)}`);
}
return {
groups: groups as Ipv6AddressGroups,
groups,
start,
end,
};
}
if (length === 8) {
if (!isIpv6AddressGroups(groups)) {
throw new Error(`InvalidIpv6Address: ${input.substr(start, end)}`);
}
return {
groups: groups as Ipv6AddressGroups,
groups,
start,
end,
};
Expand All @@ -169,8 +186,11 @@ export const parseIpv6Address = (
result.push(0);
}
result.push(...groups.slice(compressorIndex));
if (!isIpv6AddressGroups(result)) {
throw new Error(`InvalidIpv6Address: ${input.substr(start, end)}`);
}
return {
groups: result as Ipv6AddressGroups,
groups: result,
start,
end,
};
Expand Down
57 changes: 31 additions & 26 deletions src/typeChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const is$String = (v: unknown): v is string => typeof v === "string";
const is$RegExp = (v: unknown): v is RegExp => getType(v) === "RegExp";
const is$Set = (v: unknown): v is Set<unknown> => getType(v) === "Set";
const is$Function = (v: unknown): v is Callable => typeof v === "function";
const is$Object = (v: unknown): v is Record<string, unknown> =>
const is$Object = (v: unknown): v is Record<PropertyKey, unknown> =>
is$Function(v) || (typeof v === "object" && v !== null);
const lazy = <T>(getter: () => T): (() => T) => {
let cached: { value: T } | null = null;
Expand All @@ -36,7 +36,7 @@ interface FactoryProps<T> {
test?(
this: TypeChecker<T>,
input: unknown,
route?: Array<string>,
route?: Array<string | number | symbol>,
): Error | null;
}

Expand All @@ -57,9 +57,9 @@ const getCache = <T>(context: object) => {
export const typeCheckerConfig: {
/** Clears the cache. */
clearCache(): void;
/** Gets the count of types without names. */
/** Returns the number of unnamed types. */
getNoNameTypeCount(): number;
/** Resets the count of types without names. */
/** Resets the unnamed type counter. */
resetNoNameTypeCount(count?: number): void;
} = {
clearCache() {
Expand Down Expand Up @@ -90,11 +90,7 @@ const factory =
if (cached) {
return cached.value;
}
if (is$Object(arg)) {
cache.set(arg, { value: typeGuard as TypeChecker<T> });
}
checkerCache.set(typeGuard, { value: typeGuard as TypeChecker<T> });
return defineProperties<TypeChecker<T>, TypeGuard<T>>(typeGuard, {
const checker = defineProperties<TypeChecker<T>, TypeGuard<T>>(typeGuard, {
test: test
? { value: test }
: {
Expand All @@ -112,6 +108,11 @@ const factory =
},
},
});
if (is$Object(arg)) {
cache.set(arg, { value: checker });
}
checkerCache.set(typeGuard, { value: checker });
return checker;
};

/**
Expand Down Expand Up @@ -194,7 +195,7 @@ export const isDictionaryOf: <const T>(
);

/**
* Create a type checker from a type definition.
* Creates a type checker from a type definition.
* @example
* ```typescript
* // Create a type checker from a type guard function.
Expand Down Expand Up @@ -267,17 +268,26 @@ export const typeChecker: <const T>(
},
};
}
type PropertyTuple<K extends keyof T = keyof T> = [K, TypeChecker<T[K]>];
type PropertyTuple<K extends keyof T = keyof T> = readonly [
K,
TypeChecker<T[K]>,
];
const toPropertyTuple = <K extends keyof T>(k: K): PropertyTuple<K> => {
const propertyPath = `${typeName}.${String(k)}`;
return [k, typeChecker(d[k], propertyPath)];
};
const properties = lazy(
(): Array<PropertyTuple> => [
...(function* () {
for (const k of keys(d)) {
const propertyPath = `${typeName}.${String(k)}`;
yield [k, typeChecker(d[k], propertyPath)] as PropertyTuple;
}
})(),
],
(): Array<PropertyTuple> => keys(d).map((k) => toPropertyTuple(k)),
);
type TypeGuardWithRefs<U> = (
input: unknown,
refs?: WeakMap<WeakKey, string>,
) => input is U;
const runGuard = <U>(
checker: TypeChecker<U>,
input: unknown,
refs: WeakMap<WeakKey, string>,
): boolean => (checker as TypeGuardWithRefs<U>)(input, refs);
const typeGuard = (
v: unknown,
refs = new WeakMap<WeakKey, string>(),
Expand All @@ -289,12 +299,7 @@ export const typeChecker: <const T>(
throw new Error(`CircularReference: ${refs.get(v)} -> ${typeName}`);
}
refs.set(v, typeName);
return properties().every(([k, is]) =>
(is as (input: T[typeof k], refs: WeakMap<WeakKey, string>) => boolean)(
(v as T)[k],
refs,
),
);
return properties().every(([k, is]) => runGuard(is, v[k], refs));
};
return {
typeGuard,
Expand All @@ -320,7 +325,7 @@ export const typeChecker: <const T>(
return new TypeCheckError(this, input);
}
for (const [k, pd] of properties()) {
const error = pd.test((input as T)[k], route.concat(k));
const error = pd.test(input[k], route.concat(k));
if (error) {
return error;
}
Expand Down