293 lines
9.9 KiB
TypeScript
293 lines
9.9 KiB
TypeScript
const optional = Symbol();
|
|
const GlobalObject = Object;
|
|
const GlobalNumber = Number;
|
|
const GlobalArray = Array;
|
|
|
|
export namespace Type {
|
|
export function String<S extends string = string>(args: { description?: string, enum?: S[] } = {}) {
|
|
const result: TString<S> = {
|
|
type: 'string',
|
|
};
|
|
if (args.enum) {
|
|
result.enum = args.enum;
|
|
}
|
|
if (args.description) {
|
|
result.description = args.description;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function Number<N extends number = number>(args: { description?: string, enum?: N[] } = {}) {
|
|
const result: TNumber<N> = {
|
|
type: 'number',
|
|
};
|
|
if (args.enum) {
|
|
result.enum = args.enum;
|
|
}
|
|
if (args.description) {
|
|
result.description = args.description;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function Integer<N extends number = number>(args: { description?: string, enum?: N[] } = {}) {
|
|
const result: TNumber<N> = {
|
|
type: 'integer',
|
|
};
|
|
if (args.enum) {
|
|
result.enum = args.enum;
|
|
}
|
|
if (args.description) {
|
|
result.description = args.description;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function Boolean(args: { description?: string } = {}) {
|
|
const result: TBoolean = {
|
|
type: 'boolean',
|
|
};
|
|
if (args.description) {
|
|
result.description = args.description;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function Array<T extends TScheme = TScheme>(items: T, args: { description?: string } = {}) {
|
|
const result: TArray<T> = {
|
|
type: 'array',
|
|
items,
|
|
}
|
|
if (args.description) {
|
|
result.description = args.description;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function Union<T extends TScheme[]>(anyOf: [...T], args: { description?: string } = {}): TUnion<T> {
|
|
const result: TUnion<T> = { type: 'union', anyOf };
|
|
if (args.description) {
|
|
result.description = args.description;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function Optional<T extends TScheme = TScheme>(scheme: T): TOptional<T> {
|
|
const result = { ...scheme };
|
|
GlobalObject.defineProperty(result, optional, { value: true, enumerable: false, writable: false, configurable: false });
|
|
return result as unknown as TOptional<T>;
|
|
}
|
|
|
|
export function Object<T extends TProperties = TProperties>(properties: T, args: { description?: string } = {}) {
|
|
const result: TObject<T> = {
|
|
type: 'object',
|
|
properties,
|
|
required: GlobalObject.keys(properties)
|
|
.filter(key => !(properties[key] as any)[optional]),
|
|
}
|
|
if (args.description) {
|
|
result.description = args.description;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function Enum<S extends string, T extends TString<S>>(items: S[], args?: { description?: string }): T;
|
|
export function Enum<N extends number, T extends TNumber<N>>(items: N[], args?: { description?: string }): T;
|
|
export function Enum(items: (number | string)[], args: { description?: string } = {}) {
|
|
if (typeof items?.[0] === 'number') {
|
|
return Number({ enum: items.map(item => +item!), ...args });
|
|
}
|
|
return String({ enum: items.map(item => item!.toString()), ...args });
|
|
}
|
|
|
|
export interface CheckError {
|
|
path: string;
|
|
message: string;
|
|
}
|
|
|
|
function check(scheme: TScheme, value: unknown, path: string): CheckError[] {
|
|
if (value == null) {
|
|
if ((scheme as any)[optional]) return [];
|
|
return [{ path, message: `Expected ${scheme.type} at ${path}, got ${value}` }];
|
|
}
|
|
|
|
switch (scheme.type) {
|
|
case 'string': {
|
|
if (typeof value !== 'string') {
|
|
return [{ path, message: `Expected string at ${path}, got ${typeof value}` }];
|
|
}
|
|
if (scheme.enum && !scheme.enum.includes(value)) {
|
|
return [{ path, message: `Expected one of [${scheme.enum.join(', ')}] at ${path}, got "${value}"` }];
|
|
}
|
|
return [];
|
|
}
|
|
case 'number':
|
|
case 'integer': {
|
|
if (typeof value !== 'number') {
|
|
return [{ path, message: `Expected number at ${path}, got ${typeof value}` }];
|
|
}
|
|
if (scheme.type === 'integer' && !GlobalNumber.isInteger(value)) {
|
|
return [{ path, message: `Expected integer at ${path}, got ${value}` }];
|
|
}
|
|
const s = scheme as TNumber;
|
|
if (s.enum && !s.enum.includes(value)) {
|
|
return [{ path, message: `Expected one of [${s.enum.join(', ')}] at ${path}, got ${value}` }];
|
|
}
|
|
return [];
|
|
}
|
|
case 'boolean': {
|
|
if (typeof value !== 'boolean') {
|
|
return [{ path, message: `Expected boolean at ${path}, got ${typeof value}` }];
|
|
}
|
|
return [];
|
|
}
|
|
case 'array': {
|
|
if (!GlobalArray.isArray(value)) {
|
|
return [{ path, message: `Expected array at ${path}, got ${typeof value}` }];
|
|
}
|
|
const s = scheme as TArray;
|
|
return value.flatMap((item, i) => check(s.items, item, `${path}[${i}]`));
|
|
}
|
|
case 'object': {
|
|
if (typeof value !== 'object' || GlobalArray.isArray(value)) {
|
|
return [{ path, message: `Expected object at ${path}, got ${typeof value}` }];
|
|
}
|
|
const s = scheme as TObject;
|
|
const errors: CheckError[] = [];
|
|
for (const key of s.required || []) {
|
|
if (!(key in (value as Record<string, unknown>))) {
|
|
errors.push({ path: `${path}.${key}`, message: `Missing required property ${path}.${key}` });
|
|
}
|
|
}
|
|
for (const [key, propScheme] of GlobalObject.entries(s.properties)) {
|
|
const propValue = (value as Record<string, unknown>)[key];
|
|
if (propValue === undefined) continue;
|
|
errors.push(...check(propScheme, propValue, `${path}.${key}`));
|
|
}
|
|
return errors;
|
|
}
|
|
case 'union': {
|
|
const s = scheme as TUnion;
|
|
for (const branch of s.anyOf) {
|
|
if (check(branch, value, path).length === 0) return [];
|
|
}
|
|
return [{ path, message: `Expected union type at ${path}, got ${typeof value}` }];
|
|
}
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function Check(scheme: TScheme, value: unknown): CheckError[] {
|
|
return check(scheme, value, '$');
|
|
}
|
|
|
|
export function Is<T extends TScheme = TScheme>(scheme: T, value: unknown): value is Static<T> {
|
|
return Check(scheme, value).length === 0;
|
|
}
|
|
}
|
|
|
|
export interface TString<T extends string = string> {
|
|
type: 'string';
|
|
enum?: T[];
|
|
description?: string;
|
|
}
|
|
|
|
export interface TNumber<T extends number = number> {
|
|
type: 'number' | 'integer';
|
|
enum?: T[];
|
|
description?: string;
|
|
}
|
|
|
|
export interface TBoolean {
|
|
type: 'boolean';
|
|
description?: string;
|
|
}
|
|
|
|
export interface TArray<T extends TScheme = TScheme> {
|
|
type: 'array';
|
|
description?: string;
|
|
items: T;
|
|
}
|
|
|
|
export type TProperties = Record<string, TScheme>;
|
|
|
|
export interface TObject<T extends TProperties = TProperties> {
|
|
type: 'object';
|
|
description?: string;
|
|
properties: T;
|
|
required?: string[];
|
|
}
|
|
|
|
export interface TOptionalString<T extends string = string> extends TString<T> {
|
|
[optional]: true;
|
|
}
|
|
|
|
export interface TOptionalNumber<T extends number = number> extends TNumber<T> {
|
|
[optional]: true;
|
|
}
|
|
|
|
export interface TOptionalBoolean extends TBoolean {
|
|
[optional]: true;
|
|
}
|
|
|
|
export interface TOptionalArray<T extends TScheme = TScheme> extends TArray<T> {
|
|
[optional]: true;
|
|
}
|
|
|
|
export interface TOptionalObject<T extends TProperties = TProperties> extends TObject<T> {
|
|
[optional]: true;
|
|
}
|
|
|
|
export interface TUnion<T extends TScheme[] = TScheme[]> {
|
|
type: 'union';
|
|
anyOf: T;
|
|
description?: string;
|
|
}
|
|
|
|
export interface TOptionalUnion<T extends TScheme[] = TScheme[]> extends TUnion<T> {
|
|
[optional]: true;
|
|
}
|
|
|
|
export type TOptional<T extends TScheme = TScheme> =
|
|
T extends TString<infer S> ? TOptionalString<S> :
|
|
T extends TNumber<infer N> ? TOptionalNumber<N> :
|
|
T extends TBoolean ? TOptionalBoolean :
|
|
T extends TArray<infer I> ? TOptionalArray<I> :
|
|
T extends TObject<infer P> ? TOptionalObject<P> :
|
|
T extends TUnion<infer U> ? TOptionalUnion<U> :
|
|
never;
|
|
|
|
export type IsOptional<T> = T extends { [optional]: true } ? true : false;
|
|
|
|
export type TScheme = TString | TNumber | TBoolean | TArray | TObject | TUnion | TOptionalString | TOptionalNumber | TOptionalBoolean | TOptionalArray | TOptionalObject | TOptionalUnion;
|
|
|
|
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
|
|
type RequiredKeys<T extends TProperties> = {
|
|
[K in keyof T]: IsOptional<T[K]> extends true ? never : K
|
|
}[keyof T];
|
|
|
|
type OptionalKeys<T extends TProperties> = {
|
|
[K in keyof T]: IsOptional<T[K]> extends true ? K : never
|
|
}[keyof T];
|
|
|
|
type StaticObject<T extends TProperties> = Prettify<
|
|
{ [K in RequiredKeys<T>]: Static<T[K]> } &
|
|
{ [K in OptionalKeys<T>]?: Static<T[K]> }
|
|
>;
|
|
|
|
type StaticUnion<T extends TScheme[]> =
|
|
T extends [infer First extends TScheme, ...infer Rest extends TScheme[]]
|
|
? Static<First> | StaticUnion<Rest>
|
|
: never;
|
|
|
|
export type Static<T extends TScheme> =
|
|
T extends TString<infer S> ? S :
|
|
T extends TNumber<infer N> ? N :
|
|
T extends TBoolean ? boolean :
|
|
T extends TArray<infer I> ? Static<I>[] :
|
|
T extends TObject<infer P> ? StaticObject<P> :
|
|
T extends TUnion<infer U> ? StaticUnion<U> :
|
|
never;
|