openapi-remote-codegen 0.1.0

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.
@@ -0,0 +1,576 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseOpenApiSpec } from '../parser.js';
3
+ import type { OpenAPIV3 } from 'openapi-types';
4
+
5
+ function createSpec(overrides: Partial<OpenAPIV3.Document> = {}): OpenAPIV3.Document {
6
+ return {
7
+ openapi: '3.0.0',
8
+ info: { title: 'Test', version: '1.0.0' },
9
+ paths: {},
10
+ ...overrides,
11
+ };
12
+ }
13
+
14
+ describe('parseOpenApiSpec', () => {
15
+ it('parses operations with remote annotations', () => {
16
+ const spec = createSpec({
17
+ paths: {
18
+ '/api/v4/trackers': {
19
+ get: {
20
+ tags: ['V4 Trackers'],
21
+ operationId: 'Trackers_GetDefinitions',
22
+ 'x-remote-type': 'query',
23
+ parameters: [],
24
+ responses: {
25
+ '200': {
26
+ description: 'Success',
27
+ content: {
28
+ 'application/json': {
29
+ schema: { type: 'array', items: { $ref: '#/components/schemas/TrackerDto' } },
30
+ },
31
+ },
32
+ },
33
+ },
34
+ } as any,
35
+ },
36
+ },
37
+ });
38
+
39
+ const result = parseOpenApiSpec(spec);
40
+ expect(result.operations).toHaveLength(1);
41
+ expect(result.operations[0].operationId).toBe('Trackers_GetDefinitions');
42
+ expect(result.operations[0].remoteType).toBe('query');
43
+ expect(result.operations[0].tag).toBe('V4 Trackers');
44
+ });
45
+
46
+ it('skips operations without remote annotations', () => {
47
+ const spec = createSpec({
48
+ paths: {
49
+ '/api/v4/trackers': {
50
+ get: {
51
+ tags: ['V4 Trackers'],
52
+ operationId: 'Trackers_GetDefinitions',
53
+ parameters: [],
54
+ responses: { '200': { description: 'OK' } },
55
+ } as any,
56
+ },
57
+ },
58
+ });
59
+
60
+ const result = parseOpenApiSpec(spec);
61
+ expect(result.operations).toHaveLength(0);
62
+ });
63
+
64
+ it('detects enum parameters via $ref through oneOf', () => {
65
+ const spec = createSpec({
66
+ components: {
67
+ schemas: {
68
+ TrackerCategory: {
69
+ type: 'string',
70
+ enum: ['Consumable', 'Reservoir', 'Appointment'],
71
+ } as any,
72
+ },
73
+ },
74
+ paths: {
75
+ '/api/v4/trackers': {
76
+ get: {
77
+ tags: ['V4 Trackers'],
78
+ operationId: 'Trackers_GetDefinitions',
79
+ 'x-remote-type': 'query',
80
+ parameters: [
81
+ {
82
+ name: 'category',
83
+ in: 'query',
84
+ schema: {
85
+ oneOf: [
86
+ { nullable: true, oneOf: [{ $ref: '#/components/schemas/TrackerCategory' }] },
87
+ ],
88
+ },
89
+ },
90
+ ],
91
+ responses: { '200': { description: 'OK' } },
92
+ } as any,
93
+ },
94
+ },
95
+ });
96
+
97
+ const result = parseOpenApiSpec(spec);
98
+ expect(result.operations[0].parameters[0].enumName).toBe('TrackerCategory');
99
+ expect(result.operations[0].parameters[0].type).toBe('string');
100
+ });
101
+
102
+ it('detects enum parameters via direct $ref', () => {
103
+ const spec = createSpec({
104
+ components: {
105
+ schemas: {
106
+ TrackerCategory: {
107
+ type: 'string',
108
+ enum: ['Consumable', 'Reservoir'],
109
+ } as any,
110
+ },
111
+ },
112
+ paths: {
113
+ '/api/v4/trackers': {
114
+ get: {
115
+ tags: ['V4 Trackers'],
116
+ operationId: 'Trackers_GetDefinitions',
117
+ 'x-remote-type': 'query',
118
+ parameters: [
119
+ {
120
+ name: 'category',
121
+ in: 'query',
122
+ schema: { $ref: '#/components/schemas/TrackerCategory' },
123
+ },
124
+ ],
125
+ responses: { '200': { description: 'OK' } },
126
+ } as any,
127
+ },
128
+ },
129
+ });
130
+
131
+ const result = parseOpenApiSpec(spec);
132
+ expect(result.operations[0].parameters[0].enumName).toBe('TrackerCategory');
133
+ });
134
+
135
+ it('detects void response for 204 command endpoints', () => {
136
+ const spec = createSpec({
137
+ paths: {
138
+ '/api/v4/trackers/{id}': {
139
+ delete: {
140
+ tags: ['V4 Trackers'],
141
+ operationId: 'Trackers_DeleteDefinition',
142
+ 'x-remote-type': 'command',
143
+ 'x-remote-invalidates': ['GetDefinitions'],
144
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
145
+ responses: { '204': { description: 'No content' } },
146
+ } as any,
147
+ },
148
+ },
149
+ });
150
+
151
+ const result = parseOpenApiSpec(spec);
152
+ expect(result.operations[0].isVoidResponse).toBe(true);
153
+ });
154
+
155
+ it('detects void response for commands with 200 but no JSON body', () => {
156
+ const spec = createSpec({
157
+ paths: {
158
+ '/api/v4/trackers/{id}': {
159
+ delete: {
160
+ tags: ['V4 Trackers'],
161
+ operationId: 'Trackers_DeleteDefinition',
162
+ 'x-remote-type': 'command',
163
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
164
+ responses: { '200': { description: '' } },
165
+ } as any,
166
+ },
167
+ },
168
+ });
169
+
170
+ const result = parseOpenApiSpec(spec);
171
+ expect(result.operations[0].isVoidResponse).toBe(true);
172
+ });
173
+
174
+ it('does not mark as void when command 200 has JSON content', () => {
175
+ const spec = createSpec({
176
+ paths: {
177
+ '/api/v4/trackers': {
178
+ post: {
179
+ tags: ['V4 Trackers'],
180
+ operationId: 'Trackers_Create',
181
+ 'x-remote-type': 'command',
182
+ parameters: [],
183
+ requestBody: {
184
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Request' } } },
185
+ },
186
+ responses: {
187
+ '200': {
188
+ description: 'OK',
189
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/TrackerDto' } } },
190
+ },
191
+ },
192
+ } as any,
193
+ },
194
+ },
195
+ });
196
+
197
+ const result = parseOpenApiSpec(spec);
198
+ expect(result.operations[0].isVoidResponse).toBe(false);
199
+ });
200
+
201
+ it('does not mark queries as void even without response schema', () => {
202
+ const spec = createSpec({
203
+ paths: {
204
+ '/api/v4/trackers': {
205
+ get: {
206
+ tags: ['V4 Trackers'],
207
+ operationId: 'Trackers_GetDefinitions',
208
+ 'x-remote-type': 'query',
209
+ parameters: [],
210
+ responses: { '200': { description: 'OK' } },
211
+ } as any,
212
+ },
213
+ },
214
+ });
215
+
216
+ const result = parseOpenApiSpec(spec);
217
+ expect(result.operations[0].isVoidResponse).toBe(false);
218
+ });
219
+
220
+ it('parses 201 response schema for create endpoints', () => {
221
+ const spec = createSpec({
222
+ paths: {
223
+ '/api/v4/trackers': {
224
+ post: {
225
+ tags: ['V4 Trackers'],
226
+ operationId: 'Trackers_Create',
227
+ 'x-remote-type': 'command',
228
+ parameters: [],
229
+ requestBody: {
230
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Request' } } },
231
+ },
232
+ responses: {
233
+ '201': {
234
+ description: 'Created',
235
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/TrackerDto' } } },
236
+ },
237
+ },
238
+ } as any,
239
+ },
240
+ },
241
+ });
242
+
243
+ const result = parseOpenApiSpec(spec);
244
+ expect(result.operations[0].responseSchema).toBe('TrackerDto');
245
+ expect(result.operations[0].isVoidResponse).toBe(false);
246
+ });
247
+
248
+ it('parses array response types', () => {
249
+ const spec = createSpec({
250
+ paths: {
251
+ '/api/v4/trackers': {
252
+ get: {
253
+ tags: ['V4 Trackers'],
254
+ operationId: 'Trackers_GetDefinitions',
255
+ 'x-remote-type': 'query',
256
+ parameters: [],
257
+ responses: {
258
+ '200': {
259
+ description: 'OK',
260
+ content: {
261
+ 'application/json': {
262
+ schema: { type: 'array', items: { $ref: '#/components/schemas/TrackerDefinitionDto' } },
263
+ },
264
+ },
265
+ },
266
+ },
267
+ } as any,
268
+ },
269
+ },
270
+ });
271
+
272
+ const result = parseOpenApiSpec(spec);
273
+ expect(result.operations[0].responseSchema).toBe('TrackerDefinitionDto[]');
274
+ });
275
+
276
+ it('parses single-object response types', () => {
277
+ const spec = createSpec({
278
+ paths: {
279
+ '/api/v4/trackers/{id}': {
280
+ get: {
281
+ tags: ['V4 Trackers'],
282
+ operationId: 'Trackers_GetDefinition',
283
+ 'x-remote-type': 'query',
284
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
285
+ responses: {
286
+ '200': {
287
+ description: 'OK',
288
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/TrackerDefinitionDto' } } },
289
+ },
290
+ },
291
+ } as any,
292
+ },
293
+ },
294
+ });
295
+
296
+ const result = parseOpenApiSpec(spec);
297
+ expect(result.operations[0].responseSchema).toBe('TrackerDefinitionDto');
298
+ });
299
+
300
+ it('parses request body schema', () => {
301
+ const spec = createSpec({
302
+ paths: {
303
+ '/api/v4/trackers': {
304
+ post: {
305
+ tags: ['V4 Trackers'],
306
+ operationId: 'Trackers_Create',
307
+ 'x-remote-type': 'command',
308
+ parameters: [],
309
+ requestBody: {
310
+ content: {
311
+ 'application/json': {
312
+ schema: { $ref: '#/components/schemas/CreateTrackerDefinitionRequest' },
313
+ },
314
+ },
315
+ },
316
+ responses: {
317
+ '200': {
318
+ description: 'OK',
319
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/TrackerDto' } } },
320
+ },
321
+ },
322
+ } as any,
323
+ },
324
+ },
325
+ });
326
+
327
+ const result = parseOpenApiSpec(spec);
328
+ expect(result.operations[0].requestBodySchema).toBe('CreateTrackerDefinitionRequestSchema');
329
+ });
330
+
331
+ it('parses invalidation targets', () => {
332
+ const spec = createSpec({
333
+ paths: {
334
+ '/api/v4/trackers': {
335
+ post: {
336
+ tags: ['V4 Trackers'],
337
+ operationId: 'Trackers_Create',
338
+ 'x-remote-type': 'command',
339
+ 'x-remote-invalidates': ['GetDefinitions', 'Trackers_GetActiveInstances'],
340
+ parameters: [],
341
+ requestBody: {
342
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Request' } } },
343
+ },
344
+ responses: { '200': { description: 'OK', content: { 'application/json': { schema: { $ref: '#/components/schemas/Dto' } } } } },
345
+ } as any,
346
+ },
347
+ },
348
+ });
349
+
350
+ const result = parseOpenApiSpec(spec);
351
+ expect(result.operations[0].invalidates).toEqual(['GetDefinitions', 'Trackers_GetActiveInstances']);
352
+ });
353
+
354
+ it('parses array request body schema', () => {
355
+ const spec = createSpec({
356
+ paths: {
357
+ '/api/v4/treatments/bulk': {
358
+ post: {
359
+ tags: ['V4 Treatments'],
360
+ operationId: 'Treatments_CreateTreatments',
361
+ 'x-remote-type': 'command',
362
+ 'x-remote-invalidates': ['GetTreatments'],
363
+ parameters: [],
364
+ requestBody: {
365
+ content: {
366
+ 'application/json': {
367
+ schema: {
368
+ type: 'array',
369
+ items: { $ref: '#/components/schemas/Treatment' },
370
+ },
371
+ },
372
+ },
373
+ },
374
+ responses: {
375
+ '201': {
376
+ description: 'Created',
377
+ content: {
378
+ 'application/json': {
379
+ schema: { type: 'array', items: { $ref: '#/components/schemas/Treatment' } },
380
+ },
381
+ },
382
+ },
383
+ },
384
+ } as any,
385
+ },
386
+ },
387
+ });
388
+
389
+ const result = parseOpenApiSpec(spec);
390
+ expect(result.operations[0].requestBodySchema).toBe('TreatmentSchema');
391
+ expect(result.operations[0].isArrayBody).toBe(true);
392
+ });
393
+
394
+ it('sets isArrayBody to false for non-array request bodies', () => {
395
+ const spec = createSpec({
396
+ paths: {
397
+ '/api/v4/trackers': {
398
+ post: {
399
+ tags: ['V4 Trackers'],
400
+ operationId: 'Trackers_Create',
401
+ 'x-remote-type': 'command',
402
+ parameters: [],
403
+ requestBody: {
404
+ content: {
405
+ 'application/json': {
406
+ schema: { $ref: '#/components/schemas/CreateTrackerDefinitionRequest' },
407
+ },
408
+ },
409
+ },
410
+ responses: {
411
+ '200': {
412
+ description: 'OK',
413
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/TrackerDto' } } },
414
+ },
415
+ },
416
+ } as any,
417
+ },
418
+ },
419
+ });
420
+
421
+ const result = parseOpenApiSpec(spec);
422
+ expect(result.operations[0].requestBodySchema).toBe('CreateTrackerDefinitionRequestSchema');
423
+ expect(result.operations[0].isArrayBody).toBe(false);
424
+ });
425
+
426
+ it('parses x-client-property from operations', () => {
427
+ const spec = createSpec({
428
+ paths: {
429
+ '/api/v4/foods': {
430
+ get: {
431
+ tags: ['V4 Foods'],
432
+ operationId: 'Foods_GetFavorites',
433
+ 'x-remote-type': 'query',
434
+ 'x-client-property': 'foodsV4',
435
+ parameters: [],
436
+ responses: { '200': { description: 'OK' } },
437
+ } as any,
438
+ },
439
+ },
440
+ });
441
+
442
+ const result = parseOpenApiSpec(spec);
443
+ expect(result.operations[0].clientPropertyName).toBe('foodsV4');
444
+ });
445
+
446
+ it('sets clientPropertyName to undefined when x-client-property is absent', () => {
447
+ const spec = createSpec({
448
+ paths: {
449
+ '/api/v4/trackers': {
450
+ get: {
451
+ tags: ['V4 Trackers'],
452
+ operationId: 'Trackers_GetDefinitions',
453
+ 'x-remote-type': 'query',
454
+ parameters: [],
455
+ responses: { '200': { description: 'OK' } },
456
+ } as any,
457
+ },
458
+ },
459
+ });
460
+
461
+ const result = parseOpenApiSpec(spec);
462
+ expect(result.operations[0].clientPropertyName).toBeUndefined();
463
+ });
464
+
465
+ it('does not include schemas property in ParsedSpec', () => {
466
+ const spec = createSpec({
467
+ components: { schemas: { Foo: { type: 'object' } as any } },
468
+ paths: {},
469
+ });
470
+
471
+ const result = parseOpenApiSpec(spec);
472
+ expect(result).not.toHaveProperty('schemas');
473
+ });
474
+
475
+ it('parses form remote type', () => {
476
+ const spec = createSpec({
477
+ paths: {
478
+ '/api/v4/foods/favorites': {
479
+ post: {
480
+ tags: ['V4 Foods'],
481
+ operationId: 'Foods_AddFavorite',
482
+ 'x-remote-type': 'form',
483
+ 'x-remote-invalidates': ['GetFavorites'],
484
+ parameters: [],
485
+ requestBody: {
486
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/AddFavoriteRequest' } } },
487
+ },
488
+ responses: { '200': { description: 'OK', content: { 'application/json': { schema: { $ref: '#/components/schemas/FoodDto' } } } } },
489
+ } as any,
490
+ },
491
+ },
492
+ });
493
+
494
+ const result = parseOpenApiSpec(spec);
495
+ expect(result.operations[0].remoteType).toBe('form');
496
+ expect(result.operations[0].invalidates).toEqual(['GetFavorites']);
497
+ });
498
+
499
+ it('detects void response for form endpoints', () => {
500
+ const spec = createSpec({
501
+ paths: {
502
+ '/api/v4/foods/favorites/{id}': {
503
+ delete: {
504
+ tags: ['V4 Foods'],
505
+ operationId: 'Foods_RemoveFavorite',
506
+ 'x-remote-type': 'form',
507
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
508
+ responses: { '204': { description: 'No content' } },
509
+ } as any,
510
+ },
511
+ },
512
+ });
513
+
514
+ const result = parseOpenApiSpec(spec);
515
+ expect(result.operations[0].isVoidResponse).toBe(true);
516
+ });
517
+
518
+ it('parses x-remote-batch on query endpoints', () => {
519
+ const spec = createSpec({
520
+ paths: {
521
+ '/api/v4/trackers/{id}': {
522
+ get: {
523
+ tags: ['V4 Trackers'],
524
+ operationId: 'Trackers_GetDefinition',
525
+ 'x-remote-type': 'query',
526
+ 'x-remote-batch': true,
527
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
528
+ responses: { '200': { description: 'OK', content: { 'application/json': { schema: { $ref: '#/components/schemas/TrackerDefinitionDto' } } } } },
529
+ } as any,
530
+ },
531
+ },
532
+ });
533
+
534
+ const result = parseOpenApiSpec(spec);
535
+ expect(result.operations[0].isBatch).toBe(true);
536
+ });
537
+
538
+ it('isBatch is false when x-remote-batch is absent', () => {
539
+ const spec = createSpec({
540
+ paths: {
541
+ '/api/v4/trackers/{id}': {
542
+ get: {
543
+ tags: ['V4 Trackers'],
544
+ operationId: 'Trackers_GetDefinition',
545
+ 'x-remote-type': 'query',
546
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
547
+ responses: { '200': { description: 'OK' } },
548
+ } as any,
549
+ },
550
+ },
551
+ });
552
+
553
+ const result = parseOpenApiSpec(spec);
554
+ expect(result.operations[0].isBatch).toBe(false);
555
+ });
556
+
557
+ it('isBatch is false for command endpoints even with x-remote-batch', () => {
558
+ const spec = createSpec({
559
+ paths: {
560
+ '/api/v4/trackers/{id}': {
561
+ delete: {
562
+ tags: ['V4 Trackers'],
563
+ operationId: 'Trackers_DeleteDefinition',
564
+ 'x-remote-type': 'command',
565
+ 'x-remote-batch': true,
566
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
567
+ responses: { '204': { description: 'No content' } },
568
+ } as any,
569
+ },
570
+ },
571
+ });
572
+
573
+ const result = parseOpenApiSpec(spec);
574
+ expect(result.operations[0].isBatch).toBe(false);
575
+ });
576
+ });