@@ -353,6 +353,334 @@ testCase('route with unknown unions', ROUTE_WITH_UNKNOWN_UNIONS, {
353353 } ,
354354} ) ;
355355
356+ const ROUTE_WITH_PATH_PARAMS_IN_UNION_NOT_FIRST = `
357+ import * as t from 'io-ts';
358+ import * as h from '@api-ts/io-ts-http';
359+
360+ export const route = h.httpRoute({
361+ path: '/internal/api/policy/v1/{applicationName}/touchpoints/{touchpoint}/rules/evaluation',
362+ method: 'POST',
363+ request: t.union([
364+ // First schema has NO path parameters - this was causing the bug
365+ h.httpRequest({
366+ body: { emptyRequest: t.boolean }
367+ }),
368+ // Second schema HAS path parameters - these should be preserved
369+ h.httpRequest({
370+ params: {
371+ applicationName: t.string,
372+ touchpoint: t.string,
373+ },
374+ body: { requestWithParams: t.string }
375+ }),
376+ ]),
377+ response: {
378+ 200: t.string,
379+ },
380+ });
381+ ` ;
382+
383+ testCase (
384+ 'route with path params in union second schema (regression test)' ,
385+ ROUTE_WITH_PATH_PARAMS_IN_UNION_NOT_FIRST ,
386+ {
387+ info : {
388+ title : 'Test' ,
389+ version : '1.0.0' ,
390+ } ,
391+ openapi : '3.0.3' ,
392+ paths : {
393+ '/internal/api/policy/v1/{applicationName}/touchpoints/{touchpoint}/rules/evaluation' : {
394+ post : {
395+ parameters : [
396+ { in : 'path' , name : 'applicationName' , required : true , schema : { type : 'string' } } ,
397+ { in : 'path' , name : 'touchpoint' , required : true , schema : { type : 'string' } } ,
398+ ] ,
399+ requestBody : {
400+ content : {
401+ 'application/json' : {
402+ schema : {
403+ oneOf : [
404+ {
405+ properties : {
406+ emptyRequest : { type : 'boolean' }
407+ } ,
408+ required : [ 'emptyRequest' ] ,
409+ type : 'object' ,
410+ } ,
411+ {
412+ properties : {
413+ requestWithParams : { type : 'string' }
414+ } ,
415+ required : [ 'requestWithParams' ] ,
416+ type : 'object' ,
417+ } ,
418+ ] ,
419+ } ,
420+ } ,
421+ } ,
422+ } ,
423+ responses : {
424+ '200' : {
425+ description : 'OK' ,
426+ content : {
427+ 'application/json' : {
428+ schema : {
429+ type : 'string' ,
430+ } ,
431+ } ,
432+ } ,
433+ } ,
434+ } ,
435+ } ,
436+ } ,
437+ } ,
438+ components : {
439+ schemas : { } ,
440+ } ,
441+ } ,
442+ ) ;
443+
444+ const ROUTE_WITH_PATH_PARAMS_ONLY_IN_THIRD_SCHEMA = `
445+ import * as t from 'io-ts';
446+ import * as h from '@api-ts/io-ts-http';
447+
448+ export const route = h.httpRoute({
449+ path: '/api/{userId}/posts/{postId}',
450+ method: 'GET',
451+ request: t.union([
452+ // First: empty request
453+ h.httpRequest({}),
454+ // Second: only query params
455+ h.httpRequest({
456+ query: { filter: t.string }
457+ }),
458+ // Third: has the path params
459+ h.httpRequest({
460+ params: {
461+ userId: t.string,
462+ postId: t.string,
463+ },
464+ query: { details: t.boolean }
465+ }),
466+ ]),
467+ response: {
468+ 200: t.string,
469+ },
470+ });
471+ ` ;
472+
473+ testCase (
474+ 'route with path params only in third schema' ,
475+ ROUTE_WITH_PATH_PARAMS_ONLY_IN_THIRD_SCHEMA ,
476+ {
477+ info : {
478+ title : 'Test' ,
479+ version : '1.0.0' ,
480+ } ,
481+ openapi : '3.0.3' ,
482+ paths : {
483+ '/api/{userId}/posts/{postId}' : {
484+ get : {
485+ parameters : [
486+ { in : 'query' , name : 'union' , required : true , explode : true , style : 'form' , schema : {
487+ oneOf : [
488+ {
489+ properties : { filter : { type : 'string' } } ,
490+ required : [ 'filter' ] ,
491+ type : 'object'
492+ } ,
493+ {
494+ properties : { details : { type : 'boolean' } } ,
495+ required : [ 'details' ] ,
496+ type : 'object'
497+ }
498+ ]
499+ } } ,
500+ { in : 'path' , name : 'userId' , required : true , schema : { type : 'string' } } ,
501+ { in : 'path' , name : 'postId' , required : true , schema : { type : 'string' } } ,
502+ ] ,
503+ responses : {
504+ '200' : {
505+ description : 'OK' ,
506+ content : {
507+ 'application/json' : {
508+ schema : {
509+ type : 'string' ,
510+ } ,
511+ } ,
512+ } ,
513+ } ,
514+ } ,
515+ } ,
516+ } ,
517+ } ,
518+ components : {
519+ schemas : { } ,
520+ } ,
521+ } ,
522+ ) ;
523+
524+ const REAL_WORLD_POLICY_EVALUATION_ROUTE = `
525+ import * as t from 'io-ts';
526+ import * as h from '@api-ts/io-ts-http';
527+
528+ const AddressBookConnectionSides = t.union([t.literal('send'), t.literal('receive')]);
529+
530+ /**
531+ * Create policy evaluation definition
532+ * @operationId v1.post.policy.evaluation.definition
533+ * @tag Policy Builder
534+ * @private
535+ */
536+ export const route = h.httpRoute({
537+ path: '/internal/api/policy/v1/{applicationName}/touchpoints/{touchpoint}/rules/evaluations',
538+ method: 'POST',
539+ request: t.union([
540+ h.httpRequest({
541+ params: {
542+ applicationName: t.string,
543+ touchpoint: t.string,
544+ },
545+ body: t.type({
546+ approvalRequestId: t.string,
547+ counterPartyId: t.string,
548+ description: h.optional(t.string),
549+ enterpriseId: t.string,
550+ grossAmount: h.optional(t.number),
551+ idempotencyKey: t.string,
552+ isFirstTimeCounterParty: t.boolean,
553+ isMutualConnection: t.boolean,
554+ netAmount: h.optional(t.number),
555+ settlementId: t.string,
556+ userId: t.string,
557+ walletId: t.string,
558+ })
559+ }),
560+ h.httpRequest({
561+ params: {
562+ applicationName: t.string,
563+ touchpoint: t.string,
564+ },
565+ body: t.type({
566+ connectionId: t.string,
567+ description: h.optional(t.string),
568+ enterpriseId: t.string,
569+ idempotencyKey: t.string,
570+ side: AddressBookConnectionSides,
571+ walletId: t.string,
572+ })
573+ }),
574+ ]),
575+ response: {
576+ 200: t.string,
577+ },
578+ });
579+ ` ;
580+
581+ testCase (
582+ 'real-world policy evaluation route with union request bodies' ,
583+ REAL_WORLD_POLICY_EVALUATION_ROUTE ,
584+ {
585+ info : {
586+ title : 'Test' ,
587+ version : '1.0.0' ,
588+ } ,
589+ openapi : '3.0.3' ,
590+ paths : {
591+ '/internal/api/policy/v1/{applicationName}/touchpoints/{touchpoint}/rules/evaluations' : {
592+ post : {
593+ summary : 'Create policy evaluation definition' ,
594+ operationId : 'v1.post.policy.evaluation.definition' ,
595+ tags : [ 'Policy Builder' ] ,
596+ 'x-internal' : true ,
597+ parameters : [
598+ { in : 'path' , name : 'applicationName' , required : true , schema : { type : 'string' } } ,
599+ { in : 'path' , name : 'touchpoint' , required : true , schema : { type : 'string' } } ,
600+ ] ,
601+ requestBody : {
602+ content : {
603+ 'application/json' : {
604+ schema : {
605+ oneOf : [
606+ {
607+ type : 'object' ,
608+ properties : {
609+ approvalRequestId : { type : 'string' } ,
610+ counterPartyId : { type : 'string' } ,
611+ description : { type : 'string' } ,
612+ enterpriseId : { type : 'string' } ,
613+ grossAmount : { type : 'number' } ,
614+ idempotencyKey : { type : 'string' } ,
615+ isFirstTimeCounterParty : { type : 'boolean' } ,
616+ isMutualConnection : { type : 'boolean' } ,
617+ netAmount : { type : 'number' } ,
618+ settlementId : { type : 'string' } ,
619+ userId : { type : 'string' } ,
620+ walletId : { type : 'string' } ,
621+ } ,
622+ required : [
623+ 'approvalRequestId' ,
624+ 'counterPartyId' ,
625+ 'enterpriseId' ,
626+ 'idempotencyKey' ,
627+ 'isFirstTimeCounterParty' ,
628+ 'isMutualConnection' ,
629+ 'settlementId' ,
630+ 'userId' ,
631+ 'walletId'
632+ ] ,
633+ } ,
634+ {
635+ type : 'object' ,
636+ properties : {
637+ connectionId : { type : 'string' } ,
638+ description : { type : 'string' } ,
639+ enterpriseId : { type : 'string' } ,
640+ idempotencyKey : { type : 'string' } ,
641+ side : { $ref : '#/components/schemas/AddressBookConnectionSides' } ,
642+ walletId : { type : 'string' } ,
643+ } ,
644+ required : [
645+ 'connectionId' ,
646+ 'enterpriseId' ,
647+ 'idempotencyKey' ,
648+ 'side' ,
649+ 'walletId'
650+ ] ,
651+ } ,
652+ ] ,
653+ } ,
654+ } ,
655+ } ,
656+ } ,
657+ responses : {
658+ '200' : {
659+ description : 'OK' ,
660+ content : {
661+ 'application/json' : {
662+ schema : {
663+ type : 'string' ,
664+ } ,
665+ } ,
666+ } ,
667+ } ,
668+ } ,
669+ } ,
670+ } ,
671+ } ,
672+ components : {
673+ schemas : {
674+ AddressBookConnectionSides : {
675+ enum : [ 'send' , 'receive' ] ,
676+ title : 'AddressBookConnectionSides' ,
677+ type : 'string' ,
678+ } ,
679+ } ,
680+ } ,
681+ } ,
682+ ) ;
683+
356684const ROUTE_WITH_DUPLICATE_HEADERS = `
357685import * as t from 'io-ts';
358686import * as h from '@api-ts/io-ts-http';
0 commit comments