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