Skip to content

Commit f40dc05

Browse files
committed
feat(openapi): add support for tuple and mixed arrays with prefixItems in schema
Closes #125, #111
1 parent d813dec commit f40dc05

File tree

4 files changed

+322
-0
lines changed

4 files changed

+322
-0
lines changed

docs/public/openapi-schemas.json

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,44 @@
121121
}
122122
}
123123
}
124+
},
125+
"/tuple-array": {
126+
"get": {
127+
"summary": "Get a tuple array with prefixItems",
128+
"operationId": "getTupleArray",
129+
"description": "Example of a JSON array with prefixItems defining a tuple structure.",
130+
"responses": {
131+
"200": {
132+
"description": "A tuple array with fixed positions",
133+
"content": {
134+
"application/json": {
135+
"schema": {
136+
"$ref": "#/components/schemas/TupleArray"
137+
}
138+
}
139+
}
140+
}
141+
}
142+
}
143+
},
144+
"/mixed-array": {
145+
"get": {
146+
"summary": "Get a mixed array with prefixItems and items",
147+
"operationId": "getMixedArray",
148+
"description": "Example of a JSON array with both prefixItems and items schemas.",
149+
"responses": {
150+
"200": {
151+
"description": "An array with fixed positions followed by additional items",
152+
"content": {
153+
"application/json": {
154+
"schema": {
155+
"$ref": "#/components/schemas/MixedArray"
156+
}
157+
}
158+
}
159+
}
160+
}
161+
}
124162
}
125163
},
126164
"components": {
@@ -380,6 +418,117 @@
380418
"type": "string"
381419
}
382420
}
421+
},
422+
"TupleArray": {
423+
"type": "array",
424+
"description": "A tuple array with fixed position types using prefixItems",
425+
"prefixItems": [
426+
{
427+
"type": "string",
428+
"description": "First element - always a string",
429+
"example": "username"
430+
},
431+
{
432+
"type": "number",
433+
"description": "Second element - always a number",
434+
"example": 42
435+
},
436+
{
437+
"type": "boolean",
438+
"description": "Third element - always a boolean",
439+
"example": true
440+
},
441+
{
442+
"type": "object",
443+
"description": "Fourth element - a complex object",
444+
"properties": {
445+
"id": {
446+
"type": "string",
447+
"example": "abc123"
448+
},
449+
"value": {
450+
"type": "number",
451+
"example": 99.5
452+
}
453+
}
454+
},
455+
{
456+
"type": "array",
457+
"description": "Fifth element - an array of strings",
458+
"items": {
459+
"type": "string"
460+
},
461+
"example": ["tag1", "tag2", "tag3"]
462+
}
463+
],
464+
"minItems": 5,
465+
"maxItems": 5
466+
},
467+
"MixedArray": {
468+
"type": "array",
469+
"description": "An array with both prefixItems and items schemas",
470+
"prefixItems": [
471+
{
472+
"type": "string",
473+
"description": "First element - always a string identifier",
474+
"example": "user-id-123"
475+
},
476+
{
477+
"type": "object",
478+
"description": "Second element - user metadata",
479+
"properties": {
480+
"name": {
481+
"type": "string",
482+
"example": "John Doe"
483+
},
484+
"role": {
485+
"type": "string",
486+
"enum": ["admin", "user", "guest"],
487+
"example": "admin"
488+
}
489+
}
490+
}
491+
],
492+
"items": {
493+
"type": "object",
494+
"description": "Additional elements - data entries",
495+
"properties": {
496+
"timestamp": {
497+
"type": "string",
498+
"format": "date-time",
499+
"example": "2023-01-15T14:30:00Z"
500+
},
501+
"value": {
502+
"type": "number",
503+
"example": 42.5
504+
},
505+
"tags": {
506+
"type": "array",
507+
"items": {
508+
"type": "string"
509+
},
510+
"example": ["important", "reviewed"]
511+
}
512+
}
513+
},
514+
"minItems": 2,
515+
"example": [
516+
"user-id-123",
517+
{
518+
"name": "John Doe",
519+
"role": "admin"
520+
},
521+
{
522+
"timestamp": "2023-01-15T14:30:00Z",
523+
"value": 42.5,
524+
"tags": ["important", "reviewed"]
525+
},
526+
{
527+
"timestamp": "2023-01-16T09:15:00Z",
528+
"value": 17.8,
529+
"tags": ["pending"]
530+
}
531+
]
383532
}
384533
}
385534
}

src/lib/examples/getSchemaUiJson.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ function uiPropertyArrayToJson(property: OAProperty, useExample: boolean): any {
5353
return [resolveOneOfProperty(property, useExample)]
5454
}
5555

56+
if (property.meta?.hasPrefixItems && property.properties && Array.isArray(property.properties)) {
57+
return property.properties.map((prop) => {
58+
return uiPropertyToJson(prop, useExample)
59+
})
60+
}
61+
5662
if (property.properties && Array.isArray(property.properties)) {
5763
return [uiPropertyObjectToJson(property.properties, useExample)]
5864
}

src/lib/parser/getSchemaUi.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ interface Metadata {
1212
isOneOf?: boolean
1313
isOneOfItem?: boolean
1414
isConstant?: boolean
15+
isPrefixItem?: boolean
16+
prefixItemIndex?: number
17+
hasPrefixItems?: boolean
18+
isAdditionalItems?: boolean
1519
extra?: Record<string, unknown>
1620
}
1721

@@ -185,6 +189,51 @@ class UiPropertyFactory {
185189
})
186190
}
187191
}
192+
193+
if (schema.prefixItems && Array.isArray(schema.prefixItems)) {
194+
property.properties = schema.prefixItems.map((prefixItem, index) => {
195+
const prefixItemProperty = UiPropertyFactory.schemaToUiProperty(
196+
`[${index}]`,
197+
prefixItem,
198+
false,
199+
)
200+
201+
prefixItemProperty.meta = {
202+
...(prefixItemProperty.meta || {}),
203+
isPrefixItem: true,
204+
prefixItemIndex: index,
205+
}
206+
207+
return prefixItemProperty
208+
})
209+
210+
property.meta = {
211+
...(property.meta || {}),
212+
hasPrefixItems: true,
213+
}
214+
}
215+
216+
// Handle case when both prefixItems and items are present.
217+
if (schema.prefixItems && Array.isArray(schema.prefixItems) && schema.items) {
218+
const additionalItemsProperty = UiPropertyFactory.schemaToUiProperty(
219+
'[n+]', // Name indicating "additional items".
220+
schema.items,
221+
false,
222+
)
223+
224+
additionalItemsProperty.meta = {
225+
...(additionalItemsProperty.meta || {}),
226+
isAdditionalItems: true,
227+
}
228+
229+
property.properties = [
230+
...(property.properties || []),
231+
additionalItemsProperty,
232+
]
233+
234+
// Don't set subtype when we have prefixItems.
235+
property.subtype = undefined
236+
}
188237
} else if (Array.isArray(schema.type) ? schema.type.includes('object') : schema.type === 'object') {
189238
property.properties = UiPropertyFactory.extractProperties(
190239
schema.properties,

test/lib/processOpenAPI/getSchemaUi.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,124 @@ const fixtures: Record<string, FixtureTest> = {
13391339
age: 0,
13401340
},
13411341
},
1342+
1343+
'array with prefixItems': {
1344+
jsonSchema: {
1345+
type: 'array',
1346+
prefixItems: [
1347+
{ type: 'string' },
1348+
{ type: 'number' },
1349+
{ type: 'boolean' },
1350+
],
1351+
},
1352+
schemaUi: {
1353+
name: '',
1354+
types: ['array'],
1355+
required: false,
1356+
properties: [
1357+
{
1358+
name: '[0]',
1359+
types: ['string'],
1360+
required: false,
1361+
meta: {
1362+
isPrefixItem: true,
1363+
prefixItemIndex: 0,
1364+
},
1365+
},
1366+
{
1367+
name: '[1]',
1368+
types: ['number'],
1369+
required: false,
1370+
meta: {
1371+
isPrefixItem: true,
1372+
prefixItemIndex: 1,
1373+
},
1374+
},
1375+
{
1376+
name: '[2]',
1377+
types: ['boolean'],
1378+
required: false,
1379+
meta: {
1380+
isPrefixItem: true,
1381+
prefixItemIndex: 2,
1382+
},
1383+
},
1384+
],
1385+
meta: {
1386+
hasPrefixItems: true,
1387+
},
1388+
},
1389+
schemaUiJson: [
1390+
'string',
1391+
0,
1392+
true,
1393+
],
1394+
},
1395+
1396+
'array with prefixItems and items': {
1397+
jsonSchema: {
1398+
type: 'array',
1399+
prefixItems: [
1400+
{ type: 'string' },
1401+
{ type: 'number' },
1402+
],
1403+
items: {
1404+
type: 'object',
1405+
properties: {
1406+
name: { type: 'string' },
1407+
age: { type: 'integer' },
1408+
},
1409+
},
1410+
},
1411+
schemaUi: {
1412+
name: '',
1413+
types: ['array'],
1414+
required: false,
1415+
properties: [
1416+
{
1417+
name: '[0]',
1418+
types: ['string'],
1419+
required: false,
1420+
meta: {
1421+
isPrefixItem: true,
1422+
prefixItemIndex: 0,
1423+
},
1424+
},
1425+
{
1426+
name: '[1]',
1427+
types: ['number'],
1428+
required: false,
1429+
meta: {
1430+
isPrefixItem: true,
1431+
prefixItemIndex: 1,
1432+
},
1433+
},
1434+
{
1435+
name: '[n+]',
1436+
types: ['object'],
1437+
required: false,
1438+
properties: [
1439+
{ name: 'name', types: ['string'], required: false },
1440+
{ name: 'age', types: ['integer'], required: false },
1441+
],
1442+
meta: {
1443+
isAdditionalItems: true,
1444+
},
1445+
},
1446+
],
1447+
meta: {
1448+
hasPrefixItems: true,
1449+
},
1450+
},
1451+
schemaUiJson: [
1452+
'string',
1453+
0,
1454+
{
1455+
name: 'string',
1456+
age: 0,
1457+
},
1458+
],
1459+
},
13421460
}
13431461

13441462
describe('getSchemaUi and getSchemaUiJson from fixtures', () => {

0 commit comments

Comments
 (0)