diff --git a/package.json b/package.json index 6d0c2cff..ae5190e8 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@jsonjoy.com/util": "^1.9.0", "sonic-forest": "^1.2.1", "thingies": "^2.5.0", - "tree-dump": "^1.0.3" + "tree-dump": "^1.1.0" }, "devDependencies": { "@biomejs/biome": "^1.9.3", diff --git a/src/type/classes/FnType.ts b/src/type/classes/FnType.ts index 35e314ea..a8811aaf 100644 --- a/src/type/classes/FnType.ts +++ b/src/type/classes/FnType.ts @@ -1,11 +1,7 @@ import {printTree} from 'tree-dump/lib/printTree'; import * as schema from '../../schema'; -import type {SchemaOf, Type} from '../types'; import {AbsType} from './AbsType'; - -const fnNotImplemented: schema.FunctionValue = async () => { - throw new Error('NOT_IMPLEMENTED'); -}; +import type {SchemaOf, Type} from '../types'; const toStringTree = (tab: string = '', type: FnType | FnRxType) => { return printTree(tab, [ @@ -71,6 +67,14 @@ export class FnType extends A return this; } + public exec(input: schema.TypeOf>) { + const func = this.schema.default as schema.FunctionValue< + schema.TypeOf>, + schema.TypeOf> + >; + return func(input); + } + public toString(tab: string = ''): string { return super.toString(tab) + toStringTree(tab, this); } diff --git a/src/value/FnValue.ts b/src/value/FnValue.ts new file mode 100644 index 00000000..75b7163a --- /dev/null +++ b/src/value/FnValue.ts @@ -0,0 +1,15 @@ +import {Value} from './Value'; +import type {Printable} from 'tree-dump/lib/types'; +import type * as classes from '../type'; + +export class FnValue> extends Value implements Printable { + public async exec(input: classes.ResolveType, ctx?: unknown): Promise> { + const fn = this.data as any; + const output = await fn(input, ctx); + return new Value(output, this.type!.res); + } + + public name(): string { + return 'FnValue'; + } +} diff --git a/src/value/ObjValue.ts b/src/value/ObjValue.ts index 47807733..5ad7245a 100644 --- a/src/value/ObjValue.ts +++ b/src/value/ObjValue.ts @@ -1,9 +1,9 @@ -import {printTree} from 'tree-dump/lib/printTree'; +import {ModuleType} from '../type/classes/ModuleType'; +import {Value} from './Value'; +import {FnValue} from './FnValue'; import type {Printable} from 'tree-dump/lib/types'; import type * as classes from '../type'; import type {TypeBuilder} from '../type/TypeBuilder'; -import {ModuleType} from '../type/classes/ModuleType'; -import {Value} from './Value'; export type UnObjType = T extends classes.ObjType ? U : never; export type UnObjValue = T extends ObjValue ? U : never; @@ -14,6 +14,8 @@ export type ObjValueToTypeMap = ToObject<{ [K in keyof F]: ObjFieldToTuple; }>; +export type Ensure = T extends X ? T : X; + export class ObjValue> extends Value implements Printable { public static new = (system: ModuleType = new ModuleType()) => new ObjValue({}, system.t.obj); @@ -41,6 +43,18 @@ export class ObjValue> extends Value implement return new Value(data, field.val) as any; } + public fn>>( + key: K, + ): FnValue< + Ensure< + ObjValueToTypeMap>[K] extends classes.Type ? ObjValueToTypeMap>[K] : classes.Type, + classes.FnType + > + > { + const val = this.get(key); + return new FnValue(val.data, val.type as any); + } + public field>( field: F | ((t: TypeBuilder) => F), data: classes.ResolveType>, @@ -78,7 +92,7 @@ export class ObjValue> extends Value implement return this as any; } - public toString(tab: string = ''): string { - return 'ObjValue' + printTree(tab, [(tab) => this.type!.toString(tab)]); + public name(): string { + return 'ObjValue'; } } diff --git a/src/value/Value.ts b/src/value/Value.ts index 378740cc..1cbc2e30 100644 --- a/src/value/Value.ts +++ b/src/value/Value.ts @@ -1,16 +1,40 @@ import {printTree} from 'tree-dump/lib/printTree'; +import {printJson} from 'tree-dump/lib/printJson'; import type {Printable} from 'tree-dump'; import type {ResolveType, Type} from '../type/types'; +const copyForPrint = (data: unknown): unknown => { + if (typeof data === 'function') return '__fN---'; + if (Array.isArray(data)) return data.map(copyForPrint); + if (data && typeof data === 'object') { + const res: Record = {}; + for (const k in data) res[k] = copyForPrint((data as any)[k]); + return res; + } + return data; +}; + export class Value implements Printable { constructor( public data: ResolveType, public type?: T, ) {} + public name(): string { + return 'Value'; + } + public toString(tab: string = ''): string { const type = this.type; - return 'Value' + (type ? printTree(tab, [(tab) => type.toString(tab)]) : ''); + return ( + this.name() + + (type + ? printTree(tab, [ + (tab) => type.toString(tab), + (tab) => printJson(tab, copyForPrint(this.data)).replace(/"__fN---"/g, 'fn()'), + ]) + : '') + ); } } diff --git a/src/value/__tests__/ObjValue-router.spec.ts b/src/value/__tests__/ObjValue-router.spec.ts new file mode 100644 index 00000000..180d87dd --- /dev/null +++ b/src/value/__tests__/ObjValue-router.spec.ts @@ -0,0 +1,8 @@ +import {createRouter} from './ObjValue.fixtures'; + +test('can retrieve field as Value', async () => { + const log = jest.fn(); + const router = createRouter({log}); + const result = await router.fn('log.message').exec({message: 'asdf'}); + expect(result.data).toEqual({time: expect.any(Number)}); +}); diff --git a/src/value/__tests__/ObjValue.fixtures.ts b/src/value/__tests__/ObjValue.fixtures.ts new file mode 100644 index 00000000..970ba90e --- /dev/null +++ b/src/value/__tests__/ObjValue.fixtures.ts @@ -0,0 +1,41 @@ +import type {ObjType} from '../../type'; +import type {TypeBuilder} from '../../type/TypeBuilder'; +import {ObjValue} from '../ObjValue'; + +interface Services { + log: (msg: string) => void; +} + +export interface RouteDeps { + svc: Services; + t: TypeBuilder; +} +export type RouterBase = ObjType; +export type Router = ObjValue; + +const addLogMessageRoute = + ({t, svc}: RouteDeps) => + (r: Router) => { + return r.set( + 'log.message', + t.fn + .inp(t.Object(t.Key('message', t.str))) + .out( + t.object({ + time: t.num, + }), + ) + .value(({message}) => { + svc.log(message); + return {time: Date.now()}; + }), + ); + }; + +export const createRouter = (svc: Services) => { + const base = ObjValue.new(); + const t = base.system.t; + const deps: RouteDeps = {svc, t}; + const router = addLogMessageRoute(deps)(base); + return router; +}; diff --git a/src/value/__tests__/__snapshots__/ObjValue.spec.ts.snap b/src/value/__tests__/__snapshots__/ObjValue.spec.ts.snap index 447b4e24..f59eb69d 100644 --- a/src/value/__tests__/__snapshots__/ObjValue.spec.ts.snap +++ b/src/value/__tests__/__snapshots__/ObjValue.spec.ts.snap @@ -2,7 +2,10 @@ exports[`can print to string 1`] = ` "ObjValue -└─ obj - └─ "foo" - └─ str" +├─ obj +│ └─ "foo" +│ └─ str +└─ { + "foo": "bar" + }" `; diff --git a/yarn.lock b/yarn.lock index 4c64669f..5327e8e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -889,7 +889,7 @@ __metadata: rxjs: "npm:^7.8.2" sonic-forest: "npm:^1.2.1" thingies: "npm:^2.5.0" - tree-dump: "npm:^1.0.3" + tree-dump: "npm:^1.1.0" ts-jest: "npm:^29.1.2" tslib: "npm:^2.7.0" typescript: "npm:^5.6.2" @@ -3530,7 +3530,7 @@ __metadata: languageName: node linkType: hard -"tree-dump@npm:^1.0.0, tree-dump@npm:^1.0.3": +"tree-dump@npm:^1.0.0": version: 1.0.3 resolution: "tree-dump@npm:1.0.3" peerDependencies: @@ -3539,6 +3539,15 @@ __metadata: languageName: node linkType: hard +"tree-dump@npm:^1.1.0": + version: 1.1.0 + resolution: "tree-dump@npm:1.1.0" + peerDependencies: + tslib: 2 + checksum: 10c0/079f0f0163b68ee2eedc65cab1de6fb121487eba9ae135c106a8bc5e4ab7906ae0b57d86016e4a7da8c0ee906da1eae8c6a1490cd6e2a5e5ccbca321e1f959ca + languageName: node + linkType: hard + "ts-jest@npm:^29.1.2": version: 29.4.1 resolution: "ts-jest@npm:29.4.1"