diff --git a/package.json b/package.json index b53a810..ed76ab7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zeed", "type": "module", - "version": "0.24.9", + "version": "0.24.10", "description": "🌱 Simple foundation library", "author": { "name": "Dirk Holtwick", diff --git a/src/common/msg/emitter.spec.ts b/src/common/msg/emitter.spec.ts index f6cf7a4..a6246e1 100644 --- a/src/common/msg/emitter.spec.ts +++ b/src/common/msg/emitter.spec.ts @@ -169,6 +169,40 @@ describe('emitter', () => { expect(counter).toBe(100) }) + it('should work with addEventListener', async () => { + interface TestMessages { + a: () => void + b: () => void + } + + const e1 = new Emitter() + const e2 = new Emitter() + + let counter = 99 + + e1.addEventListener('a', () => counter++) + + e2.addEventListener('a', () => counter--) + e2.addEventListener('b', () => counter--) + + void e1.emit('a') + + expect(counter).toBe(100) + + void e2.emit('a') + void e2.emit('b') + + expect(counter).toBe(98) + + e1.addEventListener('a', () => counter++) + void e1.emit('a') // twice! + expect(counter).toBe(100) + + e1.removeAllListeners() + void e1.emit('a') // no listeners! + expect(counter).toBe(100) + }) + it('should wait on', async () => { expect.assertions(2) diff --git a/src/common/msg/emitter.ts b/src/common/msg/emitter.ts index cbe7c31..f3e8d6d 100644 --- a/src/common/msg/emitter.ts +++ b/src/common/msg/emitter.ts @@ -168,6 +168,10 @@ export class Emitter< }, timeoutMS) }) } + + // For compatibility reasons + addEventListener = this.on.bind(this) + removeEventListener = this.off.bind(this) } declare global { diff --git a/src/common/schema/env.ts b/src/common/schema/env.ts index 1d06203..1ef3585 100644 --- a/src/common/schema/env.ts +++ b/src/common/schema/env.ts @@ -38,12 +38,9 @@ export function parseSchemaEnv(schema: Type, env: any = process?.env ?? {} export function stringFromSchemaEnv(schema: Type, prefix = '', commentOut = false): string { assert(isSchemaObjectFlat(schema), 'schema should be a flat object') - const lines: string[] = [] - objectMap(schema._object!, (key, schema) => { lines.push(`${commentOut ? '# ' : ''}${prefix + fromCamelCase(key, '_').toUpperCase()}=${schema._default ?? ''}`) }) as T - return lines.join('\n') } diff --git a/src/common/schema/rpc.spec.ts b/src/common/schema/rpc.spec.ts index 414fa8b..5a7d136 100644 --- a/src/common/schema/rpc.spec.ts +++ b/src/common/schema/rpc.spec.ts @@ -1,41 +1,14 @@ -import { string } from './schema' +import { func, object, string } from './schema' import type { Infer, Type } from './schema' // see https://github.com/colinhacks/zod?tab=readme-ov-file#functions describe('rpc.spec', () => { it('should do something', async () => { - const rpcSchema = { - echo: string(), - } - - type RpcRaw = typeof rpcSchema - - type RpcFunc = { - [K in keyof T]: (arg: Infer) => void - } - - type Rpc = RpcFunc - }) - - it('should do something2', async () => { - function func(args: Type[], result?: Type) { - return { - args, - result, - } - } - - const rpcSchema = { + const rpcSchema = object({ echo: func([string()], string()), - } - - type RpcRaw = typeof rpcSchema - - type RpcFunc = { - [K in keyof T]: (arg: Infer) => void - } + }) - type Rpc = RpcFunc + type RpcRaw = Infer }) }) diff --git a/src/common/schema/sandbox-inherit.ts b/src/common/schema/sandbox-inherit.ts new file mode 100644 index 0000000..d880c3b --- /dev/null +++ b/src/common/schema/sandbox-inherit.ts @@ -0,0 +1,13 @@ +class TypeClass { + optional(): TypeClass { + return this + } +} + +class TypeStringClass extends TypeClass { + +} + +const o = new TypeStringClass() +const v = o.optional() +type t = typeof v // expect: TypeStringClass diff --git a/src/common/schema/sandbox.spec.ts b/src/common/schema/sandbox.spec.ts new file mode 100644 index 0000000..0602824 --- /dev/null +++ b/src/common/schema/sandbox.spec.ts @@ -0,0 +1,45 @@ +/* eslint-disable ts/no-unsafe-declaration-merging */ +// // Define an interface for the instance type +// interface MyTypeInstance { +// value: number +// test: () => number +// } + +// // Constructor function +// function MyType(this: MyTypeInstance, value: number) { +// this.value = value +// } + +// MyType.prototype.test = function () { +// return this.value +// } + +class MyType { + value: number + constructor(value: number) { + this.value = value + } +} + +interface MyType { + test: () => number +} + +describe('sandbox.spec', () => { + it('should do something', async () => { + // Create an instance of MyType using the 'new' keyword + const my = new MyType(123) + + MyType.prototype.test = function () { + return this.value + 1 + } + + // Example usage + expect(my.test()).toMatchInlineSnapshot(`124`) + expect(my).toMatchInlineSnapshot(` + MyType { + "value": 123, + } + `) + }) +}) diff --git a/src/common/schema/sandbox.ts b/src/common/schema/sandbox.ts index 052c524..5f8e76f 100644 --- a/src/common/schema/sandbox.ts +++ b/src/common/schema/sandbox.ts @@ -38,3 +38,5 @@ function number(): Type { const tt = tuple([number(), string(), boolean()]) type ttt = Infer // expected [number, string, boolean] + +// diff --git a/src/common/schema/schema.spec.ts b/src/common/schema/schema.spec.ts index 7ead4cc..4b3751c 100644 --- a/src/common/schema/schema.spec.ts +++ b/src/common/schema/schema.spec.ts @@ -10,14 +10,21 @@ describe('schema', () => { const lit = stringLiterals(['active', 'trialing', 'past_due', 'paused', 'deleted']) type ScheamLiterals = Infer type SchemaLiteralsTest = Expect> // Should pass + expectTypeOf().toMatchTypeOf() // Tuple const tup = tuple([number(), string(), boolean()]) type SchemaTuple = Infer // expected [number, string, boolean] type SchemaTupleTest = Expect> // Should pass + expectTypeOf().toMatchTypeOf<[number, string, boolean]>() + + const s1 = string().optional() // .pattern(/\d+/) + type t1a = typeof s1 + type t1 = Infer + expectTypeOf().toMatchTypeOf() const schema = object({ - id: string().default(() => '123'), + id: string().default('123'), // default(() => '123'), name: string(), age: int().optional(), active: boolean(), @@ -157,17 +164,6 @@ describe('schema', () => { "type": "string", }, "name": Object { - "_union": Array [ - Object { - "type": "string", - }, - Object { - "type": "string", - }, - Object { - "type": "string", - }, - ], "type": "string", }, }, diff --git a/src/common/schema/schema.ts b/src/common/schema/schema.ts index 74fde93..f94c62e 100644 --- a/src/common/schema/schema.ts +++ b/src/common/schema/schema.ts @@ -3,107 +3,101 @@ import { first, isBoolean, isFunction, isInteger, isNumber, isObject, isString } export interface TypeProps { } -export type TypeNames = string +// export interface TypeAssert { +// fn: (obj: any) => boolean | never +// message?: string +// } + +interface Type { + readonly type: string + readonly _check: (obj: any) => boolean + optional: () => Type + default: (value: any) => Type + parse: (obj: any) => T + map: (obj: any, fn: (this: Type, obj: any, schema: Type) => any) => any + props: (props: TypeProps) => Type +} -export interface Type { - type: TypeNames +class TypeClass implements Type { + readonly type + readonly _check + + constructor(name: string, check?: (obj: any) => boolean) { + this.type = name + this._check = check ?? (() => true) + } - _value?: T _optional?: boolean - _default?: T | (() => T) - _object?: Record - _union?: Type[] - _check: (obj: any) => boolean + optional(): TypeClass { // todo keep the inherited class type + this._optional = true + return this + } - optional: () => Type + _default?: T - // !!! this causes errors !!! No idea why yet! - // default: (value: T | (() => T)) => Type - default: (value: any) => Type + default(value: any): TypeClass { // todo keep the inherited class type + this._default = value + return this + } + + parse(obj: any): T { + if (obj == null) { + if (this._default != null) { + if (isFunction(this._default)) + obj = this._default() + else + obj = this._default + } + } + if (obj == null && this._optional === true) + return undefined as any + if (obj == null) + throw new Error('cannot be undefined') + if (!this._check || this._check(obj)) + return obj + throw new Error('wrong value') + } - parse: (obj: any, opt?: { // todo obj: T ? - transform?: boolean - strict?: boolean - }) => T + map(obj: any, fn: (this: Type, obj: any, schema: Type) => any): any { + return fn.call(this, obj, this) ?? obj + } _props?: TypeProps - props: (props: TypeProps) => Type - map: (obj: any, fn: (this: Type, obj: any, schema: Type) => any) => any + props(props: TypeProps) { + this._props = props + return this + } } export type Infer = T extends Type ? TT : never -type ObjectFixOptional = { - [K in keyof T as undefined extends T[K] ? K : never]?: T[K] & {} -} & { - [K in keyof T as undefined extends T[K] ? never : K]: T[K] & {} -} - -type ObjectPretty = Extract<{ [K in keyof V]: V[K] }, unknown> - -export type TypeObject = Type -}>>> - // Helper -function preParse(obj: T, info: Type): T { - if (obj == null) { - if (info._default != null) { - if (isFunction(info._default)) - obj = info._default() - else - obj = info._default - } - } - if (obj == null && info._optional === true) - return undefined as any - if (obj == null) - throw new Error('cannot be undefined') - if (!info._check || info._check(obj)) - return obj - throw new Error('wrong value') -} - -function generic(type: TypeNames, opt?: Partial>): Type { - const info: Type = { - parse(obj) { - return preParse(obj, this as any) - }, - map(obj, fn) { - return fn.call(this, obj, this) ?? obj - }, - _check() { - return true - }, - ...opt, - type, - optional() { - this._optional = true - return this as any - }, - default(value) { - this._default = value - return this as any - }, - props(props) { - this._props = props - return this as any - }, - } - return info +function generic(type: string, opt?: Partial>): Type { + return new TypeClass(type, opt?._check) } // Primitives +class TypeStringClass extends TypeClass { + pattern(rx: RegExp) { + // this.ass + return this + } +} + export function string() { - return generic('string', { - _check: isString, - }) + return new TypeStringClass('string', isString) } +// export function string() { +// return generic('string', { +// _check: isString, +// }) +// } + export function number() { return generic('number', { _check: isNumber, @@ -126,52 +120,78 @@ export function boolean() { // Object -export function object(tobj: T): TypeObject { - const info = generic('object', { - _object: tobj as any, - parse(obj) { - if (obj == null && this._optional === true) - return undefined - const newObj: any = {} - if (!isObject(obj)) - return new Error('expected object input') - if (!isObject(this._object)) - return new Error('expected object definition') - for (const [key, info] of Object.entries(this._object)) { - const value = info.parse((obj as any)[key]) +type ObjectFixOptional = { + [K in keyof T as undefined extends T[K] ? K : never]?: T[K] & {} +} & { + [K in keyof T as undefined extends T[K] ? never : K]: T[K] & {} +} + +type ObjectPretty = Extract<{ [K in keyof V]: V[K] }, unknown> + +export type TypeObject = Type +}>>> + +class TypeObjectClass> extends TypeClass { + _object: T + + constructor(obj: T) { + super('object', () => true) + this._object = obj + } + + parse(obj: any) { + if (obj == null && this._optional === true) + return undefined + const newObj: any = {} + if (!isObject(obj)) + return new Error('expected object input') + if (!isObject(this._object)) + return new Error('expected object definition') + for (const [key, info] of Object.entries(this._object)) { + const value = info.parse((obj as any)[key]) + if (value !== undefined) + newObj[key] = value + } + return newObj + } + + map(obj: any, fn: (this: Type, obj: any, schema: Type) => any): any { + const result = fn.call(this as any, obj, this as any) + if (result !== undefined) + return result + const newObj: any = {} + if (obj) { + for (const [key, info] of Object.entries(this._object ?? {})) { + const value = (info as Type).map((obj as any)[key], fn as any) if (value !== undefined) newObj[key] = value } - return newObj - }, - map(obj, fn) { - const result = fn.call(this as any, obj, this as any) - if (result !== undefined) - return result - const newObj: any = {} - if (obj) { - for (const [key, info] of Object.entries(this._object ?? {})) { - const value = info.map((obj as any)[key], fn) - if (value !== undefined) - newObj[key] = value - } - } - return newObj - }, - }) - return info + } + return newObj + } +} + +export function object(tobj: T): TypeObject { + return new TypeObjectClass(tobj) as any } +const x = object({ + name: string(), +}) + +type xt = Infer + // Union type TransformToUnion)[]> = T extends Array ? Infer : never export function union)[]>(options: T): Type> { return generic(first(options)?.type ?? 'any', { - _union: options, - _check(obj) { - return this._union?.some(t => t._check(obj)) ?? true - }, + // _union: options, + // _check(obj) { + // return this._union?.some(t => t._check(obj)) ?? true + // }, }) } @@ -194,7 +214,7 @@ export function stringLiterals // Function type TupleOutput = { - [K in keyof T]: T[K] extends Type ? U : never; + [K in keyof T]: T[K] extends Type ? U : never } type ArrayOutput = [