@webda/grpc 4.0.0-beta.3

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/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { GrpcService, GrpcServiceParameters } from "./grpcservice.service.js";
2
+ export { GrpcStream, GrpcStatus } from "./grpc-stream.js";
3
+ export type { GrpcMethodDef } from "./grpc-stream.js";
4
+ export { generateProto } from "./proto-generator.js";
@@ -0,0 +1,696 @@
1
+ import { suite, test } from "@webda/test";
2
+ import * as assert from "assert";
3
+ import { generateProto } from "./proto-generator.js";
4
+ import type { JSONSchema7 } from "json-schema";
5
+
6
+ @suite
7
+ class ProtoGeneratorTest {
8
+ @test
9
+ emptyOperations() {
10
+ const result = generateProto({}, {});
11
+ assert.ok(result.includes('syntax = "proto3"'));
12
+ assert.ok(result.includes("package webda;"));
13
+ assert.ok(result.includes('import "google/protobuf/empty.proto"'));
14
+ assert.ok(result.includes('import "google/protobuf/struct.proto"'));
15
+ // No services or messages
16
+ assert.ok(!result.includes("service "));
17
+ assert.ok(!result.includes("message "));
18
+ }
19
+
20
+ @test
21
+ customPackageName() {
22
+ const result = generateProto({}, {}, "myapp");
23
+ assert.ok(result.includes("package myapp;"));
24
+ }
25
+
26
+ @test
27
+ defaultPackageName() {
28
+ const result = generateProto({}, {});
29
+ assert.ok(result.includes("package webda;"));
30
+ }
31
+
32
+ @test
33
+ crudOperationsWithInputOutput() {
34
+ const schemas: Record<string, JSONSchema7> = {
35
+ "MyApp/Post": {
36
+ type: "object",
37
+ properties: {
38
+ title: { type: "string" },
39
+ body: { type: "string" }
40
+ }
41
+ },
42
+ "MyApp/PostResponse": {
43
+ type: "object",
44
+ properties: {
45
+ id: { type: "string" },
46
+ title: { type: "string" }
47
+ }
48
+ }
49
+ };
50
+
51
+ const operations = {
52
+ "Post.Create": {
53
+ input: "MyApp/Post",
54
+ output: "MyApp/PostResponse"
55
+ },
56
+ "Post.Get": {
57
+ output: "MyApp/PostResponse"
58
+ }
59
+ };
60
+
61
+ const result = generateProto(operations, schemas);
62
+
63
+ // Should create PostService
64
+ assert.ok(result.includes("service PostService {"), "Should group into PostService");
65
+ // Should create Post and PostResponse messages
66
+ assert.ok(result.includes("message Post {"), "Should create Post message");
67
+ assert.ok(result.includes("message PostResponse {"), "Should create PostResponse message");
68
+ // Should create rpcs
69
+ assert.ok(result.includes("rpc Create(Post) returns (PostResponse);"), "Should create Create rpc");
70
+ assert.ok(result.includes("rpc Get(google.protobuf.Empty) returns (PostResponse);"), "Should create Get rpc with Empty input");
71
+ }
72
+
73
+ @test
74
+ hiddenOperationsAreSkipped() {
75
+ const operations = {
76
+ "Post.Create": {
77
+ input: "MyApp/Post",
78
+ hidden: true
79
+ },
80
+ "Post.Get": {
81
+ output: "MyApp/PostResponse"
82
+ }
83
+ };
84
+ const schemas: Record<string, JSONSchema7> = {
85
+ "MyApp/PostResponse": {
86
+ type: "object",
87
+ properties: {
88
+ id: { type: "string" }
89
+ }
90
+ }
91
+ };
92
+
93
+ const result = generateProto(operations, schemas);
94
+
95
+ // Hidden operations should not appear
96
+ assert.ok(!result.includes("rpc Create"), "Hidden operation Create should not appear");
97
+ // But visible ones should
98
+ assert.ok(result.includes("rpc Get"), "Visible operation Get should appear");
99
+ }
100
+
101
+ @test
102
+ serviceGroupingByPrefix() {
103
+ const operations = {
104
+ "Post.Create": {},
105
+ "Post.Get": {},
106
+ "User.Login": {},
107
+ "User.Logout": {}
108
+ };
109
+
110
+ const result = generateProto(operations, {});
111
+
112
+ assert.ok(result.includes("service PostService {"), "Should create PostService");
113
+ assert.ok(result.includes("service UserService {"), "Should create UserService");
114
+ assert.ok(result.includes("rpc Create(google.protobuf.Empty)"), "Should create Create rpc in PostService");
115
+ assert.ok(result.includes("rpc Get(google.protobuf.Empty)"), "Should create Get rpc in PostService");
116
+ assert.ok(result.includes("rpc Login(google.protobuf.Empty)"), "Should create Login rpc in UserService");
117
+ assert.ok(result.includes("rpc Logout(google.protobuf.Empty)"), "Should create Logout rpc in UserService");
118
+ }
119
+
120
+ @test
121
+ operationWithoutDotGoesToDefaultService() {
122
+ const operations = {
123
+ Health: {}
124
+ };
125
+
126
+ const result = generateProto(operations, {});
127
+ assert.ok(result.includes("service DefaultService {"), "Should create DefaultService for operations without a dot prefix");
128
+ assert.ok(result.includes("rpc Health(google.protobuf.Empty)"), "Should use opId as rpcName");
129
+ }
130
+
131
+ @test
132
+ emptyInputOutputUsesGoogleProtobufEmpty() {
133
+ const operations = {
134
+ "Post.List": {}
135
+ };
136
+
137
+ const result = generateProto(operations, {});
138
+ assert.ok(
139
+ result.includes("rpc List(google.protobuf.Empty) returns (google.protobuf.Empty);"),
140
+ "Should use google.protobuf.Empty for both input and output when none defined"
141
+ );
142
+ }
143
+
144
+ @test
145
+ streamingHints() {
146
+ const operations = {
147
+ "Stream.ServerPush": {
148
+ grpc: { streaming: "server" }
149
+ },
150
+ "Stream.ClientPush": {
151
+ grpc: { streaming: "client" }
152
+ },
153
+ "Stream.Bidi": {
154
+ grpc: { streaming: "bidi" }
155
+ },
156
+ "Stream.Unary": {}
157
+ };
158
+
159
+ const result = generateProto(operations, {});
160
+
161
+ assert.ok(
162
+ result.includes("rpc ServerPush(google.protobuf.Empty) returns (stream google.protobuf.Empty);"),
163
+ "Server streaming should have 'stream' on output"
164
+ );
165
+ assert.ok(
166
+ result.includes("rpc ClientPush(stream google.protobuf.Empty) returns (google.protobuf.Empty);"),
167
+ "Client streaming should have 'stream' on input"
168
+ );
169
+ assert.ok(
170
+ result.includes("rpc Bidi(stream google.protobuf.Empty) returns (stream google.protobuf.Empty);"),
171
+ "Bidi streaming should have 'stream' on both"
172
+ );
173
+ assert.ok(
174
+ result.includes("rpc Unary(google.protobuf.Empty) returns (google.protobuf.Empty);"),
175
+ "Unary should have no 'stream' keyword"
176
+ );
177
+ }
178
+
179
+ @test
180
+ messageGenerationFromJsonSchema() {
181
+ const schemas: Record<string, JSONSchema7> = {
182
+ "MyApp/Item": {
183
+ type: "object",
184
+ properties: {
185
+ name: { type: "string" },
186
+ count: { type: "number" },
187
+ price: { type: "number", format: "float" },
188
+ quantity: { type: "number", format: "int32" },
189
+ bigNum: { type: "number", format: "int64" },
190
+ active: { type: "boolean" },
191
+ rank: { type: "integer" },
192
+ bigRank: { type: "integer", format: "int64" }
193
+ }
194
+ }
195
+ };
196
+
197
+ const operations = {
198
+ "Item.Get": {
199
+ output: "MyApp/Item"
200
+ }
201
+ };
202
+
203
+ const result = generateProto(operations, schemas);
204
+
205
+ assert.ok(result.includes("message Item {"), "Should create Item message");
206
+ assert.ok(result.includes("string name = 1;"), "String field");
207
+ assert.ok(result.includes("double count = 2;"), "Number field defaults to double");
208
+ assert.ok(result.includes("float price = 3;"), "Float format");
209
+ assert.ok(result.includes("int32 quantity = 4;"), "Int32 format for number");
210
+ assert.ok(result.includes("int64 bigNum = 5;"), "Int64 format for number");
211
+ assert.ok(result.includes("bool active = 6;"), "Boolean field");
212
+ assert.ok(result.includes("int32 rank = 7;"), "Integer type defaults to int32");
213
+ assert.ok(result.includes("int64 bigRank = 8;"), "Integer with int64 format");
214
+ }
215
+
216
+ @test
217
+ stringFormats() {
218
+ const schemas: Record<string, JSONSchema7> = {
219
+ "MyApp/Dates": {
220
+ type: "object",
221
+ properties: {
222
+ created: { type: "string", format: "date-time" },
223
+ birthday: { type: "string", format: "date" },
224
+ bigId: { type: "string", format: "int64" },
225
+ plain: { type: "string" }
226
+ }
227
+ }
228
+ };
229
+
230
+ const operations = {
231
+ "Date.Get": { output: "MyApp/Dates" }
232
+ };
233
+
234
+ const result = generateProto(operations, schemas);
235
+
236
+ assert.ok(result.includes("string created = 1;"), "date-time maps to string");
237
+ assert.ok(result.includes("string birthday = 2;"), "date maps to string");
238
+ assert.ok(result.includes("int64 bigId = 3;"), "string with int64 format maps to int64");
239
+ assert.ok(result.includes("string plain = 4;"), "plain string");
240
+ }
241
+
242
+ @test
243
+ nestedObjectMessage() {
244
+ const schemas: Record<string, JSONSchema7> = {
245
+ "MyApp/Container": {
246
+ type: "object",
247
+ properties: {
248
+ label: { type: "string" },
249
+ metadata: {
250
+ type: "object",
251
+ properties: {
252
+ key: { type: "string" },
253
+ value: { type: "string" }
254
+ }
255
+ }
256
+ }
257
+ }
258
+ };
259
+
260
+ const operations = {
261
+ "Container.Get": { output: "MyApp/Container" }
262
+ };
263
+
264
+ const result = generateProto(operations, schemas);
265
+
266
+ assert.ok(result.includes("message Container {"), "Should create Container message");
267
+ assert.ok(result.includes("message ContainerMetadata {"), "Should create nested ContainerMetadata message");
268
+ assert.ok(result.includes("ContainerMetadata metadata = 2;"), "Should reference nested message type");
269
+ assert.ok(result.includes("string key = 1;"), "Nested message should have fields");
270
+ assert.ok(result.includes("string value = 2;"), "Nested message should have fields");
271
+ }
272
+
273
+ @test
274
+ objectWithoutPropertiesUsesStruct() {
275
+ const schemas: Record<string, JSONSchema7> = {
276
+ "MyApp/Dynamic": {
277
+ type: "object",
278
+ properties: {
279
+ data: { type: "object" } // no properties => Struct
280
+ }
281
+ }
282
+ };
283
+
284
+ const operations = {
285
+ "Dynamic.Get": { output: "MyApp/Dynamic" }
286
+ };
287
+
288
+ const result = generateProto(operations, schemas);
289
+
290
+ assert.ok(result.includes("google.protobuf.Struct data = 1;"), "Object without properties maps to Struct");
291
+ }
292
+
293
+ @test
294
+ arrayField() {
295
+ const schemas: Record<string, JSONSchema7> = {
296
+ "MyApp/List": {
297
+ type: "object",
298
+ properties: {
299
+ items: {
300
+ type: "array",
301
+ items: { type: "string" }
302
+ },
303
+ numbers: {
304
+ type: "array",
305
+ items: { type: "integer" }
306
+ }
307
+ }
308
+ }
309
+ };
310
+
311
+ const operations = {
312
+ "List.Get": { output: "MyApp/List" }
313
+ };
314
+
315
+ const result = generateProto(operations, schemas);
316
+
317
+ assert.ok(result.includes("repeated string items = 1;"), "Array of strings should be repeated string");
318
+ assert.ok(result.includes("repeated int32 numbers = 2;"), "Array of integers should be repeated int32");
319
+ }
320
+
321
+ @test
322
+ refField() {
323
+ const schemas: Record<string, JSONSchema7> = {
324
+ "MyApp/Order": {
325
+ type: "object",
326
+ properties: {
327
+ item: { $ref: "#/definitions/MyApp/Item" }
328
+ }
329
+ },
330
+ "MyApp/Item": {
331
+ type: "object",
332
+ properties: {
333
+ name: { type: "string" }
334
+ }
335
+ }
336
+ };
337
+
338
+ const operations = {
339
+ "Order.Get": { output: "MyApp/Order" }
340
+ };
341
+
342
+ const result = generateProto(operations, schemas);
343
+
344
+ assert.ok(result.includes("Item item = 1;"), "Ref should resolve to message name");
345
+ }
346
+
347
+ @test
348
+ componentsSchemaRef() {
349
+ const schemas: Record<string, JSONSchema7> = {
350
+ "MyApp/Widget": {
351
+ type: "object",
352
+ properties: {
353
+ part: { $ref: "#/components/schemas/MyApp/Part" }
354
+ }
355
+ }
356
+ };
357
+
358
+ const operations = {
359
+ "Widget.Get": { output: "MyApp/Widget" }
360
+ };
361
+
362
+ const result = generateProto(operations, schemas);
363
+
364
+ assert.ok(result.includes("Part part = 1;"), "Components schema ref should resolve");
365
+ }
366
+
367
+ @test
368
+ operationWithSummary() {
369
+ const operations = {
370
+ "Post.Create": {
371
+ summary: "Create a new post"
372
+ }
373
+ };
374
+
375
+ const result = generateProto(operations, {});
376
+
377
+ assert.ok(result.includes("// Create a new post"), "Summary should appear as comment before rpc");
378
+ }
379
+
380
+ @test
381
+ operationWithInputSchema() {
382
+ const schemas: Record<string, JSONSchema7> = {
383
+ uuidRequest: {
384
+ type: "object",
385
+ properties: {
386
+ uuid: { type: "string" }
387
+ }
388
+ }
389
+ };
390
+
391
+ const operations = {
392
+ "Post.Get": {
393
+ input: "uuidRequest",
394
+ output: "MyApp/PostResponse"
395
+ }
396
+ };
397
+
398
+ const schemasWithOutput: Record<string, JSONSchema7> = {
399
+ ...schemas,
400
+ "MyApp/PostResponse": {
401
+ type: "object",
402
+ properties: {
403
+ id: { type: "string" }
404
+ }
405
+ }
406
+ };
407
+
408
+ const result = generateProto(operations, schemasWithOutput);
409
+
410
+ // Input schema should become the input type
411
+ assert.ok(result.includes("message UuidRequest {"), "Should create message from input schema");
412
+ assert.ok(result.includes("rpc Get(UuidRequest) returns (PostResponse);"), "Should use input as input type");
413
+ }
414
+
415
+ @test
416
+ inputUsedAsRpcInputType() {
417
+ const schemas: Record<string, JSONSchema7> = {
418
+ "MyApp/CreateInput": {
419
+ type: "object",
420
+ properties: { title: { type: "string" } }
421
+ }
422
+ };
423
+
424
+ const operations = {
425
+ "Post.Create": {
426
+ input: "MyApp/CreateInput"
427
+ }
428
+ };
429
+
430
+ const result = generateProto(operations, schemas);
431
+
432
+ // input should be used as the RPC input type
433
+ assert.ok(result.includes("rpc Create(CreateInput)"), "Input should be used as RPC input type");
434
+ }
435
+
436
+ @test
437
+ schemaToMessageNameHandlesSlashAndDotNotation() {
438
+ const schemas: Record<string, JSONSchema7> = {
439
+ "WebdaSample/Post": {
440
+ type: "object",
441
+ properties: { title: { type: "string" } }
442
+ },
443
+ "my.nested.schema": {
444
+ type: "object",
445
+ properties: { value: { type: "string" } }
446
+ }
447
+ };
448
+
449
+ const operations = {
450
+ "Post.Get": { output: "WebdaSample/Post" },
451
+ "Nested.Get": { output: "my.nested.schema" }
452
+ };
453
+
454
+ const result = generateProto(operations, schemas);
455
+
456
+ // "WebdaSample/Post" → "Post"
457
+ assert.ok(result.includes("message Post {"), "Slash-separated should use last segment");
458
+ // "my.nested.schema" → "MyNestedSchema"
459
+ assert.ok(result.includes("message MyNestedSchema {"), "Dot-separated should be PascalCased and joined");
460
+ }
461
+
462
+ @test
463
+ unknownTypeDefaultsToStruct() {
464
+ const schemas: Record<string, JSONSchema7> = {
465
+ "MyApp/Mystery": {
466
+ type: "object",
467
+ properties: {
468
+ unknownField: {} // no type at all
469
+ }
470
+ }
471
+ };
472
+
473
+ const operations = {
474
+ "Mystery.Get": { output: "MyApp/Mystery" }
475
+ };
476
+
477
+ const result = generateProto(operations, schemas);
478
+
479
+ assert.ok(result.includes("google.protobuf.Struct unknownField = 1;"), "Unknown type should default to Struct");
480
+ }
481
+
482
+ @test
483
+ arrayWithoutItemsDefaultsToStruct() {
484
+ const schemas: Record<string, JSONSchema7> = {
485
+ "MyApp/Flexible": {
486
+ type: "object",
487
+ properties: {
488
+ tags: {
489
+ type: "array"
490
+ // no items defined
491
+ }
492
+ }
493
+ }
494
+ };
495
+
496
+ const operations = {
497
+ "Flexible.Get": { output: "MyApp/Flexible" }
498
+ };
499
+
500
+ const result = generateProto(operations, schemas);
501
+
502
+ // Array without items should use Struct as the element type
503
+ assert.ok(result.includes("repeated google.protobuf.Struct tags = 1;"), "Array without items should repeat Struct");
504
+ }
505
+
506
+ @test
507
+ numberIntegerFormat() {
508
+ const schemas: Record<string, JSONSchema7> = {
509
+ "MyApp/NumTypes": {
510
+ type: "object",
511
+ properties: {
512
+ intField: { type: "number", format: "integer" }
513
+ }
514
+ }
515
+ };
516
+
517
+ const operations = {
518
+ "NumTypes.Get": { output: "MyApp/NumTypes" }
519
+ };
520
+
521
+ const result = generateProto(operations, schemas);
522
+
523
+ assert.ok(result.includes("int32 intField = 1;"), "Number with 'integer' format maps to int32");
524
+ }
525
+
526
+ @test
527
+ arrayTypeInJsonTypeToProtoRecursion() {
528
+ const schemas: Record<string, JSONSchema7> = {
529
+ "MyApp/Nested": {
530
+ type: "object",
531
+ properties: {
532
+ matrix: {
533
+ type: "array",
534
+ items: {
535
+ type: "array",
536
+ items: { type: "integer" }
537
+ }
538
+ }
539
+ }
540
+ }
541
+ };
542
+
543
+ const operations = {
544
+ "Nested.Get": { output: "MyApp/Nested" }
545
+ };
546
+
547
+ // This exercises the "array" case within jsonTypeToProto
548
+ const result = generateProto(operations, schemas);
549
+ // The outer array items is an array of integers
550
+ // jsonTypeToProto for "array" type recurses into items
551
+ assert.ok(result.includes("repeated int32 matrix = 1;"), "Nested array should recurse into items");
552
+ }
553
+
554
+ @test
555
+ operationWithInputSchemaNotInSchemas() {
556
+ const operations = {
557
+ "Post.Create": {
558
+ input: "MyApp/NonExistent"
559
+ }
560
+ };
561
+
562
+ const result = generateProto(operations, {});
563
+
564
+ // If schema not found, should use Empty
565
+ assert.ok(result.includes("rpc Create(google.protobuf.Empty)"), "Missing schema should fall back to Empty");
566
+ }
567
+
568
+ @test
569
+ operationWithOutputSchemaNotInSchemas() {
570
+ const operations = {
571
+ "Post.Get": {
572
+ output: "MyApp/NonExistent"
573
+ }
574
+ };
575
+
576
+ const result = generateProto(operations, {});
577
+
578
+ assert.ok(result.includes("returns (google.protobuf.Empty)"), "Missing output schema should fall back to Empty");
579
+ }
580
+
581
+ /**
582
+ * `schemaToMessageName` strips generic-arg brackets (`<...>`) so a
583
+ * type like `BinaryFileInfo<{}>` lands as the bare `BinaryFileInfo`
584
+ * proto identifier. Without this strip step the URL-encoded form
585
+ * (`BinaryFileInfo%3C%7B%7D%3E`) leaked into the proto output and
586
+ * proto-loader rejected it as an illegal token.
587
+ */
588
+ @test
589
+ refWithGenericBracketsStripsThemFromMessageName() {
590
+ const schemas: Record<string, JSONSchema7> = {
591
+ Wrap: {
592
+ type: "object",
593
+ properties: {
594
+ payload: { $ref: "#/definitions/BinaryFileInfo%3C%7B%7D%3E" }
595
+ }
596
+ },
597
+ "BinaryFileInfo<{}>": {
598
+ type: "object",
599
+ properties: { hash: { type: "string" } }
600
+ }
601
+ };
602
+ const operations = { "Wrap.Get": { output: "Wrap" } };
603
+
604
+ const result = generateProto(operations, schemas);
605
+ assert.ok(result.includes("BinaryFileInfo payload = 1;"), "field type drops the bracket noise");
606
+ assert.ok(result.includes("message BinaryFileInfo {"), "message body is emitted under the cleaned name");
607
+ }
608
+
609
+ /**
610
+ * A `#/definitions/<name>` ref whose target schema is missing falls
611
+ * back to `google.protobuf.Struct` so proto-loader gets a valid
612
+ * identifier instead of a dangling reference. Component-schema refs
613
+ * keep their original behaviour and emit the bare name (verified in
614
+ * `componentsSchemaRef` above).
615
+ */
616
+ @test
617
+ unresolvedDefinitionsRefFallsBackToStruct() {
618
+ const schemas: Record<string, JSONSchema7> = {
619
+ Outer: {
620
+ type: "object",
621
+ properties: {
622
+ inner: { $ref: "#/definitions/__never__" }
623
+ }
624
+ }
625
+ };
626
+ const operations = { "Outer.Get": { output: "Outer" } };
627
+
628
+ const result = generateProto(operations, schemas);
629
+ assert.ok(
630
+ result.includes("google.protobuf.Struct inner = 1;"),
631
+ "unresolved #/definitions ref must fall back to Struct"
632
+ );
633
+ }
634
+
635
+ /**
636
+ * Operation schemas that carry their own `definitions` block (e.g.
637
+ * an output schema generated by the compiler with hoisted
638
+ * definitions) get those folded into the flat schema registry
639
+ * before the proto walker resolves refs. Without this the
640
+ * `BinaryMap<T>` ref inside a method's output schema would fall
641
+ * back to `google.protobuf.Struct` even though the definition is
642
+ * sitting next to it in the same schema.
643
+ */
644
+ @test
645
+ operationSchemaLocalDefinitionsAreFoldedIntoRegistry() {
646
+ const schemas: Record<string, JSONSchema7> = {
647
+ "Wrap.Output": {
648
+ type: "object",
649
+ properties: {
650
+ map: { $ref: "#/definitions/BinaryMap" }
651
+ },
652
+ definitions: {
653
+ BinaryMap: {
654
+ type: "object",
655
+ properties: { hash: { type: "string" } }
656
+ }
657
+ }
658
+ }
659
+ };
660
+ const operations = { "Wrap.Get": { output: "Wrap.Output" } };
661
+
662
+ const result = generateProto(operations, schemas);
663
+ assert.ok(
664
+ result.includes("BinaryMap map = 1;"),
665
+ "ref to a local definition resolves to a real message instead of Struct"
666
+ );
667
+ assert.ok(result.includes("message BinaryMap {"), "the local definition is emitted as a top-level message");
668
+ }
669
+
670
+ /**
671
+ * A sanitized message name that would otherwise begin with a digit
672
+ * (e.g. `&1$metadata` after the strip becomes `1Metadata`) gets a
673
+ * leading underscore so it satisfies proto3's
674
+ * `[A-Za-z_][A-Za-z0-9_]*` identifier rule.
675
+ */
676
+ @test
677
+ sanitizedMessageNameNeverStartsWithDigit() {
678
+ const schemas: Record<string, JSONSchema7> = {
679
+ Container: {
680
+ type: "object",
681
+ properties: {
682
+ value: { $ref: "#/definitions/&1$metadata" }
683
+ }
684
+ },
685
+ "&1$metadata": {
686
+ type: "object"
687
+ }
688
+ };
689
+ const operations = { "Container.Get": { output: "Container" } };
690
+
691
+ const result = generateProto(operations, schemas);
692
+ // The cleaned name is `1Metadata`; we expect `_1Metadata` after the
693
+ // leading-letter guard.
694
+ assert.ok(result.includes("_1Metadata value = 1;"), "leading-digit sanitized name is prefixed with `_`");
695
+ }
696
+ }