Skip to content

Commit 8b3e259

Browse files
authored
Merge pull request #1048 from BitGo/dx-1580-no-ref-siblings
fix: fix no-$ref-siblings error
2 parents bb98656 + cbe1c86 commit 8b3e259

File tree

2 files changed

+231
-14
lines changed

2 files changed

+231
-14
lines changed

packages/openapi-generator/src/openapi.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,33 @@ export function schemaToOpenAPI(
5454
const { example, minItems, maxItems, ...rest } = defaultOpenAPIObject;
5555
const isArrayExample = example && Array.isArray(example);
5656

57+
const siblings = {
58+
...rest,
59+
...(!isArrayExample && example ? { example } : {}),
60+
};
61+
62+
// Handle case where innerSchema is a $ref with siblings
63+
const wrappedInnerSchema =
64+
'$ref' in innerSchema && Object.keys(siblings).length > 0
65+
? // When there's a $ref with siblings, we need to wrap it in allOf to preserve other properties
66+
{
67+
allOf: [innerSchema],
68+
}
69+
: {
70+
...innerSchema,
71+
};
72+
73+
const items = {
74+
...wrappedInnerSchema,
75+
...siblings,
76+
};
77+
5778
return {
5879
type: 'array',
5980
...(minItems ? { minItems } : {}),
6081
...(maxItems ? { maxItems } : {}),
6182
...(isArrayExample ? { example } : {}),
62-
items: {
63-
...innerSchema,
64-
...rest,
65-
...(!isArrayExample && example ? { example } : {}),
66-
},
83+
items,
6784
};
6885
case 'object':
6986
return {
@@ -153,23 +170,25 @@ export function schemaToOpenAPI(
153170
if (oneOf.length === 0) {
154171
return undefined;
155172
} else if (oneOf.length === 1) {
156-
if (
157-
Object.keys(
158-
oneOf[0] as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
159-
)[0] === '$ref'
160-
)
173+
const singleSchema = oneOf[0];
174+
if (singleSchema === undefined) {
175+
return undefined;
176+
}
177+
// Check if the schema is a $ref
178+
if ('$ref' in singleSchema) {
161179
// OpenAPI spec doesn't allow $ref properties to have siblings, so they're wrapped in an 'allOf' array
162180
return {
163181
...(nullable ? { nullable } : {}),
164-
allOf: oneOf,
182+
allOf: [singleSchema],
165183
...defaultOpenAPIObject,
166184
};
167-
else
185+
} else {
168186
return {
169187
...(nullable ? { nullable } : {}),
170-
...oneOf[0],
188+
...singleSchema,
171189
...defaultOpenAPIObject,
172190
};
191+
}
173192
} else {
174193
return { ...(nullable ? { nullable } : {}), oneOf, ...defaultOpenAPIObject };
175194
}

packages/openapi-generator/test/openapi/ref.test.ts

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ const SimpleRouteResponse = t.type({
336336
* @title Human Readable Invalid Error Schema
337337
*/
338338
const InvalidError = t.intersection([
339-
ApiError,
339+
ApiError,
340340
t.type({ error: t.literal('invalid') })]);
341341
/**
342342
* Human readable description of the ApiError schema
@@ -441,3 +441,201 @@ testCase('route with api error schema', ROUTE_WITH_SCHEMA_WITH_COMMENT, {
441441
},
442442
},
443443
});
444+
445+
const ROUTE_WITH_ARRAY_OF_INNER_SCHEMA_REF_WITH_NO_SIBLINGS = `
446+
import * as t from 'io-ts';
447+
import * as h from '@api-ts/io-ts-http';
448+
/**
449+
* A simple route with type descriptions for references
450+
*
451+
* @operationId api.v1.test
452+
* @tag Test Routes
453+
*/
454+
export const route = h.httpRoute({
455+
path: '/foo',
456+
method: 'GET',
457+
request: h.httpRequest({}),
458+
response: {
459+
200: SimpleRouteResponse
460+
},
461+
});
462+
463+
464+
/**
465+
* Human readable description of the Simple Route Response
466+
* @title Human Readable Simple Route Response
467+
*/
468+
const SimpleRouteResponse = t.type({
469+
test: t.array(TestRef)
470+
});
471+
472+
const TestRefBase = t.string;
473+
474+
const TestRef = TestRefBase;
475+
`;
476+
477+
testCase(
478+
'inner schema ref in array',
479+
ROUTE_WITH_ARRAY_OF_INNER_SCHEMA_REF_WITH_NO_SIBLINGS,
480+
{
481+
openapi: '3.0.3',
482+
info: {
483+
title: 'Test',
484+
version: '1.0.0',
485+
},
486+
paths: {
487+
'/foo': {
488+
get: {
489+
summary: 'A simple route with type descriptions for references',
490+
operationId: 'api.v1.test',
491+
tags: ['Test Routes'],
492+
parameters: [],
493+
responses: {
494+
'200': {
495+
description: 'OK',
496+
content: {
497+
'application/json': {
498+
schema: {
499+
$ref: '#/components/schemas/SimpleRouteResponse',
500+
},
501+
},
502+
},
503+
},
504+
},
505+
},
506+
},
507+
},
508+
components: {
509+
schemas: {
510+
SimpleRouteResponse: {
511+
description: 'Human readable description of the Simple Route Response',
512+
properties: {
513+
test: {
514+
type: 'array',
515+
items: {
516+
$ref: '#/components/schemas/TestRefBase',
517+
},
518+
},
519+
},
520+
required: ['test'],
521+
title: 'Human Readable Simple Route Response',
522+
type: 'object',
523+
},
524+
TestRefBase: {
525+
title: 'TestRefBase',
526+
type: 'string',
527+
},
528+
TestRef: {
529+
allOf: [
530+
{
531+
title: 'TestRef',
532+
},
533+
{
534+
$ref: '#/components/schemas/TestRefBase',
535+
},
536+
],
537+
},
538+
},
539+
},
540+
},
541+
);
542+
543+
const ROUTE_WITH_ARRAY_OF_INNER_SCHEMA_REF_AND_DESCRIPTION = `
544+
import * as t from 'io-ts';
545+
import * as h from '@api-ts/io-ts-http';
546+
/**
547+
* A simple route with type descriptions for references
548+
*
549+
* @operationId api.v1.test
550+
* @tag Test Routes
551+
*/
552+
export const route = h.httpRoute({
553+
path: '/foo',
554+
method: 'GET',
555+
request: h.httpRequest({}),
556+
response: {
557+
200: SimpleRouteResponse
558+
},
559+
});
560+
561+
562+
/**
563+
* Human readable description of the Simple Route Response
564+
* @title Human Readable Simple Route Response
565+
*/
566+
const SimpleRouteResponse = t.type({
567+
/** List of test refs */
568+
test: t.array(TestRef)
569+
});
570+
571+
const TestRefBase = t.string;
572+
573+
const TestRef = TestRefBase;
574+
`;
575+
576+
testCase(
577+
'inner schema ref in array with description',
578+
ROUTE_WITH_ARRAY_OF_INNER_SCHEMA_REF_AND_DESCRIPTION,
579+
{
580+
openapi: '3.0.3',
581+
info: {
582+
title: 'Test',
583+
version: '1.0.0',
584+
},
585+
paths: {
586+
'/foo': {
587+
get: {
588+
summary: 'A simple route with type descriptions for references',
589+
operationId: 'api.v1.test',
590+
tags: ['Test Routes'],
591+
parameters: [],
592+
responses: {
593+
'200': {
594+
description: 'OK',
595+
content: {
596+
'application/json': {
597+
schema: {
598+
$ref: '#/components/schemas/SimpleRouteResponse',
599+
},
600+
},
601+
},
602+
},
603+
},
604+
},
605+
},
606+
},
607+
components: {
608+
schemas: {
609+
SimpleRouteResponse: {
610+
description: 'Human readable description of the Simple Route Response',
611+
properties: {
612+
test: {
613+
type: 'array',
614+
items: {
615+
allOf: [{ $ref: '#/components/schemas/TestRefBase' }],
616+
description: 'List of test refs',
617+
},
618+
},
619+
},
620+
required: ['test'],
621+
title: 'Human Readable Simple Route Response',
622+
type: 'object',
623+
},
624+
TestRefBase: {
625+
title: 'TestRefBase',
626+
type: 'string',
627+
},
628+
TestRef: {
629+
allOf: [
630+
{
631+
title: 'TestRef',
632+
},
633+
{
634+
$ref: '#/components/schemas/TestRefBase',
635+
},
636+
],
637+
},
638+
},
639+
},
640+
},
641+
);

0 commit comments

Comments
 (0)