@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/.webda/cache +4 -0
- package/CHANGELOG.md +54 -0
- package/LICENSE +166 -0
- package/README.md +100 -0
- package/lib/grpc-stream.d.ts +109 -0
- package/lib/grpc-stream.d.ts.map +1 -0
- package/lib/grpc-stream.js +162 -0
- package/lib/grpc-stream.js.map +1 -0
- package/lib/grpcservice.service.d.ts +112 -0
- package/lib/grpcservice.service.d.ts.map +1 -0
- package/lib/grpcservice.service.js +459 -0
- package/lib/grpcservice.service.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -0
- package/lib/proto-generator.d.ts +14 -0
- package/lib/proto-generator.d.ts.map +1 -0
- package/lib/proto-generator.js +312 -0
- package/lib/proto-generator.js.map +1 -0
- package/package.json +42 -0
- package/src/build-integration.spec.ts +187 -0
- package/src/grpc-stream.spec.ts +417 -0
- package/src/grpc-stream.ts +192 -0
- package/src/grpcservice.service.ts +462 -0
- package/src/grpcservice.spec.ts +1092 -0
- package/src/index.ts +4 -0
- package/src/proto-generator.spec.ts +696 -0
- package/src/proto-generator.ts +338 -0
- package/tsconfig.json +33 -0
- package/vitest.config.ts +20 -0
- package/webda.module.json +111 -0
package/src/index.ts
ADDED
|
@@ -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
|
+
}
|