@swimmesberger/elarion-jsonrpc-client-generator 0.2.17 → 0.2.18
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.
- package/README.md +4 -1
- package/dist/json-schema-to-zod.js +31 -15
- package/dist/rpc-client-source.js +31 -0
- package/dist/schema.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,7 +76,10 @@ before validation, or set `validateResults: false` when another layer validates
|
|
|
76
76
|
Params validation is also enabled by default through `rpcParamsSchemas`: single calls and batch items are checked
|
|
77
77
|
against the exported schema's constraints (lengths, ranges, patterns, formats) before the request is sent, and a failure
|
|
78
78
|
throws `RpcParamsValidationError` locally — naming the method, with the underlying Zod error as `cause` — so tier-1
|
|
79
|
-
contract violations never reach the wire. Set `validateParams: false` to opt out.
|
|
79
|
+
contract violations never reach the wire. Set `validateParams: false` to opt out. The error's `fieldErrors` groups the
|
|
80
|
+
issue messages by field path in the server's `ValidationErrorData.fieldErrors` notation (`"address.street"`,
|
|
81
|
+
`"lines[1].sku"`, `""` for the params object), and a constraint's explicit server message
|
|
82
|
+
(`x-elarion-error-messages` in the schema) is the message of its Zod check.
|
|
80
83
|
|
|
81
84
|
## Client-event subscriptions
|
|
82
85
|
|
|
@@ -13,13 +13,13 @@ export function jsonSchemaToZod(schema, ctx, indent = 0) {
|
|
|
13
13
|
const allStrings = values.every((value) => typeof value === 'string');
|
|
14
14
|
let zodExpression;
|
|
15
15
|
if (allStrings && values.length > 0) {
|
|
16
|
-
zodExpression = `z.enum([${values.map((value) => JSON.stringify(value)).join(', ')}])`;
|
|
16
|
+
zodExpression = `z.enum([${values.map((value) => JSON.stringify(value)).join(', ')}]${paramsArg(resolved, 'enum')})`;
|
|
17
17
|
}
|
|
18
18
|
else {
|
|
19
19
|
const literals = values.map((value) => `z.literal(${JSON.stringify(value)})`);
|
|
20
20
|
zodExpression = literals.length === 1
|
|
21
|
-
?
|
|
22
|
-
: `z.union([${literals.join(', ')}])`;
|
|
21
|
+
? `z.literal(${JSON.stringify(values[0])}${paramsArg(resolved, 'enum')})`
|
|
22
|
+
: `z.union([${literals.join(', ')}]${paramsArg(resolved, 'enum')})`;
|
|
23
23
|
}
|
|
24
24
|
return nullish(zodExpression, nullable);
|
|
25
25
|
}
|
|
@@ -56,30 +56,46 @@ export function jsonSchemaToZod(schema, ctx, indent = 0) {
|
|
|
56
56
|
function nullish(expression, nullable) {
|
|
57
57
|
return nullable ? `${expression}.nullish()` : expression;
|
|
58
58
|
}
|
|
59
|
+
// A Zod check call carrying, as its last argument, the explicit DataAnnotations message the server exporter
|
|
60
|
+
// attached to the check's JSON Schema keyword. Checks take the plain-string message form, which Zod v3 and v4
|
|
61
|
+
// both accept without deprecation. No message → no argument, so message-free schemas stay byte-identical.
|
|
62
|
+
function check(name, args, schema, keyword) {
|
|
63
|
+
const message = errorMessage(schema, keyword);
|
|
64
|
+
return `.${name}(${(message === undefined ? args : [...args, JSON.stringify(message)]).join(', ')})`;
|
|
65
|
+
}
|
|
66
|
+
// Schema constructors (z.enum, z.literal, z.union) take the message as `{ message }`, the one params form
|
|
67
|
+
// Zod v3 (no string shorthand there) and v4 share.
|
|
68
|
+
function paramsArg(schema, keyword) {
|
|
69
|
+
const message = errorMessage(schema, keyword);
|
|
70
|
+
return message === undefined ? '' : `, { message: ${JSON.stringify(message)} }`;
|
|
71
|
+
}
|
|
72
|
+
function errorMessage(schema, keyword) {
|
|
73
|
+
return schema['x-elarion-error-messages']?.[keyword];
|
|
74
|
+
}
|
|
59
75
|
// Constraint keywords apply to the inner type in a deterministic modifier order — base type, then
|
|
60
76
|
// `.int()`, then bounds (min-side before max-side), then format/regex refinements — with `.nullish()`
|
|
61
77
|
// always appended last by the caller.
|
|
62
78
|
function stringSchema(schema) {
|
|
63
79
|
let expression = 'z.string()';
|
|
64
80
|
if (schema.minLength !== undefined) {
|
|
65
|
-
expression +=
|
|
81
|
+
expression += check('min', [String(schema.minLength)], schema, 'minLength');
|
|
66
82
|
}
|
|
67
83
|
if (schema.maxLength !== undefined) {
|
|
68
|
-
expression +=
|
|
84
|
+
expression += check('max', [String(schema.maxLength)], schema, 'maxLength');
|
|
69
85
|
}
|
|
70
86
|
if (schema.format === 'uuid') {
|
|
71
|
-
expression += '
|
|
87
|
+
expression += check('uuid', [], schema, 'format');
|
|
72
88
|
}
|
|
73
89
|
else if (schema.format === 'email') {
|
|
74
|
-
expression += '
|
|
90
|
+
expression += check('email', [], schema, 'format');
|
|
75
91
|
}
|
|
76
92
|
else if (schema.format === 'uri') {
|
|
77
|
-
expression += '
|
|
93
|
+
expression += check('url', [], schema, 'format');
|
|
78
94
|
}
|
|
79
95
|
if (schema.pattern !== undefined) {
|
|
80
96
|
// new RegExp(<JSON string literal>) is deterministic and safe for any pattern content (slashes,
|
|
81
97
|
// quotes, backslashes) — a /.../ regex literal would need its own escaping rules.
|
|
82
|
-
expression +=
|
|
98
|
+
expression += check('regex', [`new RegExp(${JSON.stringify(schema.pattern)})`], schema, 'pattern');
|
|
83
99
|
}
|
|
84
100
|
return expression;
|
|
85
101
|
}
|
|
@@ -89,26 +105,26 @@ function numberSchema(schema, base) {
|
|
|
89
105
|
expression += '.int()';
|
|
90
106
|
}
|
|
91
107
|
if (schema.minimum !== undefined) {
|
|
92
|
-
expression +=
|
|
108
|
+
expression += check('gte', [String(schema.minimum)], schema, 'minimum');
|
|
93
109
|
}
|
|
94
110
|
if (schema.exclusiveMinimum !== undefined) {
|
|
95
|
-
expression +=
|
|
111
|
+
expression += check('gt', [String(schema.exclusiveMinimum)], schema, 'exclusiveMinimum');
|
|
96
112
|
}
|
|
97
113
|
if (schema.maximum !== undefined) {
|
|
98
|
-
expression +=
|
|
114
|
+
expression += check('lte', [String(schema.maximum)], schema, 'maximum');
|
|
99
115
|
}
|
|
100
116
|
if (schema.exclusiveMaximum !== undefined) {
|
|
101
|
-
expression +=
|
|
117
|
+
expression += check('lt', [String(schema.exclusiveMaximum)], schema, 'exclusiveMaximum');
|
|
102
118
|
}
|
|
103
119
|
return expression;
|
|
104
120
|
}
|
|
105
121
|
function arraySchema(expression, schema) {
|
|
106
122
|
let result = expression;
|
|
107
123
|
if (schema.minItems !== undefined) {
|
|
108
|
-
result +=
|
|
124
|
+
result += check('min', [String(schema.minItems)], schema, 'minItems');
|
|
109
125
|
}
|
|
110
126
|
if (schema.maxItems !== undefined) {
|
|
111
|
-
result +=
|
|
127
|
+
result += check('max', [String(schema.maxItems)], schema, 'maxItems');
|
|
112
128
|
}
|
|
113
129
|
return result;
|
|
114
130
|
}
|
|
@@ -258,15 +258,46 @@ export function generateRpcClientSource(options) {
|
|
|
258
258
|
'// params schema. `cause` carries the underlying Zod error with the structured issues.',
|
|
259
259
|
'export class RpcParamsValidationError extends Error {',
|
|
260
260
|
' readonly method: RpcMethod',
|
|
261
|
+
' // The issue messages keyed by field path in the server notation of ValidationErrorData.fieldErrors',
|
|
262
|
+
' // ("address.street", "items[1].name"; "" for the params object itself), so one form helper can mark',
|
|
263
|
+
' // fields for a local failure and for a server validation RpcError alike.',
|
|
264
|
+
' readonly fieldErrors: Record<string, string[]>',
|
|
261
265
|
'',
|
|
262
266
|
' constructor(method: RpcMethod, cause: unknown) {',
|
|
263
267
|
' const detail = cause instanceof Error ? `: ${cause.message}` : ' + "'.'",
|
|
264
268
|
' super(`Invalid params for RPC method ${JSON.stringify(method)}${detail}`, { cause })',
|
|
265
269
|
" this.name = 'RpcParamsValidationError'",
|
|
266
270
|
' this.method = method',
|
|
271
|
+
' this.fieldErrors = issueFieldErrors(cause)',
|
|
267
272
|
' }',
|
|
268
273
|
'}',
|
|
269
274
|
'',
|
|
275
|
+
'// Groups Zod issues (v3 and v4 share the `issues: { path, message }[]` shape) by field path: member',
|
|
276
|
+
'// names join with ".", array indexes become "[i]" — the notation of the server\'s field-error keys.',
|
|
277
|
+
'function issueFieldErrors(cause: unknown): Record<string, string[]> {',
|
|
278
|
+
" const issues = (cause as { issues?: unknown } | null | undefined)?.issues",
|
|
279
|
+
' if (!Array.isArray(issues)) {',
|
|
280
|
+
' return {}',
|
|
281
|
+
' }',
|
|
282
|
+
'',
|
|
283
|
+
' // A Map, then Object.fromEntries: a field named like an Object.prototype member stays an own key.',
|
|
284
|
+
' const fieldErrors = new Map<string, string[]>()',
|
|
285
|
+
' for (const issue of issues as readonly { path?: readonly PropertyKey[]; message?: unknown }[]) {',
|
|
286
|
+
" let key = ''",
|
|
287
|
+
' for (const segment of issue.path ?? []) {',
|
|
288
|
+
" key += typeof segment === 'number' ? `[${segment}]` : key === '' ? String(segment) : `.${String(segment)}`",
|
|
289
|
+
' }',
|
|
290
|
+
' const messages = fieldErrors.get(key)',
|
|
291
|
+
' if (messages) {',
|
|
292
|
+
' messages.push(String(issue.message))',
|
|
293
|
+
' } else {',
|
|
294
|
+
' fieldErrors.set(key, [String(issue.message)])',
|
|
295
|
+
' }',
|
|
296
|
+
' }',
|
|
297
|
+
'',
|
|
298
|
+
' return Object.fromEntries(fieldErrors)',
|
|
299
|
+
'}',
|
|
300
|
+
'',
|
|
270
301
|
'export function createRpcApi(options: RpcClientOptions): RpcApi {',
|
|
271
302
|
' const client = createRpcClient(options)',
|
|
272
303
|
' const calls: Record<string, unknown> = {}',
|
package/dist/schema.d.ts
CHANGED
|
@@ -25,6 +25,13 @@ export interface JsonSchema {
|
|
|
25
25
|
exclusiveMaximum?: number;
|
|
26
26
|
minItems?: number;
|
|
27
27
|
maxItems?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Explicit error messages of the DataAnnotations behind the constraint keywords, keyed by keyword (for example
|
|
30
|
+
* `{ pattern: 'Code must be three capital letters.' }`), emitted by the server exporter only for attributes
|
|
31
|
+
* that declare a message. The generator passes each one to the matching Zod check, so client-side
|
|
32
|
+
* pre-validation reports the server's text.
|
|
33
|
+
*/
|
|
34
|
+
'x-elarion-error-messages'?: Record<string, string>;
|
|
28
35
|
}
|
|
29
36
|
/**
|
|
30
37
|
* One declared error of a method (ADR-0080): the kind it fails with (the kind's default code, for example
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swimmesberger/elarion-jsonrpc-client-generator",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"description": "Generate TypeScript RPC contracts, Zod schemas, and a fetch client from Elarion JSON-RPC schema exports.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|