Skip to content

Commit bbaf29a

Browse files
committed
feat(generator): emit bodyType in URL map for non-JSON endpoints
1 parent 54948ab commit bbaf29a

File tree

3 files changed

+408
-14
lines changed

3 files changed

+408
-14
lines changed

packages/generator/src/__tests__/create-url-map.test.ts

Lines changed: 365 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/** biome-ignore-all lint/style/noNonNullAssertion: test code */
2-
import { expect, test } from 'bun:test'
2+
import { describe, expect, test } from 'bun:test'
33
import type { UrlMapValue } from '@devup-api/core'
44
import type { OpenAPIV3_1 } from 'openapi-types'
5-
import { createUrlMap } from '../create-url-map'
5+
import { createUrlMap, getBodyType } from '../create-url-map'
66

77
test.each([
88
[
@@ -334,3 +334,366 @@ test.each([
334334

335335
expect(result['']).toHaveProperty(expectedKey)
336336
})
337+
338+
describe('bodyType emission', () => {
339+
test('emits bodyType: form for application/x-www-form-urlencoded endpoint', () => {
340+
const schema: OpenAPIV3_1.Document = {
341+
openapi: '3.1.0',
342+
info: { title: 'Test API', version: '1.0.0' },
343+
paths: {
344+
'/form': {
345+
post: {
346+
operationId: 'subscribe',
347+
requestBody: {
348+
content: {
349+
'application/x-www-form-urlencoded': {
350+
schema: {
351+
$ref: '#/components/schemas/SubscribeRequest',
352+
},
353+
},
354+
},
355+
},
356+
responses: {},
357+
},
358+
},
359+
},
360+
}
361+
362+
const result = createUrlMap({ '': schema })
363+
364+
expect(result['']!.subscribe).toEqual({
365+
method: 'POST',
366+
url: '/form',
367+
bodyType: 'form',
368+
})
369+
expect(result['']!['/form']).toEqual({
370+
method: 'POST',
371+
url: '/form',
372+
bodyType: 'form',
373+
})
374+
})
375+
376+
test('emits bodyType: multipart for multipart/form-data endpoint', () => {
377+
const schema: OpenAPIV3_1.Document = {
378+
openapi: '3.1.0',
379+
info: { title: 'Test API', version: '1.0.0' },
380+
paths: {
381+
'/upload': {
382+
post: {
383+
operationId: 'upload_file',
384+
requestBody: {
385+
content: {
386+
'multipart/form-data': {
387+
schema: {
388+
type: 'object',
389+
},
390+
},
391+
},
392+
},
393+
responses: {},
394+
},
395+
},
396+
},
397+
}
398+
399+
const result = createUrlMap({ '': schema })
400+
401+
expect(result['']!.uploadFile).toEqual({
402+
method: 'POST',
403+
url: '/upload',
404+
bodyType: 'multipart',
405+
})
406+
expect(result['']!['/upload']).toEqual({
407+
method: 'POST',
408+
url: '/upload',
409+
bodyType: 'multipart',
410+
})
411+
})
412+
413+
test('does NOT emit bodyType for application/json endpoint', () => {
414+
const schema: OpenAPIV3_1.Document = {
415+
openapi: '3.1.0',
416+
info: { title: 'Test API', version: '1.0.0' },
417+
paths: {
418+
'/users': {
419+
post: {
420+
operationId: 'create_user',
421+
requestBody: {
422+
content: {
423+
'application/json': {
424+
schema: {
425+
$ref: '#/components/schemas/CreateUserRequest',
426+
},
427+
},
428+
},
429+
},
430+
responses: {},
431+
},
432+
},
433+
},
434+
}
435+
436+
const result = createUrlMap({ '': schema })
437+
438+
expect(result['']!.createUser).toEqual({
439+
method: 'POST',
440+
url: '/users',
441+
})
442+
expect(result['']!.createUser).not.toHaveProperty('bodyType')
443+
expect(result['']!['/users']).not.toHaveProperty('bodyType')
444+
})
445+
446+
test('does NOT emit bodyType for GET endpoint without requestBody', () => {
447+
const schema: OpenAPIV3_1.Document = {
448+
openapi: '3.1.0',
449+
info: { title: 'Test API', version: '1.0.0' },
450+
paths: {
451+
'/users': {
452+
get: {
453+
operationId: 'get_users',
454+
responses: {},
455+
},
456+
},
457+
},
458+
}
459+
460+
const result = createUrlMap({ '': schema })
461+
462+
expect(result['']!.getUsers).toEqual({
463+
method: 'GET',
464+
url: '/users',
465+
})
466+
expect(result['']!.getUsers).not.toHaveProperty('bodyType')
467+
})
468+
469+
test('resolves $ref in requestBody to detect content type', () => {
470+
const schema: OpenAPIV3_1.Document = {
471+
openapi: '3.1.0',
472+
info: { title: 'Test API', version: '1.0.0' },
473+
paths: {
474+
'/form': {
475+
post: {
476+
operationId: 'submit_form',
477+
requestBody: {
478+
$ref: '#/components/requestBodies/FormBody',
479+
},
480+
responses: {},
481+
},
482+
},
483+
},
484+
components: {
485+
requestBodies: {
486+
FormBody: {
487+
content: {
488+
'application/x-www-form-urlencoded': {
489+
schema: {
490+
type: 'object',
491+
properties: {
492+
name: { type: 'string' },
493+
},
494+
},
495+
},
496+
},
497+
},
498+
},
499+
},
500+
}
501+
502+
const result = createUrlMap({ '': schema })
503+
504+
expect(result['']!.submitForm).toEqual({
505+
method: 'POST',
506+
url: '/form',
507+
bodyType: 'form',
508+
})
509+
})
510+
511+
test('resolves $ref in requestBody for multipart content type', () => {
512+
const schema: OpenAPIV3_1.Document = {
513+
openapi: '3.1.0',
514+
info: { title: 'Test API', version: '1.0.0' },
515+
paths: {
516+
'/upload': {
517+
post: {
518+
operationId: 'upload_file',
519+
requestBody: {
520+
$ref: '#/components/requestBodies/UploadBody',
521+
},
522+
responses: {},
523+
},
524+
},
525+
},
526+
components: {
527+
requestBodies: {
528+
UploadBody: {
529+
content: {
530+
'multipart/form-data': {
531+
schema: {
532+
type: 'object',
533+
},
534+
},
535+
},
536+
},
537+
},
538+
},
539+
}
540+
541+
const result = createUrlMap({ '': schema })
542+
543+
expect(result['']!.uploadFile).toEqual({
544+
method: 'POST',
545+
url: '/upload',
546+
bodyType: 'multipart',
547+
})
548+
})
549+
550+
test('handles mixed endpoints with different body types', () => {
551+
const schema: OpenAPIV3_1.Document = {
552+
openapi: '3.1.0',
553+
info: { title: 'Test API', version: '1.0.0' },
554+
paths: {
555+
'/users': {
556+
post: {
557+
operationId: 'create_user',
558+
requestBody: {
559+
content: {
560+
'application/json': {
561+
schema: { $ref: '#/components/schemas/User' },
562+
},
563+
},
564+
},
565+
responses: {},
566+
},
567+
},
568+
'/form': {
569+
post: {
570+
operationId: 'subscribe',
571+
requestBody: {
572+
content: {
573+
'application/x-www-form-urlencoded': {
574+
schema: { $ref: '#/components/schemas/SubscribeRequest' },
575+
},
576+
},
577+
},
578+
responses: {},
579+
},
580+
},
581+
'/upload': {
582+
post: {
583+
operationId: 'upload_file',
584+
requestBody: {
585+
content: {
586+
'multipart/form-data': {
587+
schema: { type: 'object' },
588+
},
589+
},
590+
},
591+
responses: {},
592+
},
593+
},
594+
},
595+
}
596+
597+
const result = createUrlMap({ '': schema })
598+
599+
// JSON endpoint: no bodyType
600+
expect(result['']!.createUser).not.toHaveProperty('bodyType')
601+
// Form endpoint: bodyType = 'form'
602+
expect(result['']!.subscribe!.bodyType).toBe('form')
603+
// Multipart endpoint: bodyType = 'multipart'
604+
expect(result['']!.uploadFile!.bodyType).toBe('multipart')
605+
})
606+
})
607+
608+
describe('getBodyType', () => {
609+
const emptyDoc: OpenAPIV3_1.Document = {
610+
openapi: '3.1.0',
611+
info: { title: 'Test', version: '1.0.0' },
612+
paths: {},
613+
}
614+
615+
test('returns undefined when no requestBody', () => {
616+
expect(getBodyType({ responses: {} }, emptyDoc)).toBeUndefined()
617+
})
618+
619+
test('returns undefined for JSON content', () => {
620+
expect(
621+
getBodyType(
622+
{
623+
requestBody: {
624+
content: {
625+
'application/json': { schema: { type: 'object' } },
626+
},
627+
},
628+
responses: {},
629+
},
630+
emptyDoc,
631+
),
632+
).toBeUndefined()
633+
})
634+
635+
test('returns form for urlencoded content', () => {
636+
expect(
637+
getBodyType(
638+
{
639+
requestBody: {
640+
content: {
641+
'application/x-www-form-urlencoded': {
642+
schema: { type: 'object' },
643+
},
644+
},
645+
},
646+
responses: {},
647+
},
648+
emptyDoc,
649+
),
650+
).toBe('form')
651+
})
652+
653+
test('returns multipart for multipart/form-data content', () => {
654+
expect(
655+
getBodyType(
656+
{
657+
requestBody: {
658+
content: {
659+
'multipart/form-data': { schema: { type: 'object' } },
660+
},
661+
},
662+
responses: {},
663+
},
664+
emptyDoc,
665+
),
666+
).toBe('multipart')
667+
})
668+
669+
test('returns undefined when requestBody has no content', () => {
670+
expect(
671+
getBodyType(
672+
{
673+
requestBody: { content: {} },
674+
responses: {},
675+
},
676+
emptyDoc,
677+
),
678+
).toBeUndefined()
679+
})
680+
681+
test('prefers urlencoded over multipart when both present', () => {
682+
expect(
683+
getBodyType(
684+
{
685+
requestBody: {
686+
content: {
687+
'application/x-www-form-urlencoded': {
688+
schema: { type: 'object' },
689+
},
690+
'multipart/form-data': { schema: { type: 'object' } },
691+
},
692+
},
693+
responses: {},
694+
},
695+
emptyDoc,
696+
),
697+
).toBe('form')
698+
})
699+
})

packages/generator/src/__tests__/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ test('index.ts exports', () => {
55
expect({ ...indexModule }).toEqual({
66
// URL map
77
createUrlMap: expect.any(Function),
8+
getBodyType: expect.any(Function),
89
// Interface generation
910
generateInterface: expect.any(Function),
1011
// Zod generation

0 commit comments

Comments
 (0)