@swimmesberger/elarion-jsonrpc-client-generator 0.2.5-preview.99.1 → 0.2.5
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.
|
@@ -296,7 +296,8 @@ function createTopicTreeNode() {
|
|
|
296
296
|
function emitProperties(node, indent) {
|
|
297
297
|
const pad = ' '.repeat(indent);
|
|
298
298
|
return Array.from(node.children.entries())
|
|
299
|
-
|
|
299
|
+
// Code-unit comparison, never localeCompare: output must be byte-identical regardless of host locale.
|
|
300
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
300
301
|
.map(([segment, child]) => `${pad}readonly ${JSON.stringify(segment)}: ${emitNodeType(child, indent)}`);
|
|
301
302
|
}
|
|
302
303
|
function emitNodeType(node, indent) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { baseType, childContext, formatPropertyName, isNullable, resolveSchema,
|
|
1
|
+
import { baseType, childContext, formatPropertyName, isNullable, resolveSchema, } from './json-schema.js';
|
|
2
2
|
export function jsonSchemaToTypeScript(schema, ctx, indent = 0) {
|
|
3
|
-
const resolved = resolveSchema(schema, ctx);
|
|
3
|
+
const { schema: resolved, ctx: resolvedCtx } = resolveSchema(schema, ctx);
|
|
4
4
|
const pad = ' '.repeat(indent);
|
|
5
5
|
const nullable = isNullable(resolved);
|
|
6
6
|
const base = baseType(resolved);
|
|
@@ -26,14 +26,17 @@ export function jsonSchemaToTypeScript(schema, ctx, indent = 0) {
|
|
|
26
26
|
return nullable ? 'boolean | null | undefined' : 'boolean';
|
|
27
27
|
}
|
|
28
28
|
if (base === 'array' && resolved.items) {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// Unstripped items: the recursion emits the item's own nullability (`string | null | undefined`).
|
|
30
|
+
const itemType = jsonSchemaToTypeScript(resolved.items, childContext(resolvedCtx, 'items'), indent);
|
|
31
|
+
// A union item type must be parenthesized — `"red" | "green"[]` is `"red" | ("green"[])`.
|
|
32
|
+
const element = itemType.includes('|') ? `(${itemType})` : itemType;
|
|
33
|
+
return nullable ? `${element}[] | null | undefined` : `${element}[]`;
|
|
31
34
|
}
|
|
32
35
|
if (base === 'object' && resolved.properties) {
|
|
33
36
|
const required = new Set(resolved.required ?? []);
|
|
34
37
|
const lines = Object.entries(resolved.properties).map(([key, property]) => {
|
|
35
38
|
const optional = required.has(key) ? '' : '?';
|
|
36
|
-
const propertyType = jsonSchemaToTypeScript(property, childContext(
|
|
39
|
+
const propertyType = jsonSchemaToTypeScript(property, childContext(resolvedCtx, `properties.${key}`), indent + 1);
|
|
37
40
|
return `${pad} ${formatPropertyName(key)}${optional}: ${propertyType}`;
|
|
38
41
|
});
|
|
39
42
|
const objectType = `{\n${lines.join('\n')}\n${pad}}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { baseType, childContext, formatPropertyName, isNullable, resolveSchema,
|
|
1
|
+
import { baseType, childContext, formatPropertyName, isNullable, resolveSchema, } from './json-schema.js';
|
|
2
2
|
export function jsonSchemaToZod(schema, ctx, indent = 0) {
|
|
3
|
-
const resolved = resolveSchema(schema, ctx);
|
|
3
|
+
const { schema: resolved, ctx: resolvedCtx } = resolveSchema(schema, ctx);
|
|
4
4
|
const nullable = isNullable(resolved);
|
|
5
5
|
const base = baseType(resolved);
|
|
6
6
|
// Schemas validate the user-facing shape: a file field is a native File on both sides of a call (the
|
|
@@ -33,14 +33,17 @@ export function jsonSchemaToZod(schema, ctx, indent = 0) {
|
|
|
33
33
|
return nullish('z.boolean()', nullable);
|
|
34
34
|
}
|
|
35
35
|
if (base === 'array' && resolved.items) {
|
|
36
|
-
|
|
36
|
+
// The item schema goes in unstripped: the recursion handles item-level nullability, so a
|
|
37
|
+
// `["string","null"]` item emits `z.string().nullish()` and a legal `["a", null]` element validates.
|
|
38
|
+
// Nullability is only ever stripped at the top-level params/result envelope.
|
|
39
|
+
const itemSchema = jsonSchemaToZod(resolved.items, childContext(resolvedCtx, 'items'), indent);
|
|
37
40
|
return nullish(arraySchema(`z.array(${itemSchema})`, resolved), nullable);
|
|
38
41
|
}
|
|
39
42
|
if (base === 'object' && resolved.properties) {
|
|
40
43
|
const required = new Set(resolved.required ?? []);
|
|
41
44
|
const pad = ' '.repeat(indent + 1);
|
|
42
45
|
const fields = Object.entries(resolved.properties).map(([key, property]) => {
|
|
43
|
-
let fieldSchema = jsonSchemaToZod(property, childContext(
|
|
46
|
+
let fieldSchema = jsonSchemaToZod(property, childContext(resolvedCtx, `properties.${key}`), indent + 1);
|
|
44
47
|
if (!required.has(key)) {
|
|
45
48
|
fieldSchema += '.optional()';
|
|
46
49
|
}
|
package/dist/json-schema.d.ts
CHANGED
|
@@ -11,7 +11,17 @@ export interface SchemaContext {
|
|
|
11
11
|
export declare function isNullable(schema: JsonSchema): boolean;
|
|
12
12
|
export declare function baseType(schema: JsonSchema): string | undefined;
|
|
13
13
|
export declare function stripNullable(schema: JsonSchema): JsonSchema;
|
|
14
|
-
export
|
|
14
|
+
export interface ResolvedSchema {
|
|
15
|
+
schema: JsonSchema;
|
|
16
|
+
/**
|
|
17
|
+
* The context after resolution, carrying every `$ref` traversed to reach the schema. Recursion into
|
|
18
|
+
* properties/items must continue from this context (not the caller's), so a cycle that re-enters one of
|
|
19
|
+
* those refs through a property (the shape STJ emits for self-referential DTOs) is detected instead of
|
|
20
|
+
* overflowing the stack.
|
|
21
|
+
*/
|
|
22
|
+
ctx: SchemaContext;
|
|
23
|
+
}
|
|
24
|
+
export declare function resolveSchema(schema: JsonSchema, ctx: SchemaContext): ResolvedSchema;
|
|
15
25
|
export declare function childContext(ctx: SchemaContext, segment: string): SchemaContext;
|
|
16
26
|
export declare function formatPropertyName(key: string): string;
|
|
17
27
|
/**
|
package/dist/json-schema.js
CHANGED
|
@@ -35,7 +35,7 @@ export function stripNullable(schema) {
|
|
|
35
35
|
export function resolveSchema(schema, ctx) {
|
|
36
36
|
assertSupportedComposition(schema, ctx.path);
|
|
37
37
|
if (!schema.$ref) {
|
|
38
|
-
return schema;
|
|
38
|
+
return { schema, ctx };
|
|
39
39
|
}
|
|
40
40
|
if (!schema.$ref.startsWith('#/')) {
|
|
41
41
|
throw new UnsupportedJsonSchemaError(ctx.path, `only local JSON Pointer $ref values are supported (${schema.$ref})`);
|
|
@@ -75,17 +75,17 @@ export function collectFilePaths(schema, ctx) {
|
|
|
75
75
|
return paths;
|
|
76
76
|
}
|
|
77
77
|
function visitFileNodes(schema, ctx, prefix, paths) {
|
|
78
|
-
const resolved = resolveSchema(schema, ctx);
|
|
78
|
+
const { schema: resolved, ctx: resolvedCtx } = resolveSchema(schema, ctx);
|
|
79
79
|
if (resolved['x-elarion-file'] === true) {
|
|
80
80
|
paths.push(prefix);
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
83
|
if (resolved.items) {
|
|
84
|
-
visitFileNodes(stripNullable(resolved.items), childContext(
|
|
84
|
+
visitFileNodes(stripNullable(resolved.items), childContext(resolvedCtx, 'items'), [...prefix, '*'], paths);
|
|
85
85
|
}
|
|
86
86
|
if (resolved.properties) {
|
|
87
87
|
for (const [key, property] of Object.entries(resolved.properties)) {
|
|
88
|
-
visitFileNodes(property, childContext(
|
|
88
|
+
visitFileNodes(property, childContext(resolvedCtx, `properties.${key}`), [...prefix, key], paths);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -326,7 +326,8 @@ export function generateRpcClientSource(options) {
|
|
|
326
326
|
' }',
|
|
327
327
|
'',
|
|
328
328
|
' function parseResult<M extends RpcMethod>(method: M, result: unknown): RpcResult<M> {',
|
|
329
|
-
'
|
|
329
|
+
' // A transform returning null/undefined is a legitimate result — never ??-fallback to the raw value.',
|
|
330
|
+
' const transformed = options.transformResult ? options.transformResult(method, result) : result',
|
|
330
331
|
' if (!validateResults) {',
|
|
331
332
|
' return transformed as RpcResult<M>',
|
|
332
333
|
' }',
|
|
@@ -351,6 +352,12 @@ export function generateRpcClientSource(options) {
|
|
|
351
352
|
' try {',
|
|
352
353
|
' const raw = await post(request, context, requestOptions, span)',
|
|
353
354
|
' const response = parseResponseEnvelope(raw)',
|
|
355
|
+
' // A spec-compliant server reports request-level failures (e.g. parse errors) with "id": null —',
|
|
356
|
+
' // surface the server error instead of a generic id-mismatch protocol error.',
|
|
357
|
+
' if (response.error && (response.id === null || response.id === undefined)) {',
|
|
358
|
+
' throw toRpcError(response.error)',
|
|
359
|
+
' }',
|
|
360
|
+
'',
|
|
354
361
|
' if (!idsEqual(response.id, request.id)) {',
|
|
355
362
|
" throw new RpcProtocolError('JSON-RPC response id does not match request id.')",
|
|
356
363
|
' }',
|
|
@@ -412,6 +419,11 @@ export function generateRpcClientSource(options) {
|
|
|
412
419
|
' const raw = await post(requests, context, requestOptions, span)',
|
|
413
420
|
'',
|
|
414
421
|
' if (!Array.isArray(raw)) {',
|
|
422
|
+
' // JSON-RPC 2.0: a whole-batch failure (invalid JSON, batch too large, ...) is a single error',
|
|
423
|
+
' // response object — surface the server error instead of a generic protocol error.',
|
|
424
|
+
' if (isRecord(raw) && isJsonRpcErrorObject(raw.error)) {',
|
|
425
|
+
' throw toRpcError(raw.error)',
|
|
426
|
+
' }',
|
|
415
427
|
" throw new RpcProtocolError('Expected JSON-RPC batch response array.')",
|
|
416
428
|
' }',
|
|
417
429
|
'',
|
|
@@ -652,7 +664,8 @@ function generateApiTypeLines(methodTree) {
|
|
|
652
664
|
function emitProperties(node, endpointType, indent) {
|
|
653
665
|
const pad = ' '.repeat(indent);
|
|
654
666
|
return Array.from(node.children.entries())
|
|
655
|
-
|
|
667
|
+
// Code-unit comparison, never localeCompare: output must be byte-identical regardless of host locale.
|
|
668
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
656
669
|
.map(([segment, child]) => {
|
|
657
670
|
return `${pad}readonly ${JSON.stringify(segment)}: ${emitNodeType(child, endpointType, indent)}`;
|
|
658
671
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swimmesberger/elarion-jsonrpc-client-generator",
|
|
3
|
-
"version": "0.2.5
|
|
3
|
+
"version": "0.2.5",
|
|
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",
|