Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions packages/openapi-generator/src/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,17 +183,20 @@ function parseRequestUnion(
parameters.push(...headerParams.values());
}

const firstSubSchema = schema.schemas[0];
if (firstSubSchema !== undefined && firstSubSchema.type === 'object') {
const pathSchema = firstSubSchema.properties['params'];
if (pathSchema !== undefined && pathSchema.type === 'object') {
for (const [name, prop] of Object.entries(pathSchema.properties)) {
parameters.push({
type: 'path',
name,
schema: prop,
required: pathSchema.required.includes(name),
});
// Find the first schema in the union that has path parameters
for (const subSchema of schema.schemas) {
if (subSchema.type === 'object') {
const pathSchema = subSchema.properties['params'];
if (pathSchema !== undefined && pathSchema.type === 'object') {
for (const [name, prop] of Object.entries(pathSchema.properties)) {
parameters.push({
type: 'path',
name,
schema: prop,
required: pathSchema.required.includes(name),
});
}
break; // Found path params, stop looking
}
}
}
Expand Down
357 changes: 357 additions & 0 deletions packages/openapi-generator/test/openapi/union.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,363 @@ testCase('route with unknown unions', ROUTE_WITH_UNKNOWN_UNIONS, {
},
});

const ROUTE_WITH_PATH_PARAMS_IN_UNION_NOT_FIRST = `
import * as t from 'io-ts';
import * as h from '@api-ts/io-ts-http';

export const route = h.httpRoute({
path: '/internal/api/policy/v1/{applicationName}/touchpoints/{touchpoint}/rules/evaluation',
method: 'POST',
request: t.union([
h.httpRequest({
body: { emptyRequest: t.boolean }
}),
h.httpRequest({
params: {
applicationName: t.string,
touchpoint: t.string,
},
body: { requestWithParams: t.string }
}),
]),
response: {
200: t.string,
},
});
`;

testCase(
'route with path params in union second schema (regression test)',
ROUTE_WITH_PATH_PARAMS_IN_UNION_NOT_FIRST,
{
info: {
title: 'Test',
version: '1.0.0',
},
openapi: '3.0.3',
paths: {
'/internal/api/policy/v1/{applicationName}/touchpoints/{touchpoint}/rules/evaluation':
{
post: {
parameters: [
{
in: 'path',
name: 'applicationName',
required: true,
schema: { type: 'string' },
},
{
in: 'path',
name: 'touchpoint',
required: true,
schema: { type: 'string' },
},
],
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
properties: {
emptyRequest: { type: 'boolean' },
},
required: ['emptyRequest'],
type: 'object',
},
{
properties: {
requestWithParams: { type: 'string' },
},
required: ['requestWithParams'],
type: 'object',
},
],
},
},
},
},
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
components: {
schemas: {},
},
},
);

const ROUTE_WITH_PATH_PARAMS_ONLY_IN_THIRD_SCHEMA = `
import * as t from 'io-ts';
import * as h from '@api-ts/io-ts-http';

export const route = h.httpRoute({
path: '/api/{userId}/posts/{postId}',
method: 'GET',
request: t.union([
// First: empty request
h.httpRequest({}),
// Second: only query params
h.httpRequest({
query: { filter: t.string }
}),
// Third: has the path params
h.httpRequest({
params: {
userId: t.string,
postId: t.string,
},
query: { details: t.boolean }
}),
]),
response: {
200: t.string,
},
});
`;

testCase(
'route with path params only in third schema',
ROUTE_WITH_PATH_PARAMS_ONLY_IN_THIRD_SCHEMA,
{
info: {
title: 'Test',
version: '1.0.0',
},
openapi: '3.0.3',
paths: {
'/api/{userId}/posts/{postId}': {
get: {
parameters: [
{
in: 'query',
name: 'union',
required: true,
explode: true,
style: 'form',
schema: {
oneOf: [
{
properties: { filter: { type: 'string' } },
required: ['filter'],
type: 'object',
},
{
properties: { details: { type: 'boolean' } },
required: ['details'],
type: 'object',
},
],
},
},
{ in: 'path', name: 'userId', required: true, schema: { type: 'string' } },
{ in: 'path', name: 'postId', required: true, schema: { type: 'string' } },
],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
components: {
schemas: {},
},
},
);

const ROUTE_WITH_FULLY_DEFINED_PARAMS = `
import * as t from 'io-ts';
import * as h from '@api-ts/io-ts-http';

const AddressBookConnectionSides = t.union([t.literal('send'), t.literal('receive')]);

/**
* Create policy evaluation definition
* @operationId v1.post.policy.evaluation.definition
* @tag Policy Builder
* @private
*/
export const route = h.httpRoute({
path: '/internal/api/policy/v1/{applicationName}/touchpoints/{touchpoint}/rules/evaluations',
method: 'POST',
request: t.union([
h.httpRequest({
params: {
applicationName: t.string,
touchpoint: t.string,
},
body: t.type({
approvalRequestId: t.string,
counterPartyId: t.string,
description: h.optional(t.string),
enterpriseId: t.string,
grossAmount: h.optional(t.number),
idempotencyKey: t.string,
isFirstTimeCounterParty: t.boolean,
isMutualConnection: t.boolean,
netAmount: h.optional(t.number),
settlementId: t.string,
userId: t.string,
walletId: t.string,
})
}),
h.httpRequest({
params: {
applicationName: t.string,
touchpoint: t.string,
},
body: t.type({
connectionId: t.string,
description: h.optional(t.string),
enterpriseId: t.string,
idempotencyKey: t.string,
side: AddressBookConnectionSides,
walletId: t.string,
})
}),
]),
response: {
200: t.string,
},
});
`;

testCase(
'union request with consistently defined path parameters',
ROUTE_WITH_FULLY_DEFINED_PARAMS,
{
info: {
title: 'Test',
version: '1.0.0',
},
openapi: '3.0.3',
paths: {
'/internal/api/policy/v1/{applicationName}/touchpoints/{touchpoint}/rules/evaluations':
{
post: {
summary: 'Create policy evaluation definition',
operationId: 'v1.post.policy.evaluation.definition',
tags: ['Policy Builder'],
'x-internal': true,
parameters: [
{
in: 'path',
name: 'applicationName',
required: true,
schema: { type: 'string' },
},
{
in: 'path',
name: 'touchpoint',
required: true,
schema: { type: 'string' },
},
],
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
type: 'object',
properties: {
approvalRequestId: { type: 'string' },
counterPartyId: { type: 'string' },
description: { type: 'string' },
enterpriseId: { type: 'string' },
grossAmount: { type: 'number' },
idempotencyKey: { type: 'string' },
isFirstTimeCounterParty: { type: 'boolean' },
isMutualConnection: { type: 'boolean' },
netAmount: { type: 'number' },
settlementId: { type: 'string' },
userId: { type: 'string' },
walletId: { type: 'string' },
},
required: [
'approvalRequestId',
'counterPartyId',
'enterpriseId',
'idempotencyKey',
'isFirstTimeCounterParty',
'isMutualConnection',
'settlementId',
'userId',
'walletId',
],
},
{
type: 'object',
properties: {
connectionId: { type: 'string' },
description: { type: 'string' },
enterpriseId: { type: 'string' },
idempotencyKey: { type: 'string' },
side: {
$ref: '#/components/schemas/AddressBookConnectionSides',
},
walletId: { type: 'string' },
},
required: [
'connectionId',
'enterpriseId',
'idempotencyKey',
'side',
'walletId',
],
},
],
},
},
},
},
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
components: {
schemas: {
AddressBookConnectionSides: {
enum: ['send', 'receive'],
title: 'AddressBookConnectionSides',
type: 'string',
},
},
},
},
);

const ROUTE_WITH_DUPLICATE_HEADERS = `
import * as t from 'io-ts';
import * as h from '@api-ts/io-ts-http';
Expand Down
Loading