Skip to content

Commit

Permalink
Merge pull request #480 from streamich/json-type-value-router
Browse files Browse the repository at this point in the history
JSON Type Value based router
  • Loading branch information
streamich authored Dec 8, 2023
2 parents 76908ec + bcee118 commit 2f542f6
Show file tree
Hide file tree
Showing 26 changed files with 421 additions and 292 deletions.
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

0 comments on commit 2f542f6

Please sign in to comment.