Skip to content

Commit

Permalink
Parse RpcMessages with Zod
Browse files Browse the repository at this point in the history
  • Loading branch information
syyyr committed Sep 25, 2024
1 parent c916636 commit 1bdcb6b
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 227 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "libshv-js",
"version": "3.3.6",
"version": "3.4.0",
"description": "Typescript implementation of libshv",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
Expand Down
2 changes: 1 addition & 1 deletion src/chainpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class ChainPackReader {
}

const implReturn = (x: RpcValueType) => {
const ret = meta !== undefined ? new RpcValueWithMetaData(x, meta) : x;
const ret = meta !== undefined ? new RpcValueWithMetaData(meta, x) : x;
return ret;
};

Expand Down
2 changes: 1 addition & 1 deletion src/cpon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class CponReader {
}

const implReturn = (x: RpcValueType) => {
const ret = meta !== undefined ? new RpcValueWithMetaData(x, meta) : x;
const ret = meta !== undefined ? new RpcValueWithMetaData(meta, x) : x;
return ret;
};

Expand Down
237 changes: 57 additions & 180 deletions src/rpcmessage.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import {type IMap, type Int, isIMap, makeIMap, makeMetaMap, type MetaMap, type RpcValue, RpcValueWithMetaData} from './rpcvalue';
import {toCpon} from './cpon';
import {toChainPack} from './chainpack';
import * as z from './zod';

enum RpcMessageTag {
RequestId = 8,
ShvPath = 9,
Method = 10,
CallerIds = 11,
}
export const RPC_MESSAGE_REQUEST_ID = 8;
export const RPC_MESSAGE_SHV_PATH = 9;
export const RPC_MESSAGE_METHOD = 10;
export const RPC_MESSAGE_CALLER_IDS = 11;

enum RpcMessageKey {
Params = 1,
Result = 2,
Error = 3,
}
export const RPC_MESSAGE_PARAMS = 1;
export const RPC_MESSAGE_RESULT = 2;
export const RPC_MESSAGE_ERROR = 3;

export const ERROR_CODE = 1;
export const ERROR_MESSAGE = 2;
Expand All @@ -34,169 +28,52 @@ enum ErrorCode {
NotImplemented = 12,
}

export type ErrorMap = {
[ERROR_CODE]: ErrorCode;
[ERROR_MESSAGE]?: string;
[ERROR_DATA]?: RpcValue;
};

class RpcError extends Error {
constructor(private readonly err_info: ErrorMap) {
super(err_info[ERROR_MESSAGE] ?? 'Unknown RpcError');
}

data() {
return this.err_info[ERROR_DATA];
}
}

export class ProtocolError extends Error {}

export class InvalidRequest extends RpcError {}
export class MethodNotFound extends RpcError {}
export class InvalidParams extends RpcError {}
export class InternalError extends RpcError {}
export class ParseError extends RpcError {}
export class MethodCallTimeout extends RpcError {}
export class MethodCallCancelled extends RpcError {}
export class MethodCallException extends RpcError {}
export class Unknown extends RpcError {}
export class LoginRequired extends RpcError {}
export class UserIDRequired extends RpcError {}
export class NotImplemented extends RpcError {}

class RpcMessage {
value: IMap;
meta: MetaMap;
constructor(rpc_val?: RpcValue) {
if (rpc_val === undefined) {
this.value = makeIMap({});
this.meta = makeMetaMap({});
return;
}

if (!(rpc_val instanceof RpcValueWithMetaData && isIMap(rpc_val.value))) {
throw new TypeError(`RpcMessage initialized with a non-IMap: ${toCpon(rpc_val)}`);
}

this.value = rpc_val.value;
this.meta = rpc_val.meta;
}

isValid() {
return this.shvPath() && (this.isRequest() || this.isResponse || this.isSignal());
}

isRequest(): boolean {
return this.requestId() !== undefined && this.method() !== undefined;
}

isResponse(): boolean {
return this.requestId() !== undefined && this.method() === undefined;
}

isSignal(): boolean {
return this.requestId() === undefined && this.method() !== undefined;
}

requestId(): Int | undefined {
return (this.meta[RpcMessageTag.RequestId] as Int);
}

setRequestId(id: number) {
this.meta[RpcMessageTag.RequestId] = id;
}

callerIds(): RpcValue[] | undefined {
return this.meta[RpcMessageTag.CallerIds] as RpcValue[];
}

setCallerIds(ids: RpcValue[]) {
this.meta[RpcMessageTag.CallerIds] = ids;
}

shvPath(): string | undefined {
return (this.meta[RpcMessageTag.ShvPath] as string);
}

setShvPath(val: string) {
this.meta[RpcMessageTag.ShvPath] = val;
}

method(): string | undefined {
return (this.meta[RpcMessageTag.Method] as string);
}

setMethod(val: string) {
this.meta[RpcMessageTag.Method] = val;
}

params() {
return this.value[RpcMessageKey.Params] as RpcValue;
}

setParams(params: RpcValue) {
this.value[RpcMessageKey.Params] = params;
}

resultOrError() {
if (Object.hasOwn(this.value, RpcMessageKey.Error)) {
if (!isIMap(this.value[RpcMessageKey.Error])) {
return new ProtocolError('Response had an error, but this error was not a map');
}

const errorMap = this.value[RpcMessageKey.Error];
if (typeof errorMap[ERROR_CODE] !== 'number') {
return new ProtocolError('Response had an error, but this error did not contain at least an error code');
}

const code = errorMap[ERROR_CODE] as unknown;

const ErrorTypeCtor = (() => {
switch (code) {
case ErrorCode.InvalidRequest: return InvalidRequest;
case ErrorCode.MethodNotFound: return MethodNotFound;
case ErrorCode.InvalidParams: return InvalidParams;
case ErrorCode.InternalError: return InternalError;
case ErrorCode.ParseError: return ParseError;
case ErrorCode.MethodCallTimeout: return MethodCallTimeout;
case ErrorCode.MethodCallCancelled: return MethodCallCancelled;
case ErrorCode.MethodCallException: return MethodCallException;
case ErrorCode.Unknown: return Unknown;
case ErrorCode.LoginRequired: return LoginRequired;
case ErrorCode.UserIDRequired: return UserIDRequired;
case ErrorCode.NotImplemented: return NotImplemented;
default: return Unknown;
}
})();

return new ErrorTypeCtor(this.value[RpcMessageKey.Error] as IMap<ErrorMap>);
}

if (Object.hasOwn(this.value, RpcMessageKey.Result)) {
return this.value[RpcMessageKey.Result] as RpcValue;
}

return new ProtocolError('Response included neither result nor error');
}

setResult(result: RpcValue) {
this.value[RpcMessageKey.Result] = result;
}

setError(error: string) {
this.value[RpcMessageKey.Error] = error;
}

toCpon() {
return toCpon(new RpcValueWithMetaData(this.value, this.meta));
}

toChainPack() {
return toChainPack(new RpcValueWithMetaData(this.value, this.meta));
}
}

export type RpcResponse<T = RpcValue> = T | Error;

export {RpcMessage, RpcError, ErrorCode};
const ErrorMapZod = z.imap({
[ERROR_CODE]: z.number(),
[ERROR_MESSAGE]: z.string().optional(),
[ERROR_DATA]: z.rpcvalue().optional(),
});

export type ErrorMap = z.infer<typeof ErrorMapZod>;

const RpcRequestMetaZod = z.metamap({
[RPC_MESSAGE_REQUEST_ID]: z.number(),
[RPC_MESSAGE_METHOD]: z.string(),
[RPC_MESSAGE_SHV_PATH]: z.string(),
});

const RpcRequestValueZod = z.imap({
[RPC_MESSAGE_PARAMS]: z.rpcvalue().optional(),
});

const RpcResponseMetaZod = z.metamap({
[RPC_MESSAGE_REQUEST_ID]: z.number(),
});
const RpcResponseValueZod = z.imap({
[RPC_MESSAGE_RESULT]: z.rpcvalue(),
}).or(z.imap({
[RPC_MESSAGE_ERROR]: ErrorMapZod,
}));

const RpcSignalMetaZod = z.metamap({
[RPC_MESSAGE_SHV_PATH]: z.string(),
[RPC_MESSAGE_METHOD]: z.string(),
});
const RpcSignalValueZod = z.imap({
[RPC_MESSAGE_PARAMS]: z.rpcvalue().optional(),
});

const RpcRequestZod = z.withMeta(RpcRequestMetaZod, RpcRequestValueZod);
const RpcResponseZod = z.withMeta(RpcResponseMetaZod, RpcResponseValueZod);
const RpcSignalZod = z.withMeta(RpcSignalMetaZod, RpcSignalValueZod);
const RpcMessageZod = z.union([RpcRequestZod, RpcResponseZod, RpcSignalZod]);
export type RpcRequest = z.infer<typeof RpcRequestZod>;
export type RpcResponse = z.infer<typeof RpcResponseZod>;
export type RpcSignal = z.infer<typeof RpcSignalZod>;
export type RpcMessage = z.infer<typeof RpcMessageZod>;

export const isSignal = (message: RpcMessage): message is RpcSignal => !(RPC_MESSAGE_REQUEST_ID in message.meta) && RPC_MESSAGE_METHOD in message.meta;
export const isRequest = (message: RpcMessage): message is RpcRequest => RPC_MESSAGE_REQUEST_ID in message.meta && RPC_MESSAGE_METHOD in message.meta;
export const isResponse = (message: RpcMessage): message is RpcResponse => RPC_MESSAGE_REQUEST_ID in message.meta && !(RPC_MESSAGE_METHOD in message.meta);

export {RpcMessageZod, ErrorCode};
8 changes: 5 additions & 3 deletions src/rpcvalue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ const isShvMap = (x: unknown): x is ShvMap => typeof x === 'object' && (x as Shv

const isIMap = (x: unknown): x is IMap => typeof x === 'object' && (x as IMap)[shvMapType] === 'imap';

const isMetaMap = (x: unknown): x is IMap => typeof x === 'object' && (x as MetaMap)[shvMapType] === 'metamap';

const makeMetaMap = <T extends Record<string | number, RpcValue> = Record<string | number, RpcValue>, U extends Record<number, RpcValue> = Omit<T, typeof shvMapType>>(x: U = {} as U): MetaMap<U> => ({
...x,
[shvMapType]: 'metamap',
Expand All @@ -112,10 +114,10 @@ const makeMap = <T extends Record<string, RpcValue> = Record<string, RpcValue>,
[shvMapType]: 'map',
});

class RpcValueWithMetaData {
constructor(public value: RpcValueType, public meta: MetaMap) {}
class RpcValueWithMetaData<MetaSchema extends MetaMap = MetaMap, ValueSchema extends RpcValueType = RpcValueType> {
constructor(public meta: MetaSchema, public value: ValueSchema) {}
}

export type RpcValue = RpcValueType | RpcValueWithMetaData;

export {shvMapType, Decimal, Double, type IMap, type MetaMap, RpcValueWithMetaData, type ShvMap, UInt, withOffset, makeMap, makeIMap, makeMetaMap, isIMap, isShvMap};
export {shvMapType, Decimal, Double, type IMap, type MetaMap, RpcValueWithMetaData, type ShvMap, UInt, withOffset, makeMap, makeIMap, makeMetaMap, isIMap, isMetaMap, isShvMap};
Loading

0 comments on commit 1bdcb6b

Please sign in to comment.