Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON Type Value based router #480

Merged
merged 8 commits into from
Dec 8, 2023
26 changes: 23 additions & 3 deletions src/json-type-value/ObjectValue.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import {Value} from './Value';
import {toText} from '../json-type/typescript/toText';
import type {ResolveType} from '../json-type';
import type {ResolveType, TypeSystem} from '../json-type';
import type * as classes from '../json-type/type';
import type * as ts from '../json-type/typescript/types';

type UnObjectType<T> = T extends classes.ObjectType<infer U> ? U : never;
type UnObjectFieldTypeVal<T> = T extends classes.ObjectFieldType<any, infer U> ? U : never;
export type UnObjectType<T> = T extends classes.ObjectType<infer U> ? U : never;
export type UnObjectValue<T> = T extends ObjectValue<infer U> ? U : never;
export type UnObjectFieldTypeVal<T> = T extends classes.ObjectFieldType<any, infer U> ? U : never;
export type ObjectFieldToTuple<F> = F extends classes.ObjectFieldType<infer K, infer V> ? [K, V] : never;
export type ToObject<T> = T extends [string, unknown][] ? {[K in T[number] as K[0]]: K[1]} : never;
export type ObjectValueToTypeMap<F> = ToObject<{[K in keyof F]: ObjectFieldToTuple<F[K]>}>;

// export type MergeObjectsTypes<A, B> =
// A extends classes.ObjectType<infer A2>
Expand All @@ -22,6 +26,8 @@ type UnObjectFieldTypeVal<T> = T extends classes.ObjectFieldType<any, infer U> ?
// never;

export class ObjectValue<T extends classes.ObjectType<any>> extends Value<T> {
public static create = (system: TypeSystem) => new ObjectValue(system.t.obj, {});

public field<F extends classes.ObjectFieldType<any, any>>(
field: F,
data: ResolveType<UnObjectFieldTypeVal<F>>,
Expand Down Expand Up @@ -51,6 +57,20 @@ export class ObjectValue<T extends classes.ObjectType<any>> extends Value<T> {
return new ObjectValue(extendedType, extendedData) as any;
}

public get<K extends keyof ObjectValueToTypeMap<UnObjectType<T>>>(
key: K,
): Value<
ObjectValueToTypeMap<UnObjectType<T>>[K] extends classes.Type
? ObjectValueToTypeMap<UnObjectType<T>>[K]
: classes.Type
> {
const field = this.type.getField(<string>key);
if (!field) throw new Error('NO_FIELD');
const type = field.value;
const data = this.data[<string>key];
return new Value(type, data) as any;
}

public toTypeScriptAst(): ts.TsTypeLiteral {
const node: ts.TsTypeLiteral = {
node: 'TypeLiteral',
Expand Down
10 changes: 10 additions & 0 deletions src/json-type-value/Value.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import type {JsonValueCodec} from '../json-pack/codecs/types';
import type {ResolveType, Type} from '../json-type';

export class Value<T extends Type> {
constructor(public type: T, public data: ResolveType<T>) {}

public encode(codec: JsonValueCodec): void {
const value = this.data;
const type = this.type;
if (value === undefined) return;
const encoder = codec.encoder;
if (!type) encoder.writeAny(value);
else type.encoder(codec.format)(value, encoder);
}
}
11 changes: 11 additions & 0 deletions src/json-type-value/__tests__/ObjectValue.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {TypeSystem} from '../../json-type/system';
import {ObjectValue} from '../ObjectValue';

test('can retrieve field as Value', () => {
const system = new TypeSystem();
const {t} = system;
const obj = new ObjectValue(t.Object(t.prop('foo', t.str)), {foo: 'bar'});
const foo = obj.get('foo');
expect(foo.type.getTypeName()).toBe('str');
expect(foo.data).toBe('bar');
});
6 changes: 6 additions & 0 deletions src/json-type/type/classes/AbstractType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export abstract class AbstractType<S extends schema.Schema> implements BaseType<
/** @todo Retype this to `Schema`. */
protected abstract schema: S;

public getSystem(): TypeSystem {
const system = this.system;
if (!system) throw new Error('NO_SYSTEM');
return system;
}

public getTypeName(): S['__t'] {
return this.schema.__t;
}
Expand Down
10 changes: 0 additions & 10 deletions src/reactive-rpc/common/messages/Value.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {Value as V} from '../../../json-type-value/Value';
import type {JsonValueCodec} from '../../../json-pack/codecs/types';
import type {Type} from '../../../json-type';

/**
Expand All @@ -9,13 +8,4 @@ export class RpcValue<V = unknown> extends V<any> {
constructor(public data: V, public type: Type | undefined) {
super(type, data);
}

public encode(codec: JsonValueCodec): void {
const value = this.data;
const type = this.type;
if (value === undefined) return;
const encoder = codec.encoder;
if (!type) encoder.writeAny(value);
else type.encoder(codec.format)(value, encoder);
}
}
125 changes: 125 additions & 0 deletions src/reactive-rpc/common/rpc/caller/ObjectValueCaller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {RpcError} from './error';
import {RpcCaller, type RpcApiCallerOptions} from './RpcCaller';
import {type AbstractType, FunctionStreamingType, FunctionType} from '../../../../json-type/type/classes';
import {StaticRpcMethod, type StaticRpcMethodOptions} from '../methods/StaticRpcMethod';
import {StreamingRpcMethod, type StreamingRpcMethodOptions} from '../methods/StreamingRpcMethod';
import {
type ObjectType,
type Schema,
type TypeSystem,
ObjectFieldType,
TypeOf,
SchemaOf,
Type,
} from '../../../../json-type';
import type {ObjectValue, UnObjectType, UnObjectValue} from '../../../../json-type-value/ObjectValue';
import type {Value} from '../../../../json-type-value/Value';
import type {Observable} from 'rxjs';
import type {RpcValue} from '../../messages/Value';

type ObjectFieldToTuple<F> = F extends ObjectFieldType<infer K, infer V> ? [K, V] : never;
type ToObject<T> = T extends [string, unknown][] ? {[K in T[number] as K[0]]: K[1]} : never;
type ObjectFieldsToMap<F> = ToObject<{[K in keyof F]: ObjectFieldToTuple<F[K]>}>;
type ObjectValueToTypeMap<V> = ObjectFieldsToMap<UnObjectType<UnObjectValue<V>>>;

type MethodReq<F> = F extends FunctionType<infer Req, any>
? TypeOf<SchemaOf<Req>>
: F extends FunctionStreamingType<infer Req, any>
? TypeOf<SchemaOf<Req>>
: never;

type MethodRes<F> = F extends FunctionType<any, infer Res>
? TypeOf<SchemaOf<Res>>
: F extends FunctionStreamingType<any, infer Res>
? TypeOf<SchemaOf<Res>>
: never;

type MethodDefinition<Ctx, F> = F extends FunctionType<any, any>
? StaticRpcMethodOptions<Ctx, MethodReq<F>, MethodRes<F>>
: F extends FunctionStreamingType<any, any>
? StreamingRpcMethodOptions<Ctx, MethodReq<F>, MethodRes<F>>
: never;

export interface ObjectValueCallerOptions<V extends ObjectValue<ObjectType<any>>, Ctx = unknown>
extends Omit<RpcApiCallerOptions<Ctx>, 'getMethod'> {
router: V;
}

export class ObjectValueCaller<V extends ObjectValue<ObjectType<any>>, Ctx = unknown> extends RpcCaller<Ctx> {
protected readonly router: V;
protected readonly system: TypeSystem;
protected readonly methods = new Map<string, StaticRpcMethod<Ctx> | StreamingRpcMethod<Ctx>>();

constructor({router: value, ...rest}: ObjectValueCallerOptions<V, Ctx>) {
super({
...rest,
getMethod: (name) => this.get(name) as any as StaticRpcMethod<Ctx> | StreamingRpcMethod<Ctx>,
});
this.router = value;
const system = value.type.system;
if (!system) throw new Error('NO_SYSTEM');
this.system = system;
}

public get<K extends keyof ObjectValueToTypeMap<V>>(
id: K,
): MethodDefinition<Ctx, ObjectValueToTypeMap<V>[K]> | undefined {
let method = this.methods.get(id as string) as any;
if (method) return method;
const fn = this.router.get(<string>id) as Value<Type>;
if (!fn || !(fn.type instanceof FunctionType || fn.type instanceof FunctionStreamingType)) {
return undefined;
}
const fnType = fn.type as FunctionType<Type, Type> | FunctionStreamingType<Type, Type>;
const {req, res} = fnType;
const call = fn.data;
const validator = fnType.req.validator('object');
const requestSchema = (fnType.req as AbstractType<Schema>).getSchema();
const isRequestVoid = requestSchema.__t === 'const' && requestSchema.value === undefined;
const validate = isRequestVoid
? () => {}
: (req: unknown) => {
const error: any = validator(req);
if (error) {
const message = error.message + (Array.isArray(error?.path) ? ' Path: /' + error.path.join('/') : '');
throw RpcError.value(RpcError.validation(message, error));
}
};
method =
fnType instanceof FunctionType
? new StaticRpcMethod({req, res, validate, call})
: new StreamingRpcMethod({req, res, validate, call$: call});
this.methods.set(id as string, method as any);
return method;
}

public async call<K extends keyof ObjectValueToTypeMap<V>>(
id: K,
request: MethodReq<ObjectValueToTypeMap<V>[K]>,
ctx: Ctx,
): Promise<RpcValue<MethodRes<ObjectValueToTypeMap<V>[K]>>> {
return super.call(id as string, request, ctx) as any;
}

public async callSimple<K extends keyof ObjectValueToTypeMap<V>>(
id: K,
request: MethodReq<ObjectValueToTypeMap<V>[K]>,
ctx: Ctx = {} as any,
): Promise<MethodRes<ObjectValueToTypeMap<V>[K]>> {
try {
const res = await this.call(id as string, request, ctx);
return res.data;
} catch (err) {
const error = err as RpcValue<RpcError>;
throw error.data;
}
}

public call$<K extends keyof ObjectValueToTypeMap<V>>(
id: K,
request: Observable<MethodReq<ObjectValueToTypeMap<V>[K]>>,
ctx: Ctx,
): Observable<RpcValue<MethodRes<ObjectValueToTypeMap<V>[K]>>> {
return super.call$(id as string, request, ctx) as any;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {TypeSystem} from '../../../../../json-type';
import {ObjectValue} from '../../../../../json-type-value/ObjectValue';
import {ObjectValueCaller} from '../ObjectValueCaller';

test('can execute simple calls', async () => {
const system = new TypeSystem();
const {t} = system;
const router = ObjectValue.create(system)
.prop('ping', t.Function(t.any, t.Const(<const>'pong')), async () => 'pong')
.prop('echo', t.Function(t.any, t.any), async (req) => req);
const caller = new ObjectValueCaller({router});
const res1 = await caller.call('ping', null, {});
expect(res1.data).toBe('pong');
const res2 = await caller.callSimple('echo', {foo: 'bar'}, {});
expect(res2).toEqual({foo: 'bar'});
});
13 changes: 10 additions & 3 deletions src/reactive-rpc/common/testing/buildE2eClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import type {Observable} from 'rxjs';
import type {ResolveType} from '../../../json-type';
import type {TypeRouter} from '../../../json-type/system/TypeRouter';
import type {TypeRouterCaller} from '../rpc/caller/TypeRouterCaller';
import type {RpcCaller} from '../rpc/caller/RpcCaller';
import type {ObjectValueCaller} from '../rpc/caller/ObjectValueCaller';
import type {ObjectValue, ObjectValueToTypeMap, UnObjectType} from '../../../json-type-value/ObjectValue';

export interface BuildE2eClientOptions {
/**
Expand Down Expand Up @@ -64,7 +67,7 @@ export interface BuildE2eClientOptions {
token?: string;
}

export const buildE2eClient = <Caller extends TypeRouterCaller<any>>(caller: Caller, opt: BuildE2eClientOptions) => {
export const buildE2eClient = <Caller extends RpcCaller<any>>(caller: Caller, opt: BuildE2eClientOptions) => {
const writer = opt.writer ?? new Writer(Fuzzer.randomInt2(opt.writerDefaultBufferKb ?? [4, 4]) * 1024);
const codecs = new RpcCodecs(new Codecs(writer), new RpcMessageCodecs());
const ctx = new ConnectionContext(
Expand Down Expand Up @@ -109,8 +112,12 @@ export const buildE2eClient = <Caller extends TypeRouterCaller<any>>(caller: Cal
};
};

type UnTypeRouterCaller<T> = T extends TypeRouterCaller<infer R> ? R : never;
type UnTypeRouter<T> = T extends TypeRouter<infer R> ? R : never;
type UnTypeRouterCaller<T> = T extends TypeRouterCaller<infer R> ? R : T extends ObjectValueCaller<infer R> ? R : never;
type UnTypeRouter<T> = T extends TypeRouter<infer R>
? R
: T extends ObjectValue<infer R>
? ObjectValueToTypeMap<UnObjectType<R>>
: never;
type UnwrapFunction<F> = F extends FunctionType<infer Req, infer Res>
? (req: ResolveType<Req>) => Promise<ResolveType<Res>>
: F extends FunctionStreamingType<infer Req, infer Res>
Expand Down
29 changes: 15 additions & 14 deletions src/server/routes/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,25 @@ import {edit} from './methods/edit';
import {listen} from './methods/listen';
import {Block, BlockId, BlockPatch, BlockSeq} from './schema';
import {history} from './methods/history';
import type {RoutesBase, TypeRouter} from '../../../json-type/system/TypeRouter';
import type {RouteDeps} from '../types';
import type {RouteDeps, Router, RouterBase} from '../types';

export const blocks =
(d: RouteDeps) =>
<R extends RoutesBase>(r: TypeRouter<R>) => {
r.system.alias('BlockId', BlockId);
r.system.alias('BlockSeq', BlockSeq);
r.system.alias('Block', Block);
r.system.alias('BlockPatch', BlockPatch);
<R extends RouterBase>(r: Router<R>) => {
const {system} = d;

system.alias('BlockId', BlockId);
system.alias('BlockSeq', BlockSeq);
system.alias('Block', Block);
system.alias('BlockPatch', BlockPatch);

// prettier-ignore
return (
( create(d)
( get(d)
( remove(d)
( edit(d)
( listen(d)
( history(d)
( r ))))))));
( create(d)
( get(d)
( remove(d)
( edit(d)
( listen(d)
( history(d)
( r ))))))));
};
30 changes: 12 additions & 18 deletions src/server/routes/blocks/methods/create.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import type {RoutesBase, TypeRouter} from '../../../../json-type/system/TypeRouter';
import type {RouteDeps} from '../../types';
import type {RouteDeps, Router, RouterBase} from '../../types';
import type {BlockId, BlockPatch} from '../schema';

export const create =
({services}: RouteDeps) =>
<R extends RoutesBase>(router: TypeRouter<R>) => {
const t = router.t;

({t, services}: RouteDeps) =>
<R extends RouterBase>(r: Router<R>) => {
const Request = t.Object(
t.prop('id', t.Ref<typeof BlockId>('BlockId')).options({
title: 'New block ID',
Expand All @@ -20,17 +17,14 @@ export const create =

const Response = t.obj;

const Func = t
.Function(Request, Response)
.options({
title: 'Create Block',
intro: 'Creates a new block or applies patches to it.',
description: 'Creates a new block or applies patches to it.',
})
.implement(async ({id, patches}) => {
const {block} = await services.blocks.create(id, patches);
return {};
});
const Func = t.Function(Request, Response).options({
title: 'Create Block',
intro: 'Creates a new block or applies patches to it.',
description: 'Creates a new block or applies patches to it.',
});

return router.fn('blocks.create', Func);
return r.prop('blocks.create', Func, async ({id, patches}) => {
const {block} = await services.blocks.create(id, patches);
return {};
});
};
Loading