@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 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
- ? literals[0]
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 += `.min(${schema.minLength})`;
81
+ expression += check('min', [String(schema.minLength)], schema, 'minLength');
66
82
  }
67
83
  if (schema.maxLength !== undefined) {
68
- expression += `.max(${schema.maxLength})`;
84
+ expression += check('max', [String(schema.maxLength)], schema, 'maxLength');
69
85
  }
70
86
  if (schema.format === 'uuid') {
71
- expression += '.uuid()';
87
+ expression += check('uuid', [], schema, 'format');
72
88
  }
73
89
  else if (schema.format === 'email') {
74
- expression += '.email()';
90
+ expression += check('email', [], schema, 'format');
75
91
  }
76
92
  else if (schema.format === 'uri') {
77
- expression += '.url()';
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 += `.regex(new RegExp(${JSON.stringify(schema.pattern)}))`;
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 += `.gte(${schema.minimum})`;
108
+ expression += check('gte', [String(schema.minimum)], schema, 'minimum');
93
109
  }
94
110
  if (schema.exclusiveMinimum !== undefined) {
95
- expression += `.gt(${schema.exclusiveMinimum})`;
111
+ expression += check('gt', [String(schema.exclusiveMinimum)], schema, 'exclusiveMinimum');
96
112
  }
97
113
  if (schema.maximum !== undefined) {
98
- expression += `.lte(${schema.maximum})`;
114
+ expression += check('lte', [String(schema.maximum)], schema, 'maximum');
99
115
  }
100
116
  if (schema.exclusiveMaximum !== undefined) {
101
- expression += `.lt(${schema.exclusiveMaximum})`;
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 += `.min(${schema.minItems})`;
124
+ result += check('min', [String(schema.minItems)], schema, 'minItems');
109
125
  }
110
126
  if (schema.maxItems !== undefined) {
111
- result += `.max(${schema.maxItems})`;
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.17",
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",