libshv-js 3.10.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,3 +28,4 @@ jobs:
28
28
  run: |
29
29
  node test.js
30
30
  node test_json.js
31
+ node test_zod.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libshv-js",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
4
4
  "description": "Typescript implementation of libshv",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -28,7 +28,7 @@
28
28
  "./zod.ts": "./src/zod.ts"
29
29
  },
30
30
  "dependencies": {
31
- "zod": "^3.24.2"
31
+ "zod": "^3.25.67"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@typescript-eslint/parser": "^8.27.0",
package/src/rpcvalue.ts CHANGED
@@ -102,6 +102,25 @@ const isIMap = (x: unknown): x is IMap => typeof x === 'object' && (x as IMap)[s
102
102
 
103
103
  const isMetaMap = (x: unknown): x is IMap => typeof x === 'object' && (x as MetaMap)[shvMapType] === 'metamap';
104
104
 
105
+ export const typeName = (x: unknown): string => {
106
+ switch (true) {
107
+ case x === undefined: return 'Null';
108
+ case typeof x === 'boolean': return 'Bool';
109
+ case typeof x === 'string': return 'String';
110
+ case x instanceof ArrayBuffer: return 'Blob';
111
+ case x instanceof UInt: return 'UInt';
112
+ case typeof x === 'number': return 'Int';
113
+ case x instanceof Double: return 'Double';
114
+ case x instanceof Decimal: return 'Decimal';
115
+ case Array.isArray(x): return 'List';
116
+ case x instanceof Date: return 'Date';
117
+ case isShvMap(x): return 'ShvMap';
118
+ case isIMap(x): return 'IMap';
119
+ case isMetaMap(x): return 'MetaMap';
120
+ default: return '<unknown type>';
121
+ }
122
+ };
123
+
105
124
  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> => ({
106
125
  ...x,
107
126
  [shvMapType]: 'metamap',
package/src/ws-client.ts CHANGED
@@ -162,6 +162,7 @@ class WsClient {
162
162
 
163
163
  this.websocket.addEventListener('message', async (evt: MessageEvent<ArrayBuffer>) => {
164
164
  const rpcVal = dataToRpcValue(evt.data);
165
+
165
166
  const rpcMsg = RpcMessageZod.parse(rpcVal);
166
167
  this.logDebug(`message received: ${toCpon(rpcMsg)}`);
167
168
 
@@ -199,7 +200,9 @@ class WsClient {
199
200
  }));
200
201
  } catch (error: unknown) {
201
202
  const sendError = (error: RpcError) => {
202
- respond(error.err_info);
203
+ respond(makeIMap({
204
+ [RPC_MESSAGE_ERROR]: error.err_info,
205
+ }));
203
206
  };
204
207
 
205
208
  switch (true) {
package/src/zod.ts CHANGED
@@ -1,21 +1,44 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import {z, type ZodType} from 'zod';
3
- import {Decimal, Double, type IMap, isIMap, isMetaMap, isShvMap, type MetaMap, type RpcValue, type RpcValueType, RpcValueWithMetaData, type ShvMap, UInt} from './rpcvalue';
2
+ import {z, type ZodType} from 'zod/v4';
3
+ import {Decimal, Double, isIMap, isMetaMap, isShvMap, type MetaMap, RpcValue, type RpcValueType, RpcValueWithMetaData, shvMapType, typeName, UInt} from './rpcvalue';
4
4
 
5
- export const map = <T extends Record<string, ZodType<any, any, any>>>(schema?: T) => z.custom<ShvMap<{[K in keyof T]: z.infer<T[K]>}>>((data: RpcValue) => isShvMap(data) && (schema === undefined || z.object(schema).safeParse(data).success));
6
- export const recmap = <T extends ZodType<any, any, any>>(schema: T) => z.custom<ShvMap<Record<string, z.infer<T>>>>((data: RpcValue) => isShvMap(data) && z.record(z.string(), schema).safeParse(data).success);
7
- export const imap = <T extends Record<string, ZodType<any, any, any>>>(schema?: T) => z.custom<IMap<{[K in keyof T]: z.infer<T[K]>}>>((data: RpcValue) => isIMap(data) && (schema === undefined || z.object(schema).safeParse(data).success));
8
- export const metamap = <T extends Record<string | number, ZodType<any, any, any>>>(schema?: T) => z.custom<MetaMap<{[K in keyof T]: z.infer<T[K]>}>>((data: RpcValue) => isMetaMap(data) && (schema === undefined || z.object(schema).safeParse(data).success));
5
+ const implMakeMapParser = <MapBrand extends string, ObjectParser extends ZodType<object>>(mapValidator: (val: unknown) => boolean, _mapBrand: MapBrand, mapName: string, objectParser: ObjectParser) => z.custom<z.infer<ObjectParser> & {[shvMapType]: MapBrand}>().check(ctx => {
6
+ if (!mapValidator(ctx.value)) {
7
+ ctx.issues.push({
8
+ expected: 'map',
9
+ code: 'invalid_type',
10
+ input: ctx.value,
11
+ message: `Invalid input: expected ${mapName}, received ${typeName(ctx.value)}`,
12
+ });
13
+ return;
14
+ }
15
+
16
+ // For record, Zod finds out map brand, and calls the record's value parser with the value. There's no way to detect
17
+ // it, because Zod does not supply the parser with the key. To prevent this, remove the key before passing the value
18
+ // to Zod. This unfortunately means that we need to copy the whole object. :/
19
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
+ const {[shvMapType]: zodMustNotHaveThis, ...rest} = {...ctx.value};
21
+ const parsedObject = objectParser.safeParse(rest);
22
+
23
+ if (!parsedObject.success) {
24
+ ctx.issues = parsedObject.error.issues;
25
+ }
26
+ });
27
+
28
+ export const map = <T extends Record<string, ZodType<any>>>(schema: T) => implMakeMapParser(isShvMap, 'map', 'ShvMap', z.object(schema));
29
+ export const imap = <T extends Record<number, ZodType<any>>>(schema: T) => implMakeMapParser(isIMap, 'imap', 'IMap', z.object(schema));
30
+ export const metamap = <T extends Record<string | number, ZodType<any>>>(schema: T) => implMakeMapParser(isMetaMap, 'metamap', 'MetaMap', z.object(schema));
31
+ export const recmap = <T extends ZodType<any>>(schema: T) => implMakeMapParser(val => isShvMap(val) || isIMap(val), 'map', 'ShvMap', z.record(z.any(), schema));
9
32
  export const int = () => z.number();
10
33
 
11
34
  export const uint = () => z.instanceof(UInt<number>);
12
35
  export const double = () => z.instanceof(Double);
13
36
  export const decimal = () => z.instanceof(Decimal);
14
37
  export const blob = () => z.instanceof(ArrayBuffer);
15
- export const list = () => z.array(z.lazy(rpcvalue));
38
+ export const list: () => z.ZodArray<z.ZodLazy<z.ZodType<RpcValue>>> = () => z.array(z.lazy<z.ZodType<RpcValue>>(rpcvalue));
16
39
 
17
40
  const withMetaInstanceParser = z.instanceof(RpcValueWithMetaData);
18
- export const rpcvalue: () => ZodType<RpcValue, any, any> = () => z.union([
41
+ export const rpcvalue: () => ZodType<RpcValue> = () => z.lazy(() => z.union([
19
42
  z.undefined(),
20
43
  z.boolean(),
21
44
  z.number(),
@@ -26,15 +49,48 @@ export const rpcvalue: () => ZodType<RpcValue, any, any> = () => z.union([
26
49
  z.string(),
27
50
  z.date(),
28
51
  list(),
29
- map(),
30
- imap(),
52
+ recmap(rpcvalue()),
31
53
  withMetaInstanceParser,
32
- ]);
54
+ ]) as z.ZodType<RpcValue>);
55
+
56
+ export const withMeta = <MetaSchema extends MetaMap, ValueSchema extends RpcValueType>(metaParser: ZodType<MetaSchema>, valueParser: ZodType<ValueSchema>) =>
57
+ z.custom<RpcValueWithMetaData<z.infer<typeof metaParser>, z.infer<typeof valueParser>>>().check(ctx => {
58
+ if (!withMetaInstanceParser.safeParse(ctx.value).success) {
59
+ ctx.issues.push({
60
+ expected: 'map',
61
+ code: 'invalid_type',
62
+ input: ctx.value,
63
+ message: 'Invalid input: expected a value with metadata, got a value with no metadata}',
64
+ });
65
+ return;
66
+ }
67
+
68
+ const parsedMeta = metaParser.safeParse(ctx.value.meta);
69
+ if (!parsedMeta.success) {
70
+ ctx.issues = [
71
+ {
72
+ expected: 'map',
73
+ code: 'invalid_type',
74
+ input: ctx.value,
75
+ message: 'Wrong RpcValueWithMetaData meta',
76
+ },
77
+ ...parsedMeta.error.issues,
78
+ ];
79
+ return;
80
+ }
33
81
 
34
- export const withMeta = <MetaSchema extends MetaMap, ValueSchema extends RpcValueType>(metaParser: ZodType<MetaSchema, any, any>, valueParser: ZodType<ValueSchema, any, any>) =>
35
- z.custom<RpcValueWithMetaData<z.infer<typeof metaParser>, z.infer<typeof valueParser>>>((data: any) => withMetaInstanceParser.and(z.object({
36
- meta: metaParser,
37
- value: valueParser,
38
- })).safeParse(data).success);
82
+ const parsedValue = valueParser.safeParse(ctx.value.value);
83
+ if (!parsedValue.success) {
84
+ ctx.issues = [
85
+ {
86
+ expected: 'map',
87
+ code: 'invalid_type',
88
+ input: ctx.value,
89
+ message: 'Wrong RpcValueWithMetaData value',
90
+ },
91
+ ...parsedValue.error.issues,
92
+ ];
93
+ }
94
+ });
39
95
 
40
- export * from 'zod';
96
+ export * from 'zod/v4';
package/test_zod.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { fromCpon } from './src/cpon';
2
+ import { makeIMap, makeMap, makeMetaMap } from './src/rpcvalue';
3
+ import * as z from './src/zod';
4
+
5
+ z.map({ ok: z.boolean(), })
6
+ .parse(makeMap({ok: true}))
7
+ .ok;
8
+ z.imap({ 1: z.boolean(), })
9
+ .parse(makeIMap({1: true}))[1];
10
+
11
+ {
12
+ const x = z.metamap({1: z.boolean(), ok: z.boolean()})
13
+ .parse(makeMetaMap({1: true, ok: true}))
14
+ x.ok;
15
+ x[1];
16
+ }
17
+
18
+ const SiteInfoZod = z.recmap(z.map({
19
+ optKey: z.map({
20
+ }).optional(),
21
+ }));
22
+
23
+ const lol = fromCpon(`{"key": {}}`);
24
+ SiteInfoZod.parse(lol);