diff --git a/package.json b/package.json index fd23e245..07345a63 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,26 @@ "type": "github", "url": "https://github.com/sponsors/streamich" }, - "keywords": ["json-type", "type", "schema", "json-schema", "jtd", "json", "pointer", "jit"], + "keywords": [ + "json-type", + "type", + "schema", + "json-schema", + "jtd", + "json", + "pointer", + "jit" + ], "engines": { "node": ">=10.0" }, "main": "lib/index.js", "types": "lib/index.d.ts", "typings": "lib/index.d.ts", - "files": ["LICENSE", "lib/"], + "files": [ + "LICENSE", + "lib/" + ], "license": "Apache-2.0", "scripts": { "format": "biome format ./src", @@ -51,6 +63,7 @@ "@jsonjoy.com/json-random": "^1.2.0", "@jsonjoy.com/util": "^1.9.0", "sonic-forest": "^1.2.1", + "thingies": "^2.5.0", "tree-dump": "^1.0.3" }, "devDependencies": { @@ -67,11 +80,16 @@ "typescript": "^5.6.2" }, "jest": { - "moduleFileExtensions": ["ts", "js"], + "moduleFileExtensions": [ + "ts", + "js" + ], "transform": { "^.+\\.ts$": "ts-jest" }, - "transformIgnorePatterns": [".*/node_modules/.*"], + "transformIgnorePatterns": [ + ".*/node_modules/.*" + ], "testRegex": ".*/(__tests__|__jest__|demo)/.*\\.(test|spec)\\.ts$" } } diff --git a/src/codegen/binary/AbstractBinaryCodegen.ts b/src/codegen/binary/AbstractBinaryCodegen.ts index 80d0777a..43e6ef86 100644 --- a/src/codegen/binary/AbstractBinaryCodegen.ts +++ b/src/codegen/binary/AbstractBinaryCodegen.ts @@ -133,8 +133,32 @@ var uint8 = writer.uint8, view = writer.view;`, return this.codegen.compile(); } + protected abstract linkGet(): void; + protected onAny(path: SchemaPath, r: JsExpression, type: AnyType): void { - this.codegen.js(`encoder.writeAny(${r.use()});`); + const codegen = this.codegen; + const rv = codegen.var(r.use()); + codegen.link('Value'); + this.linkGet(); + codegen.if( + /* js */ `${rv} instanceof Value`, + () => { + const rType = codegen.var(/* js */ `${rv}.type`); + const rData = codegen.var(/* js */ `${rv}.data`); + codegen.if( + /* js */ `${rType}`, + () => { + codegen.js(/* js */ `get(${rType})(${rData},encoder);`); + }, + () => { + this.codegen.js(`encoder.writeAny(${rData});`); + }, + ); + }, + () => { + this.codegen.js(`encoder.writeAny(${rv});`); + }, + ); } protected onCon(path: SchemaPath, r: JsExpression, type: ConType): void { diff --git a/src/codegen/binary/cbor/CborCodegen.ts b/src/codegen/binary/cbor/CborCodegen.ts index 6a7a10d7..a9ed69e8 100644 --- a/src/codegen/binary/cbor/CborCodegen.ts +++ b/src/codegen/binary/cbor/CborCodegen.ts @@ -2,10 +2,11 @@ import {JsExpression} from '@jsonjoy.com/codegen/lib/util/JsExpression'; import {normalizeAccessor} from '@jsonjoy.com/codegen/lib/util/normalizeAccessor'; import {CborEncoder} from '@jsonjoy.com/json-pack/lib/cbor/CborEncoder'; import {KeyOptType, type KeyType, type ObjType, type Type} from '../../../type'; -import type {CompiledBinaryEncoder, SchemaPath} from '../../types'; import {lazyKeyedFactory} from '../../util'; import {AbstractBinaryCodegen} from '../AbstractBinaryCodegen'; import {writer} from '../writer'; +import {once} from 'thingies/lib/once'; +import type {CompiledBinaryEncoder, SchemaPath} from '../../types'; export class CborCodegen extends AbstractBinaryCodegen { public static readonly get = lazyKeyedFactory((type: Type, name?: string) => { @@ -18,6 +19,11 @@ export class CborCodegen extends AbstractBinaryCodegen { protected encoder = new CborEncoder(writer); + @once + protected linkGet(): void { + this.codegen.linkDependency(CborCodegen.get, 'get'); + } + protected onObj(path: SchemaPath, value: JsExpression, type: ObjType): void { const codegen = this.codegen; const r = codegen.r(); diff --git a/src/codegen/binary/cbor/__tests__/CborCodegen.spec.ts b/src/codegen/binary/cbor/__tests__/CborCodegen.spec.ts index baff32ce..1083259f 100644 --- a/src/codegen/binary/cbor/__tests__/CborCodegen.spec.ts +++ b/src/codegen/binary/cbor/__tests__/CborCodegen.spec.ts @@ -1,14 +1,52 @@ import {Writer} from '@jsonjoy.com/buffers/lib/Writer'; import {CborDecoder} from '@jsonjoy.com/json-pack/lib/cbor/CborDecoder'; import {CborEncoder} from '@jsonjoy.com/json-pack/lib/cbor/CborEncoder'; -import type {Type} from '../../../../type'; -import type {ModuleType} from '../../../../type/classes/ModuleType'; +import {ModuleType} from '../../../../type/classes/ModuleType'; import {testBinaryCodegen} from '../../__tests__/testBinaryCodegen'; import {CborCodegen} from '../CborCodegen'; +import {unknown, Value} from '../../../../value'; +import type {Type} from '../../../../type'; const encoder = new CborEncoder(new Writer(16)); const decoder = new CborDecoder(); +describe('inline Value', () => { + test('can encode "any" field', () => { + const {t} = new ModuleType(); + const type = t.object({foo: t.any}); + const fn = CborCodegen.get(type); + encoder.writer.reset(); + fn({foo: true}, encoder); + const encoded = encoder.writer.flush(); + const decoded = decoder.decode(encoded); + expect(decoded).toEqual({foo: true}); + }); + + test('can encode anon Value', () => { + const {t} = new ModuleType(); + const type = t.object({foo: t.any}); + const fn = CborCodegen.get(type); + encoder.writer.reset(); + const value = unknown('test'); + fn({foo: value}, encoder); + const encoded = encoder.writer.flush(); + const decoded = decoder.decode(encoded); + expect(decoded).toEqual({foo: 'test'}); + }); + + test('can encode typed Value', () => { + const {t} = new ModuleType(); + const type = t.object({foo: t.any}); + const fn = CborCodegen.get(type); + encoder.writer.reset(); + const value = new Value(123, t.con(123)); + fn({foo: value}, encoder); + const encoded = encoder.writer.flush(); + const decoded = decoder.decode(encoded); + expect(decoded).toEqual({foo: 123}); + }); +}); + const transcode = (system: ModuleType, type: Type, value: unknown) => { const fn = CborCodegen.get(type); encoder.writer.reset(); diff --git a/src/codegen/binary/json/JsonCodegen.ts b/src/codegen/binary/json/JsonCodegen.ts index 5cbc782c..bd26b6e0 100644 --- a/src/codegen/binary/json/JsonCodegen.ts +++ b/src/codegen/binary/json/JsonCodegen.ts @@ -2,10 +2,11 @@ import {JsExpression} from '@jsonjoy.com/codegen/lib/util/JsExpression'; import {normalizeAccessor} from '@jsonjoy.com/codegen/lib/util/normalizeAccessor'; import {JsonEncoder} from '@jsonjoy.com/json-pack/lib/json/JsonEncoder'; import {type ArrType, type MapType, KeyOptType, type KeyType, type ObjType, type Type} from '../../../type'; -import type {CompiledBinaryEncoder, SchemaPath} from '../../types'; import {lazyKeyedFactory} from '../../util'; import {AbstractBinaryCodegen} from '../AbstractBinaryCodegen'; import {writer} from '../writer'; +import {once} from 'thingies/lib/once'; +import type {CompiledBinaryEncoder, SchemaPath} from '../../types'; export class JsonCodegen extends AbstractBinaryCodegen { public static readonly get = lazyKeyedFactory((type: Type, name?: string) => { @@ -20,6 +21,11 @@ export class JsonCodegen extends AbstractBinaryCodegen { protected encoder = new JsonEncoder(writer); + @once + protected linkGet(): void { + this.codegen.linkDependency(JsonCodegen.get, 'get'); + } + protected onArr(path: SchemaPath, r: JsExpression, type: ArrType): void { const codegen = this.codegen; const rLen = codegen.var(/* js */ `${r.use()}.length`); diff --git a/src/codegen/binary/json/__tests__/JsonCodegen.spec.ts b/src/codegen/binary/json/__tests__/JsonCodegen.spec.ts index b6d5878e..d81ae7aa 100644 --- a/src/codegen/binary/json/__tests__/JsonCodegen.spec.ts +++ b/src/codegen/binary/json/__tests__/JsonCodegen.spec.ts @@ -1,12 +1,52 @@ import {Writer} from '@jsonjoy.com/buffers/lib/Writer'; import {parse} from '@jsonjoy.com/json-pack/lib/json-binary'; import {JsonEncoder} from '@jsonjoy.com/json-pack/lib/json/JsonEncoder'; +import {JsonDecoder} from '@jsonjoy.com/json-pack/lib/json/JsonDecoder'; import {ModuleType} from '../../../../type/classes/ModuleType'; import {testBinaryCodegen} from '../../__tests__/testBinaryCodegen'; import {JsonCodegen} from '../JsonCodegen'; import type {Type} from '../../../../type'; +import {unknown, Value} from '../../../../value'; const encoder = new JsonEncoder(new Writer(16)); +const decoder = new JsonDecoder(); + +describe('inline Value', () => { + test('can encode "any" field', () => { + const {t} = new ModuleType(); + const type = t.object({foo: t.any}); + const fn = JsonCodegen.get(type); + encoder.writer.reset(); + fn({foo: true}, encoder); + const encoded = encoder.writer.flush(); + const decoded = decoder.decode(encoded); + expect(decoded).toEqual({foo: true}); + }); + + test('can encode anon Value', () => { + const {t} = new ModuleType(); + const type = t.object({foo: t.any}); + const fn = JsonCodegen.get(type); + encoder.writer.reset(); + const value = unknown('test'); + fn({foo: value}, encoder); + const encoded = encoder.writer.flush(); + const decoded = decoder.decode(encoded); + expect(decoded).toEqual({foo: 'test'}); + }); + + test('can encode typed Value', () => { + const {t} = new ModuleType(); + const type = t.object({foo: t.any}); + const fn = JsonCodegen.get(type); + encoder.writer.reset(); + const value = new Value(123, t.con(123)); + fn({foo: value}, encoder); + const encoded = encoder.writer.flush(); + const decoded = decoder.decode(encoded); + expect(decoded).toEqual({foo: 123}); + }); +}); const transcode = (system: ModuleType, type: Type, value: unknown) => { const fn = JsonCodegen.get(type); diff --git a/src/codegen/binary/msgpack/MsgPackCodegen.ts b/src/codegen/binary/msgpack/MsgPackCodegen.ts index ffd30a73..48b3958f 100644 --- a/src/codegen/binary/msgpack/MsgPackCodegen.ts +++ b/src/codegen/binary/msgpack/MsgPackCodegen.ts @@ -2,10 +2,11 @@ import {JsExpression} from '@jsonjoy.com/codegen/lib/util/JsExpression'; import {normalizeAccessor} from '@jsonjoy.com/codegen/lib/util/normalizeAccessor'; import {MsgPackEncoder} from '@jsonjoy.com/json-pack/lib/msgpack/MsgPackEncoder'; import {KeyOptType, type KeyType, type ObjType, type Type} from '../../../type'; -import type {CompiledBinaryEncoder, SchemaPath} from '../../types'; import {lazyKeyedFactory} from '../../util'; import {AbstractBinaryCodegen} from '../AbstractBinaryCodegen'; import {writer} from '../writer'; +import {once} from 'thingies/lib/once'; +import type {CompiledBinaryEncoder, SchemaPath} from '../../types'; export class MsgPackCodegen extends AbstractBinaryCodegen { public static readonly get = lazyKeyedFactory((type: Type, name?: string) => { @@ -18,6 +19,11 @@ export class MsgPackCodegen extends AbstractBinaryCodegen { protected encoder = new MsgPackEncoder(writer); + @once + protected linkGet(): void { + this.codegen.linkDependency(MsgPackCodegen.get, 'get'); + } + protected onObj(path: SchemaPath, value: JsExpression, type: ObjType): void { const codegen = this.codegen; const r = codegen.r(); diff --git a/src/codegen/binary/msgpack/__tests__/MsgPackCodegen.spec.ts b/src/codegen/binary/msgpack/__tests__/MsgPackCodegen.spec.ts index 3675bbc7..80ec7f37 100644 --- a/src/codegen/binary/msgpack/__tests__/MsgPackCodegen.spec.ts +++ b/src/codegen/binary/msgpack/__tests__/MsgPackCodegen.spec.ts @@ -1,13 +1,51 @@ import {Writer} from '@jsonjoy.com/buffers/lib/Writer'; import {MsgPackDecoder} from '@jsonjoy.com/json-pack/lib/msgpack/MsgPackDecoder'; import {MsgPackEncoder} from '@jsonjoy.com/json-pack/lib/msgpack/MsgPackEncoder'; -import type {ModuleType, Type} from '../../../../type'; +import {ModuleType, type Type} from '../../../../type'; import {testBinaryCodegen} from '../../__tests__/testBinaryCodegen'; import {MsgPackCodegen} from '../MsgPackCodegen'; +import {unknown, Value} from '../../../../value'; const encoder = new MsgPackEncoder(new Writer(16)); const decoder = new MsgPackDecoder(); +describe('inline Value', () => { + test('can encode "any" field', () => { + const {t} = new ModuleType(); + const type = t.object({foo: t.any}); + const fn = MsgPackCodegen.get(type); + encoder.writer.reset(); + fn({foo: true}, encoder); + const encoded = encoder.writer.flush(); + const decoded = decoder.decode(encoded); + expect(decoded).toEqual({foo: true}); + }); + + test('can encode anon Value', () => { + const {t} = new ModuleType(); + const type = t.object({foo: t.any}); + const fn = MsgPackCodegen.get(type); + encoder.writer.reset(); + const value = unknown('test'); + fn({foo: value}, encoder); + const encoded = encoder.writer.flush(); + const decoded = decoder.decode(encoded); + expect(decoded).toEqual({foo: 'test'}); + }); + + test('can encode typed Value', () => { + const {t} = new ModuleType(); + const type = t.object({foo: t.any}); + const fn = MsgPackCodegen.get(type); + encoder.writer.reset(); + const value = new Value(123, t.con(123)); + fn({foo: value}, encoder); + const encoded = encoder.writer.flush(); + const decoded = decoder.decode(encoded); + expect(decoded).toEqual({foo: 123}); + }); +}); + const transcode = (system: ModuleType, type: Type, value: unknown) => { const fn = MsgPackCodegen.get(type); encoder.writer.reset(); diff --git a/src/codegen/capacity/CapacityEstimatorCodegen.ts b/src/codegen/capacity/CapacityEstimatorCodegen.ts index 7ba653be..30c92fb0 100644 --- a/src/codegen/capacity/CapacityEstimatorCodegen.ts +++ b/src/codegen/capacity/CapacityEstimatorCodegen.ts @@ -8,6 +8,7 @@ import {DiscriminatorCodegen} from '../discriminator'; import {lazyKeyedFactory} from '../util'; import {AbstractCodegen} from '../AbstractCodege'; import type {SchemaPath} from '../types'; +import {Value} from '../../value'; export type CompiledCapacityEstimator = (value: unknown) => number; @@ -36,6 +37,10 @@ export class CapacityEstimatorCodegen extends AbstractCodegen { const stepsJoined: CodegenStepExecJs[] = []; for (let i = 0; i < steps.length; i++) { @@ -58,7 +63,27 @@ export class CapacityEstimatorCodegen extends AbstractCodegen { + const rType = codegen.var(/* js */ `${rv}.type`); + const rData = codegen.var(/* js */ `${rv}.data`); + codegen.if( + /* js */ `${rType}`, + () => { + codegen.js(/* js */ `size += get(${rType})(${rData});`); + }, + () => { + codegen.js(/* js */ `size += maxEncodingCapacity(${rData});`); + }, + ); + }, + () => { + codegen.js(/* js */ `size += maxEncodingCapacity(${rv});`); + }, + ); } protected onCon(path: SchemaPath, r: JsExpression, type: ConType): void { diff --git a/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts b/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts index 6c623376..55970187 100644 --- a/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts +++ b/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts @@ -3,6 +3,7 @@ import {t} from '../../../type'; import {ModuleType} from '../../../type/classes/ModuleType'; import {CapacityEstimatorCodegen} from '../CapacityEstimatorCodegen'; import {Random} from '../../../random'; +import {unknown, Value} from '../../../value'; describe('"any" type', () => { test('returns the same result as maxEncodingCapacity()', () => { @@ -11,6 +12,26 @@ describe('"any" type', () => { const values = [null, true, false, 1, 123.123, '', 'adsf', [], {}, {foo: 'bar'}, [{a: [{b: null}]}]]; for (const value of values) expect(estimator(value)).toBe(maxEncodingCapacity(value)); }); + + test('can encode "any" field', () => { + const type = t.object({foo: t.any}); + const estimator = CapacityEstimatorCodegen.get(type); + expect(estimator({foo: true})).toBe(maxEncodingCapacity({foo: true})); + }); + + test('can encode anon Value', () => { + const type = t.object({foo: t.any}); + const value = unknown('test'); + const estimator = CapacityEstimatorCodegen.get(type); + expect(estimator({foo: value})).toBe(maxEncodingCapacity({foo: value.data})); + }); + + test('can encode typed Value', () => { + const type = t.object({foo: t.any}); + const value = new Value(123, t.con(123)); + const estimator = CapacityEstimatorCodegen.get(type); + expect(estimator({foo: value})).toBe(maxEncodingCapacity({foo: value.data})); + }); }); describe('"con" type', () => { diff --git a/src/codegen/json/JsonTextCodegen.ts b/src/codegen/json/JsonTextCodegen.ts index 0dbf8c2e..8abe158b 100644 --- a/src/codegen/json/JsonTextCodegen.ts +++ b/src/codegen/json/JsonTextCodegen.ts @@ -3,12 +3,13 @@ import {Codegen, CodegenStepExecJs} from '@jsonjoy.com/codegen'; import {JsExpression} from '@jsonjoy.com/codegen/lib/util/JsExpression'; import {normalizeAccessor} from '@jsonjoy.com/codegen/lib/util/normalizeAccessor'; import {stringify} from '@jsonjoy.com/json-pack/lib/json-binary/codec'; -import type {json_string} from '@jsonjoy.com/util/lib/json-brand'; import {asString} from '@jsonjoy.com/util/lib/strings/asString'; import {KeyOptType} from '../../type'; -import type {ArrType, ConType, MapType, ObjType, OrType, RefType, StrType, Type} from '../../type'; import {DiscriminatorCodegen} from '../discriminator'; import {lazyKeyedFactory} from '../util'; +import {Value} from '../../value'; +import type {json_string} from '@jsonjoy.com/util/lib/json-brand'; +import type {ArrType, ConType, MapType, ObjType, OrType, RefType, StrType, Type} from '../../type'; export type JsonEncoderFn = (value: T) => json_string; @@ -39,6 +40,8 @@ export class JsonTextCodegen { epilogue: `return s;`, linkable: { toBase64, + Value, + getEncoder: JsonTextCodegen.get, }, processSteps: (steps) => { const stepsJoined: Step[] = []; @@ -205,7 +208,27 @@ if (${rLength}) { switch (kind) { case 'any': { const r = codegen.var(value.use()); - codegen.js(`s += stringify(${r});`); + codegen.link('Value'); + codegen.link('getEncoder'); + codegen.if( + /* js */ `${r} instanceof Value`, + () => { + const rType = codegen.var(/* js */ `${r}.type`); + const rData = codegen.var(/* js */ `${r}.data`); + codegen.if( + /* js */ `${rType}`, + () => { + codegen.js(/* js */ `s += getEncoder(${rType})(${rData});`); + }, + () => { + codegen.js(`s += stringify(${rData});`); + }, + ); + }, + () => { + codegen.js(`s += stringify(${r});`); + }, + ); break; } case 'bool': { diff --git a/src/codegen/json/__tests__/JsonTextCodegen.spec.ts b/src/codegen/json/__tests__/JsonTextCodegen.spec.ts index c9cde7bc..9decd6ce 100644 --- a/src/codegen/json/__tests__/JsonTextCodegen.spec.ts +++ b/src/codegen/json/__tests__/JsonTextCodegen.spec.ts @@ -2,6 +2,7 @@ import {parse} from '@jsonjoy.com/json-pack/lib/json-binary/codec'; import {t} from '../../../type'; import {ModuleType} from '../../../type/classes/ModuleType'; import {JsonTextCodegen} from '../JsonTextCodegen'; +import {unknown, Value} from '../../../value'; describe('"any" type', () => { test('stringify simple JSON', () => { @@ -20,6 +21,26 @@ describe('"any" type', () => { const encoder = JsonTextCodegen.get(t.any); expect(encoder(-1)).toBe('-1'); }); + + test('can encode "any" field', () => { + const type = t.object({foo: t.any}); + const encoder = JsonTextCodegen.get(type); + expect(encoder({foo: true})).toBe('{"foo":true}'); + }); + + test('can encode anon Value', () => { + const type = t.object({foo: t.any}); + const value = unknown('test'); + const encoder = JsonTextCodegen.get(type); + expect(encoder({foo: value})).toBe('{"foo":"test"}'); + }); + + test('can encode typed Value', () => { + const type = t.object({foo: t.any}); + const value = new Value(123, t.con(123)); + const encoder = JsonTextCodegen.get(type); + expect(encoder({foo: value})).toBe('{"foo":123}'); + }); }); describe('"bool" type', () => { diff --git a/src/type/classes/AbsType.ts b/src/type/classes/AbsType.ts index 5b258b19..a2410d7a 100644 --- a/src/type/classes/AbsType.ts +++ b/src/type/classes/AbsType.ts @@ -28,7 +28,7 @@ export abstract class AbsType implements BaseType, P } public value(data: schema.TypeOf) { - return new Value(this, data as any); + return new Value(data as any, this); } /** diff --git a/src/value/ObjValue.ts b/src/value/ObjValue.ts index 215ebd63..47807733 100644 --- a/src/value/ObjValue.ts +++ b/src/value/ObjValue.ts @@ -15,7 +15,7 @@ export type ObjValueToTypeMap = ToObject<{ }>; export class ObjValue> extends Value implements Printable { - public static new = (system: ModuleType = new ModuleType()) => new ObjValue(system.t.obj, {}); + public static new = (system: ModuleType = new ModuleType()) => new ObjValue({}, system.t.obj); public get system(): classes.ModuleType { return (this.type as T).getSystem(); @@ -35,10 +35,10 @@ export class ObjValue> extends Value implement ): Value< ObjValueToTypeMap>[K] extends classes.Type ? ObjValueToTypeMap>[K] : classes.Type > { - const field = this.type.getField(key); + const field = this.type!.getField(key); if (!field) throw new Error('NO_FIELD'); const data = (this.data as Record)[key]; - return new Value(field.val, data) as any; + return new Value(data, field.val) as any; } public field>( @@ -47,7 +47,7 @@ export class ObjValue> extends Value implement ): ObjValue, F]>> { field = typeof field === 'function' ? field((this.type as classes.ObjType).getSystem().t) : field; (this.data as any)[field.key] = data; - const type = this.type; + const type = this.type!; const system = type.system; if (!system) throw new Error('NO_SYSTEM'); type.keys.push(field); @@ -66,12 +66,12 @@ export class ObjValue> extends Value implement } public set(key: K, value: Value) { - return this.add(key, value.type, value.data); + return this.add(key, value.type!, value.data); } public merge>(obj: O): ObjValue, ...UnObjType]>> { Object.assign(this.data as object, obj.data); - const type = this.type; + const type = this.type!; const system = type.system; if (!system) throw new Error('NO_SYSTEM'); type.keys.push(...type.keys); @@ -79,6 +79,6 @@ export class ObjValue> extends Value implement } public toString(tab: string = ''): string { - return 'ObjValue' + printTree(tab, [(tab) => this.type.toString(tab)]); + return 'ObjValue' + printTree(tab, [(tab) => this.type!.toString(tab)]); } } diff --git a/src/value/Value.ts b/src/value/Value.ts index 0d60adee..378740cc 100644 --- a/src/value/Value.ts +++ b/src/value/Value.ts @@ -1,14 +1,17 @@ -import type {Printable} from 'tree-dump'; import {printTree} from 'tree-dump/lib/printTree'; +import type {Printable} from 'tree-dump'; import type {ResolveType, Type} from '../type/types'; export class Value implements Printable { constructor( - public type: T, public data: ResolveType, + public type?: T, ) {} public toString(tab: string = ''): string { - return 'Value' + printTree(tab, [(tab) => this.type.toString(tab)]); + const type = this.type; + return 'Value' + (type ? printTree(tab, [(tab) => type.toString(tab)]) : ''); } } + +export const unknown = (data: unknown): Value => new (Value as any)(data); diff --git a/src/value/__tests__/ObjValue.spec.ts b/src/value/__tests__/ObjValue.spec.ts index 4d0e7618..a8f5bb4c 100644 --- a/src/value/__tests__/ObjValue.spec.ts +++ b/src/value/__tests__/ObjValue.spec.ts @@ -4,16 +4,16 @@ import {ObjValue} from '../ObjValue'; test('can retrieve field as Value', () => { const system = new ModuleType(); const {t} = system; - const obj = new ObjValue(t.Object(t.Key('foo', t.str)), {foo: 'bar'}); + const obj = new ObjValue({foo: 'bar'}, t.Object(t.Key('foo', t.str))); const foo = obj.get('foo'); - expect(foo.type.kind()).toBe('str'); + expect(foo.type!.kind()).toBe('str'); expect(foo.data).toBe('bar'); }); test('can print to string', () => { const system = new ModuleType(); const {t} = system; - const obj = new ObjValue(t.Object(t.Key('foo', t.str)), {foo: 'bar'}); + const obj = new ObjValue({foo: 'bar'}, t.Object(t.Key('foo', t.str))); expect(obj + '').toMatchSnapshot(); }); @@ -33,8 +33,8 @@ describe('.set()', () => { ); const value = router.get('ping'); expect(value.data).toBe(procedure); - expect(value.type.req.getSchema()).toEqual(t.undef.getSchema()); - expect(value.type.res.getSchema()).toEqual(t.str.getSchema()); + expect(value.type!.req.getSchema()).toEqual(t.undef.getSchema()); + expect(value.type!.res.getSchema()).toEqual(t.str.getSchema()); }); test('can set multiple fields', () => { @@ -77,10 +77,10 @@ describe('.set()', () => { .value((id) => ({id, name: 'User ' + id, friends: async (friendId) => 'Friend ' + friendId})), ); expect(router.get('ping').data).toBe(procedure); - expect(router.get('getUser').type.req.getSchema()).toEqual( + expect(router.get('getUser').type!.req.getSchema()).toEqual( t.str.title('User ID').description('ID of the user to retrieve').getSchema(), ); - expect(router.get('getUser').type.res.getSchema()).toEqual( + expect(router.get('getUser').type!.res.getSchema()).toEqual( t .object({ id: t.str, @@ -89,6 +89,6 @@ describe('.set()', () => { }) .getSchema(), ); - expect(router.get('echo').type.req.getSchema()).toEqual(t.any.getSchema()); + expect(router.get('echo').type!.req.getSchema()).toEqual(t.any.getSchema()); }); }); diff --git a/src/value/__tests__/Value.spec.ts b/src/value/__tests__/Value.spec.ts new file mode 100644 index 00000000..55563f0d --- /dev/null +++ b/src/value/__tests__/Value.spec.ts @@ -0,0 +1,8 @@ +import {unknown, Value} from '../Value'; + +test('typeless value', () => { + const val = unknown('test'); + expect(val instanceof Value).toBe(true); + expect(val.data).toBe('test'); + expect(val.type).toBe(undefined); +}); diff --git a/src/value/index.ts b/src/value/index.ts index 61f26e13..1f5fb310 100644 --- a/src/value/index.ts +++ b/src/value/index.ts @@ -1,3 +1,3 @@ export {value} from './util'; -export {Value} from './Value'; +export {Value, unknown} from './Value'; export {ObjValue} from './ObjValue'; diff --git a/src/value/util.ts b/src/value/util.ts index e681701c..9466a3b5 100644 --- a/src/value/util.ts +++ b/src/value/util.ts @@ -7,5 +7,5 @@ export const value: { (type: T, data: unknown): Value; } = (type: any, data: any): any => { if (type.kind() === 'obj') return new ObjValue(type as classes.ObjType, data); - return new Value(type as classes.Type, data); + return new Value(data, type as classes.Type); }; diff --git a/yarn.lock b/yarn.lock index 95ff931a..8e06410b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -898,6 +898,7 @@ __metadata: jest: "npm:^29.7.0" rxjs: "npm:^7.8.2" sonic-forest: "npm:^1.2.1" + thingies: "npm:^2.5.0" tree-dump: "npm:^1.0.3" ts-jest: "npm:^29.1.2" tslib: "npm:^2.7.0"